Blog

Setting up OpenVPN on Tomato firmware for WRT54GL and configure for Tunnelblick client

Setting up OpenVPN on a Tomato based router (e.g. WRT54GL) is actually pretty easy once you know the steps involved in generating the required server and client certificates. First, let’s download easy-rsa from Github, which makes the process of generating the required artefacts a lot easier and start with the server setup. Once we have the server running we will setup the client configuration to be used by Tunnelblick.

Server setup

Once you’ve downloaded and extracted easy-rsa we first need to set some required parameters. Do so by copying the existing vars_example file to vars:

cd ./easy-rsa-master/
cd ./easyrsa3/
cp vars.example vars

You now should have the following files inside easyrsa3:

MBP:easyrsa3 mak$ ls -alh
-rwxr-xr-x@  1 mak  staff    33K 14 Sep 09:50 vars
-rwxr-xr-x@  1 mak  staff   4,5K 14 Sep 09:50 openssl-1.0.cnf
-rwxr-xr-x@  1 mak  staff   7,9K 14 Sep 09:50 vars.example
drwxr-xr-x@  6 mak  staff   204B 14 Sep 09:50 x509-types

Vars file

Let’s edit the vars file:

Note: These parameters must match your OpenVPN server’s configuration, specially options like EASYRSA_ALGO in case you changed the default settings in Tomato.

I’ve extracted only the lines you are required to edit, feel free to adjust the other options too but make sure you know what you are doing 😉

set_var EASYRSA_REQ_COUNTRY	   "AT"
set_var EASYRSA_REQ_PROVINCE  "Steiermark"
set_var EASYRSA_REQ_CITY	   "Graz"
set_var EASYRSA_REQ_ORG	           "Your company"
set_var EASYRSA_REQ_EMAIL	   "you@your-company.com"
set_var EASYRSA_REQ_OU		   "Some OU"

set_var EASYRSA_KEY_SIZE	   4096

Public Key Infrastructure PKI

Now that we’ve setup easy-rsa it’s time to initialize the public key infrastructure PKI:

MBP:easyrsa3 mak$ ./easyrsa init-pki

Note: using Easy-RSA configuration from: ./vars

init-pki complete; you may now create a CA or requests.
Your newly created PKI dir is: /Users/mak/easy-rsa-master/easyrsa3/pki

Certificate Authority CA

Then build the certificate authority CA:

MBP:easyrsa3 mak$ ./easyrsa build-ca

Note: using Easy-RSA configuration from: ./vars
Generating a 4096 bit RSA private key
....................................++
....................................
writing new private key to '/Users/mak/easy-rsa-master/easyrsa3/pki/private/ca.key'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [Easy-RSA CA]:YOURSERVER

CA creation complete and you may now import and sign cert requests.
Your new CA certificate file for publishing is at:
/Users/mak/easy-rsa-master/easyrsa3/pki/ca.crt

Server key

Now it’s time to build the server key:

MBP:easyrsa3 mak$ ./easyrsa build-server-full YOURSERVER

Note: using Easy-RSA configuration from: ./vars
Generating a 4096 bit RSA private key
............................................................++
......................................................++
writing new private key to '/Users/mak/easy-rsa-master/easyrsa3/pki/private/YOURSERVER.key'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
Using configuration from /Users/mak/easy-rsa-master/easyrsa3/openssl-1.0.cnf
Enter pass phrase for /Users/mak/easy-rsa-master/easyrsa3/pki/private/ca.key:
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :ASN.1 12:'YOURSERVER'
Certificate is to be certified until Dec  6 07:38:24 2024 GMT (3650 days)

Write out database with 1 new entries
Data Base Updated

In order for Tomato to be able to use your key we need to remove the passphrase. Otherwise OpenVPN will fail to start since it does not offer a way to enter the passphrase interactively:

MBP:easyrsa3 mak$ cd pki/private
MBP:private mak$ openssl rsa -in YOURSERVER.key -out YOURSERVER_NO_PASS.key 
Enter pass phrase for YOURSERVER.key:
writing RSA key

Diffie Hellman prime number

For OpenVPN to work we need a Diffie Hellman configuration, which will take a long time for the 4096 bit long prime number generation to complete:

MBP:easyrsa3 mak$ ./easyrsa gen-dh

Note: using Easy-RSA configuration from: ./vars
Generating DH parameters, 4096 bit long safe prime, generator 2
This is going to take a long time
.................................................+.

Enter artefacts into tomato

Finally, simply set the generated artefacts in the OpenVPN configuration in the tomato admin backend: openvpn-config

 

openvpn-config-2

openvpn-config-3

openvpn-config-4 For the keys section you need the content of the following artefacts:

  • ca.crt
  • YOURSERVER.crt
  • YOURSERVER_NO_PASS.key
  • dh.pem

Client setup

Now that we have the server setup propery it’s time to generate client certificates.

Client certificate

Note: You should always generate separate certificates for your clients!

MBP:easyrsa3 mak$ ./easyrsa build-client-full some_user

Note: using Easy-RSA configuration from: ./vars
Generating a 4096 bit RSA private key
..++
....................................++
writing new private key to '/Users/mak/easy-rsa-master/easyrsa3/pki/private/some_user.key'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
Using configuration from /Users/mak/easy-rsa-master/easyrsa3/openssl-1.0.cnf
Enter pass phrase for /Users/mak/easy-rsa-master/easyrsa3/pki/private/ca.key:
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :ASN.1 12:'some_user'
Certificate is to be certified until Dec  7 07:38:55 2024 GMT (3650 days)

Write out database with 1 new entries
Data Base Updated

Now we simply remove the passphrase from the client key like we did with the server key:

openssl rsa -in some_user.key -out some_user_NO_PASS.key 

OpenVPN configuration file

Next, we need to create a .ovpn configuration file, based on the settings in tomato from earlier, e.g. some_user.ovpn:

##############################################
# Sample client-side OpenVPN 2.0 config file #
# for connecting to multi-client server.     #
#                                            #
# This configuration can be used by multiple #
# clients, however each client should have   #
# its own cert and key files.                #
#                                            #
# On Windows, you might want to rename this  #
# file so it has a .ovpn extension           #
##############################################

# Specify that we are a client and that we
# will be pulling certain config file directives
# from the server.
client

# Use the same setting as you are using on
# the server.
# On most systems, the VPN will not function
# unless you partially or fully disable
# the firewall for the TUN/TAP interface.
;dev tun
dev tap

# Windows needs the TAP-Win32 adapter name
# from the Network Connections panel
# if you have more than one.  On XP SP2,
# you may need to disable the firewall
# for the TAP adapter.
;dev-node MyTap

# Are we connecting to a TCP or
# UDP server?  Use the same setting as
# on the server.
proto udp

# The hostname/IP and port of the server.
# You can have multiple remote entries
# to load balance between the servers.
remote YOURSERVER 1194

# Choose a random host from the remote
# list for load-balancing.  Otherwise
# try hosts in the order specified.
;remote-random

# Keep trying indefinitely to resolve the
# host name of the OpenVPN server.  Very useful
# on machines which are not permanently connected
# to the internet such as laptops.
resolv-retry infinite

# Most clients don't need to bind to
# a specific local port number.
nobind

# Downgrade privileges after initialization (non-Windows only)
;user nobody
;group nobody

# Try to preserve some state across restarts.
persist-key
persist-tun

# If you are connecting through an
# HTTP proxy to reach the actual OpenVPN
# server, put the proxy server/IP and
# port number here.  See the man page
# if your proxy server requires
# authentication.
;http-proxy-retry # retry on connection failures
;http-proxy [proxy server] [proxy port #]

# Wireless networks often produce a lot
# of duplicate packets.  Set this flag
# to silence duplicate packet warnings.
;mute-replay-warnings

# SSL/TLS parms.
# See the server config file for more
# description.  It's best to use
# a separate .crt/.key file pair
# for each client.  A single ca
# file can be used for all clients.
ca ca.crt
cert client.crt
key client.key

# Verify server certificate by checking
# that the certicate has the nsCertType
# field set to "server".  This is an
# important precaution to protect against
# a potential attack discussed here:
#  http://openvpn.net/howto.html#mitm
#
# To use this feature, you will need to generate
# your server certificates with the nsCertType
# field set to "server".  The build-key-server
# script in the easy-rsa folder will do this.
;ns-cert-type server

# If a tls-auth key is used on the server
# then every client must also have the key.
;tls-auth ta.key 1

# Select a cryptographic cipher.
# If the cipher option is used on the server
# then you must also specify it here.
;cipher x

# Enable compression on the VPN link.
# Don't enable this unless it is also
# enabled in the server config file.
comp-lzo

# Set log file verbosity.
verb 3

# Silence repeating messages
;mute 20

As you can see this configuration file references several other files:

  • ca.crt
  • client.crt
  • client.key

Of course you need to use the actual user files (e.g. some_user_NO_PASS.key) here. It’s best to keep the artefact filenames generic. Thus, simply copy some_user_NO_PASS.key to client.key. To make things even easier simply create a folder that holds all your user’s configuration files, e.g. some_user:

OpenVPN client configuration folder

 

Setup Tunnelblick in Mac

In order to distribute your client configuration folder just rename it to end with .tblk to be used by Tunnelblick for Mac:

OpenVPN client configuration .tblk Tunnelblick

In order to install the client configuration for Tunnelblick simply double-click the .tblk file and you are all set!

Adding advanced formatting to Harshen’s jQuery countdown timer plugin

Harshen’s jQuery countdown timer plugin works great and is simple to use. In a recent project we needed to use special formatting for the countdown timer to be implemented. Although, Harshen’s solution offer to timeSeparator option to specify the separator string to be used this was not flexible enough. The customer wanted the countdown timer to display the remainder time in the format:

ddd / ddh / ddm / dds

Although, using the timeSeparator option it is possible to get the separating “/” to work, we needed the days(d), hours(h), minutes(m) and seconds(s) formatted using HTML.

The plugin extension

Having a quick look at the plugin to only thing to do was to overwrite the $this.html() functionality by a custom function that takes care of processing additional formatting rules. In order to keep the formatting as flexible as possible I opted to add regulare expression support through additional options:

  1. regexpMatchFormat
  2. regexpReplaceWith

regexpMatchFormat specifies the format to match the original output string produced by the plugin. regexpReplaceWith then takes care of replacing the original string with the desired regular expression format. Furthermore, an additional function called html was introduced:

/**
 * Replaces content by optional regular expression specified via options 
 * regexpMatchFormat and regexpReplaceWith.
 * @param Object $this
 * @param String content
 */  
function html($this, content) {
  var processedContent = content;

  if (typeof window['regexpMatchFormat_' + $this.attr('id')] !== 'undefined' &&
      typeof window['regexpReplaceWith_' + $this.attr('id')] !== 'undefined') {
    var regexp = new RegExp(window['regexpMatchFormat_' + $this.attr('id')]);
    processedContent = content.replace(regexp,
      window['regexpReplaceWith_' + $this.attr('id')]);
  }

  $this.html(processedContent);
}

Handling of the addtional two options was added in the countdown function:

var regexpMatchFormat = "";
var regexpReplaceWith = "";
if (options.regexpMatchFormat != undefined && options.regexpReplaceWith != undefined) {
  window['regexpMatchFormat_' + $this.attr('id')] = options.regexpMatchFormat;
  window['regexpReplaceWith_' + $this.attr('id')] = options.regexpReplaceWith;
}

Below you find the regular expression used for this project’s requirements:

<script type="text/javascript">// <![CDATA[
jQuery(document).ready(function ($) {
    $('#future_date').countdowntimer({
      dateAndTime: "2015/01/01 00:00:00",
      size: "lg",
      regexpMatchFormat: "([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})",
      regexpReplaceWith: "$1<sup>d</sup> / $2<sup>h</sup> / $3<sup>m</sup> / $4<sup>s</sup>"
     });
    });
// ]]></script>

Thanks again to Harshen for the very nice plugin 🙂

Process product attribute HTML values in Magento using template processor

In case you are wondering how to parse HTML content of product attributes here is how:

// load product model resource
$productResource = Mage::getSingleton('catalog/product')->getResource();
  
// load HTML product attribute content and process using CMS template processor
$helper = Mage::helper('cms');
$processor = $helper->getPageTemplateProcessor();
$attributeContent = $processor->filter(Mage::registry('current_product')->getSomeAttributeByCode());

Using this approach you can of course process CMS block template placeholders too.

Magento produces duplicate customer EAV entries instead of updating existing ones

In a recent Magento 1.8.0.0 project updating customer entities resulted in duplicate EAV entries instead of updating existing ones. Some of the symptoms of this problem were:

  • Customer attributes not being updated, instead new duplicate entries produced
  • Password reset not working, instead duplicate entries produced
  • Login not working

In general, anything related to updating customer EAV entries caused duplicate entries. Furthermore, although new (duplicate) entries were generated still only the old values were taken into consideration. Thus, having a closer look at the corresponding database tables revealed that the unique indexes over entity_id and attribute_id for these tables were missing, e.g. customer_address_entity_datetime:

Magento missing unique index

… which actually should look like this:

magento-unique-indexes

Due to the missing unique index over entity_id and attribute_id columns (here UNQ_CUSTOMER_ADDRESS_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID) entries were not updated but rather duplicated, as shown below for entity_id 37339 and attribute_id 29:

magento-missing-unique-indexes-duplicate-values

Apart from the missing unique index also the index for entity_id was missing too.

How to fix it

Note: Make sure to backup your database before adding the required indexes!

We need to add unique indexes over entity_id and attribute_id for all EAV tables. This includes tables ending in

  1. _entity_datetime
  2. _entity_decimal
  3. _entity_int
  4. _entity_text
  5. _entity_varchar

But, since duplicate entries already exist we first need to clean up the corresponding tables.

Remove duplicate entries

In order to remove duplicates while keeping the newest values only we can use the SQL query for customer_address_entity_int, as shown below:

Again: Make sure to backup your database first!

DELETE caei1 
FROM customer_address_entity_int caei1,
customer_address_entity_int caei2 
WHERE caei1.value_id < caei2.value_id 
AND caei1.`entity_type_id` = caei2.`entity_type_id` 
AND caei1.`attribute_id` = caei2.`attribute_id` 
AND caei1.`entity_id` = caei2.`entity_id`;

In case you want to test it with one entity_id first simple append e.g.:

 
 ... AND caei1.`entity_id` = XYZ

Do this step for all related EAV tables.

Add unique indexes

Now that duplicates have been removed we can add the required unique indexes over entity_id and attribute_id for all related tables. For instance, the following query adds a unique index over entity_id and attribute_id for table customer_entity_varchar:

 
ALTER TABLE `magento`.`customer_entity_varchar` 
ADD UNIQUE `UNQ_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID` ( `entity_id` , `attribute_id` ) 

Furthermore, check that there exists an index for entity_id too (this was missing in this setup too):

ALTER TABLE `customer_entity_varchar` 
ADD INDEX `IDX_CUSTOMER_ENTITY_VARCHAR_ENTITY_ID` ( `entity_id` )

That’s it! Based on the added (and required) unique indexes Magento will automatically overwrite any existing EAV entrues, thus updating them instead of creating new ones.

Resize root Partition on Raspbian to use free space

Raspbian comes pre-configured to run on smaller SD cards. Thus, the default partition sizes are generally configured to not exceed 4GB. In case you want to use SD cards larger than 4GB (e.g. 32GB) you will definitely want to use the free unallocated space, right? Luckily, this can be achieved rather easily. The default Raspbian partition schema looks something like the following using a 32GB sd card:

pi@raspberrypi / $ sudo fdisk -c -l /dev/mmcblk0

Disk /dev/mmcblk0: 31.3 GB, 31322013696 bytes
4 heads, 16 sectors/track, 955872 cylinders, total 61175808 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00090806

Device Boot      Start         End      Blocks   Id  System
/dev/mmcblk0p1            8192      122879       57344    c  W95 FAT32 (LBA)
/dev/mmcblk0p2          122880     6399999     3138560   83  Linux

Although you could manually resize your partitions using fdisk Raspbian comes with a handy tool called raspi-config that will take care of this process for you:

raspi-config

All you need to do in order to resize your root partition is to select Expand Filesystem in the menu (see screenshot). raspi-config takes care of the rest for you, pretty neat, right? After a reboot all of your free disk space will be used:

pi@raspberrypi ~ $ df -h
Filesystem      Size  Used Avail Use% Mounted on
rootfs           29G  2.3G   26G   9% /
/dev/root        29G  2.3G   26G   9% /
devtmpfs        215M     0  215M   0% /dev
tmpfs            44M  208K   44M   1% /run
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs            88M     0   88M   0% /run/shm
/dev/mmcblk0p1   56M  9.7M   47M  18% /boot

In case you are still interested in manually resizing your partitions you might want to have a look at e.g. Chris Newland’s post. Enjoy!

Good Bye HTML5 – Welcome HTML5.1!

As you might have already heard the W3C has finally officially published a recommendation for the (15 years old) HTML5 standard.

HTML5

In fact, HTML 5.0 has (co-)existed for quite some time now. Numerous frameworks implemented HTML5 features long before the official recommendation (as usual).

And be honest, who hasn’t used at least some of the features HTML 5 has to offer?

So, what about HTML5.1? Well, it for sure will bring new features. But, one of the core aspects is the built-in support for DRM which should end the era of additional browser plugins forced on users especially by streaming providers.

But it will definitely take some time before we get to officially use HTML5.1 😉

Good bye HTML5 – and Welcome HTML5.1!

Can’t upload product image on eBay Error using M2ePro on Magento

After a switch to a https only setup on a Magento 1.8.0 installation using M2ePro 6.1.6 the following error ocurred when trying to list items using images:

Can’t upload product image on eBay

A quick Google search revealed the following official statement from the company behind M2ePro:

This problem does not concern to m2e pro.

The setup here uses 301 redirects to permanently redirect http requests to https, in addition to Magento being set to use secure URLs only. Despite Magento`s setting to force links to be generated https-only a quick code review of M2EPro revealed that image URLs are generated for http only. In fact, possible https links are replaced by the http ones, as shown in the prepareImageUrl method in class Ess_M2ePro_Model_Magento_Product:

private function prepareImageUrl($url) {
  if (!is_string($url) || $url == '') {
    return '';
  }

  return str_replace(array('https://', ' '), array('http://', '%20'), $url);
}

Now the first test was to change the str_replace(), thus forcing image URLs to be created https only:

return str_replace(array('http://', ' '), array('https://', '%20'), $url);

This forces links to be prefixed with the https:// protocol. Now it should work, right? Since M2ePro receives working secure image URLs that it can forward to eBay which in return fetches them on demand. Wrong! It seems like there is a general problem with eBay being able to process https image URLs. So, currently the only option seems to allow unsecure http image URLs to be fetched by eBay. An statement from eBay support did not resolve the problem at hand:

If the direct image upload is working and you are able to list products it’s not a problem related to eBay.

I will report back in case the SSL problem with image URLs get resolved.

JIRA WAR Setup NoClassDefFoundError TransactionUtil Exception

When receiving a NoClassDefFoundError TransactionUtil exception when setting up JIRA using WAR make sure that all required libraries are installed.

java.lang.NoClassDefFoundError: org/ofbiz/core/entity/TransactionUtil
	com.atlassian.jira.web.filters.steps.requestcleanup.RequestCleanupStep.finallyAfterDoFilter(RequestCleanupStep.java:63)
	com.atlassian.jira.web.filters.steps.ChainedFilterStepRunner.doFilter(ChainedFilterStepRunner.java:85)
	com.atlassian.core.filters.cache.AbstractCachingFilter.doFilter(AbstractCachingFilter.java:33)
	com.atlassian.core.filters.AbstractHttpFilter.doFilter(AbstractHttpFilter.java:31)
	com.atlassian.core.filters.encoding.AbstractEncodingFilter.doFilter(AbstractEncodingFilter.java:41)
	com.atlassian.core.filters.AbstractHttpFilter.doFilter(AbstractHttpFilter.java:31)
	com.atlassian.jira.web.filters.PathMatchingEncodingFilter.doFilter(PathMatchingEncodingFilter.java:49)
	com.atlassian.core.filters.AbstractHttpFilter.doFilter(AbstractHttpFilter.java:31)
	com.atlassian.jira.startup.JiraStartupChecklistFilter.doFilter(JiraStartupChecklistFilter.java:74)
	com.atlassian.multitenant.servlet.MultiTenantServletFilter.doFilter(MultiTenantServletFilter.java:91)
	com.atlassian.jira.web.filters.steps.ChainedFilterStepRunner.doFilter(ChainedFilterStepRunner.java:78)

Multiple Google AdSense ad blocks on same page are causing error 400 Bad Response

When trying to include multiple Google AdSense ad blocks using the code provided by Google on the same page you will likely get a 400 Bad Response error.

This is caused by including adsbygoogle.js multiple times:

<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>

Thus, simply include adsbygoogle.js only once and use the ad generation code where applicable:

<ins class="adsbygoogle"
     style="display:inline-block;width:728px;height:90px"
     data-ad-client="XYZ"
     data-ad-slot="XYZ"></ins>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>

Hint: When using WordPress and custom themes you can simply add the required Adsense JS file adsbygoogle.js using the following code in your theme’s functions.php file:

function add_google_adsense_js() {
    wp_enqueue_script( 'google_adsense_js', '//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js');
}

add_action( 'wp_enqueue_scripts', 'add_google_adsense_js' ); 

Doctrine 2 Exception EntityManager is closed

Doctrine 2’s EntityManager class will permanently close connections upon failed transactions. Thus, further requests using these closed instances will fail with the following exception:

Doctrine\\ORM\\ORMException' with message 'The EntityManager is closed.

So, make sure to check your EntityManager’s state before your actual tasks:

if (!$entityManager->isOpen()) {
  $entityManager = $entityManager->create(
    $entityManager->getConnection(), $entityManager->getConfiguration());
}

Better, create a function called getEntityManager that takes care of this check and makes sure that you always get a viable instance of EntityManager to work with:

private static function getEntityManager() {
  if (!self::$entityManager->isOpen()) {
    self::$entityManager = self::$entityManager->create(
      self::$entityManager->getConnection(), self::$entityManager->getConfiguration());
  }

  return self::$entityManager;
}

So you can use:

self::$entityManager = self::getEntityManager();
self::$entityManager->persist($some_entity_instance);
self::$entityManager->flush();

Thats’s it.

Enable Address Fields in Magento Customer Registration Forms

In order to enable address fields in Magento customer registration forms you only need to enable the attribute setShowAddressField.

To do so either add the following in your template or in the base local.xml (/app/design/frontend/base/default/layout/local.xml):

<customer_account_create> 
  <reference name="customer_form_register"> 
    <action method="setShowAddressFields">
      <param>true</param>
    </action> 
  </reference>
</customer_account_create>

As always, make sure to reload your cache afterwards. This setting will enable to execution of

<?php if($this->getShowAddressFields()): ?>

in register.phtml (/app/design/frontend/base/default/template/customer/form/register.phtml). That’s it 🙂

503 Error Back-end server is at capacity AWS ELB

When receiving Back-end server is at capacity 503 errors from your Elastic Load Balancer (ELB) instance in the Amazon Web Services (AWS) EC2 be sure to check your HealthyHostCount monitoring setup in the CloudWatch. ELB instances will return 503 errors (apart from other conditions) when your HealthyHostCount monitoring setup has reached the threshold set and stop any further processing, while the backend service might still work correctly which might cause you some unexpected headache.

Thus, check that the file/path you’ve set (e.g. /ping.php) can be reached by the ELB-HealthChecker and that the HTTP code returned is 200. So, for instance redirects to the monitoring file (e.g. /ping.php) will be treated as an error by HealthChecker (HTTP status != 200). In order to check if your configured health monitoring works (HTTP status == 200) as usual check your server logs for ELB-HealthChecker entries:

grep ELB-HealthChecker /var/log/apache2/*

x.y.z.1 - - [28/Aug/2014:12:38:01 +0200] "GET /ping.php HTTP/1.1" 301 464 "-" "ELB-HealthChecker/1.0"
x.y.z.2 - - [28/Aug/2014:12:38:11 +0200] "GET /ping.php HTTP/1.1" 301 464 "-" "ELB-HealthChecker/1.0"
x.y.z.1 - - [28/Aug/2014:12:38:31 +0200] "GET /ping.php HTTP/1.1" 301 448 "-" "ELB-HealthChecker/1.0"
x.y.z.2 - - [28/Aug/2014:12:38:41 +0200] "GET /ping.php HTTP/1.1" 200 183 "-" "ELB-HealthChecker/1.0"
x.y.z.1 - - [28/Aug/2014:12:39:01 +0200] "GET /ping.php HTTP/1.1" 200 183 "-" "ELB-HealthChecker/1.0"
x.y.z.2 - - [28/Aug/2014:12:39:11 +0200] "GET /ping.php HTTP/1.1" 200 239 "-" "ELB-HealthChecker/1.0"

As you can see here a redirect caused a 301 HTTP status to be returned for the first three entries in the list, which automatically incremented the UnHealthyHostCount in CloudWatch and reduced the HealthyHostCount. For further info visit the ELB Developer Guide.