Category: "LAMP"

Windows 7 Dual-Boot CentOS 6.4 on an External USB Drive

Before you do this, make recovery disk(s) for Windows 7. Unless you already have them. You may want to backup anything you have on the Windows 7 drive, but if you’re only using it to run browsers, you haven’t invested that much anyway. Make sure you have some sort of recovery disks or you will either have to buy them or pay someone to fix your disk. Label the disk. Eventually.

The first thing you’ll need for this is a CentOS 6.4 LiveCD. Go to one of the CentOS mirrors (http://www.centos.org/modules/tinycontent/index.php?id=15) and use the following URL pattern: http://mirror.example.com/centos/6/isos/i386/CentOS-6.4-i386-LiveCD.iso. If you have a 64-bit machine, use x86_64. You can also use a USB to boot, I recommend http://unetbootin.sourceforge.net/

Install CentOS on the external drive. I recommend doing a minimal install to get the boot loader set up, then you can either reinstall or add packages to get the system set up the way you want it to.

Once CentOS is on the drive, try to boot from it. You’ll probably have to press a key (on this Asus laptop it is Esc) to choose which drive to boot from. If it won’t boot, you’ll need to adjust the grub settings.

Apparently you can’t change the Windows 7 boot loader. I’m not going to claim this is an authoritative statement, however, installing grub on the Windows 7 drive caused it to fail to boot with a ‘Hard disk error’ (or something similar). This required the Windows 7 recovery disks to recover.

Therefore, you must put the bootloader on the external drive and configure the BIOS to try to boot from the external drive first, with an option to go to Windows.

The problem I had was that by booting off a USB stick the device numbers were a bit off.

Once I had grub loaded on the external drive, I use the find command to determine how the disk was referenced. Then I manually edited the device.map file and grub.conf files after booting into the LiveCD.

For an Asus laptop with Windows 7 on the internal hard disk, and an external USB disk drive, the device.map file looked like this:

It wasn’t really generated by anaconda since I edited it, but that’s okay.

# this device map was generated by anaconda
(hd0)     /dev/sdb

I edited grub.conf using hd0 to refer to the external drive and hd1 to refer to the internal drive.

# grub.conf generated by anaconda
#
# Note that you do not have to rerun grub after making changes to this file
# NOTICE:  You have a /boot partition.  This means that
#          all kernel and initrd paths are relative to /boot/, eg.
#          root (hd0,0)
#          kernel /vmlinuz-version ro root=/dev/mapper/vg_asuslaptopcentos-lv_ro
ot
#          initrd /initrd-[generic-]version.img
#boot=/dev/sda1
default=0
timeout=5
splashimage=(hd0,0)/grub/splash.xpm.gz
hiddenmenu
title CentOS (2.6.32-358.6.2.el6.i686)
	root (hd0,0)
	kernel /vmlinuz-2.6.32-358.6.2.el6.i686 ro root=/dev/mapper/vg_asuslapto
pcentos-lv_root rd_NO_LUKS LANG=en_US.UTF-8 rd_NO_MD rd_LVM_LV=vg_asuslaptopcent
os/lv_swap SYSFONT=latarcyrheb-sun16 crashkernel=auto rd_LVM_LV=vg_asuslaptopcen
tos/lv_root  KEYBOARDTYPE=pc KEYTABLE=us rd_NO_DM rhgb quiet
	initrd /initramfs-2.6.32-358.6.2.el6.i686.img
title CentOS (2.6.32-358.el6.i686)
	root (hd0,0)
	kernel /vmlinuz-2.6.32-358.el6.i686 ro root=/dev/mapper/vg_asuslaptopcen
tos-lv_root rd_NO_LUKS LANG=en_US.UTF-8 rd_NO_MD rd_LVM_LV=vg_asuslaptopcentos/l
v_swap SYSFONT=latarcyrheb-sun16 crashkernel=auto rd_LVM_LV=vg_asuslaptopcentos/
lv_root  KEYBOARDTYPE=pc KEYTABLE=us rd_NO_DM rhgb quiet
	initrd /initramfs-2.6.32-358.el6.i686.img
title Windows 7
	rootnoverify (hd1,1)
	chainloader +1

What I learned:

Drive references may vary based on how the machine was booted.

You can’t just plug an external drive into a different machine and work with it, you may mess up the other machine.

Don’t change the Windows 7 boot loader, although you can use EasyBCD to recover. Maybe.

Taking the time to read the grub documentation is well worth the investment.

eZ Publish - LiteSpeed - Conditional header

I have an eZ Publish site, running in host access mode, with three subdomains, admin, mobile and down.

I don’t want the subdomains to be indexed by search engines, and I especially want to keep the admin interface out of the search engines.

Somehow, the admin interface got into Google. I updated the robots.txt file, but that only prevents further scans, it won’t cause pages to be removed (http://support.google.com/webmasters/bin/answer.py?hl=en&answer=156449).

To prevent the subdomains from being listed in Google search results, I tried to use these directives on a LiteSpeed server:

SetEnvIfNoCase Host ^(admin|mobile|down).*$ NO_INDEX=true
Header set X-Robots-Tag "noindex" env=NO_INDEX

It seems that the conditional always evaluates to true (or is ignored).

In that case, placing the Header directive in a VirtualHost section would be the best approach, but I don’t have access to the server configuration. I sent a ticket in to the hosting company, but they’re really busy. That’s a nice way of saying they didn’t fix it quickly.

One of the nicest features of eZ is its robust configuration, which includes HTTP headers. Adding these settings in siteaccess/admin/site.ini.append.php worked.

[HTTPHeaderSettings]
CustomHeader=enabled
OnlyForAnonymous=enabled
HeaderList[]=X-Robots-Tag
X-Robots-Tag[]
X-Robots-Tag[/]=noindex, nofollow

To remove the admin interface from Google, I’ll send them a request, just to be sure.

Deny Access Based on Accept-Language Header

It can be difficult to determine the source of a request to a server. You can use geolocation to lookup an IP address, one excellent service is IP2Location.

However, if the goal is to deny access to servers from outside the country, language may be an effective identifier.

Whether the language is sent in the headers is an unknown, however, if it is sent, and it isn’t the language of the site, access can be denied with these commands:

In this case, if ‘en’ (English) is not one of the languages the client will accept, the request will be denied.

        RewriteCond %{HTTP:Accept-Language} !(en) [NC]
        RewriteRule ^.*  - [F]

This should be part of a layered strategy, with other rules ensuring additional protection.

A word of caution. If the Accept-Language header is missing, access will also be denied. Search engine bots often omit the Accept-Language header.

eZ Publish Workflow - electronic media delivery

I’m helping on a site that sells music.

I wanted a way to bundle a person’s purchases into a single file that could be downloaded once. There are two types of electronic media, mp3_product and album. Each may have an image associated with it. An mp3_product is an .mp3 files, an album is a .zip file, containing .mp3s.

The link above is a great resource. Using it, I created a custom workflow:

<?php
 
class CreateDownloadFileType extends eZWorkflowEventType
{
    const WORKFLOW_TYPE_STRING = "createdownloadfile";
    public function __construct()
    {
        parent::__construct( CreateDownloadFileType::WORKFLOW_TYPE_STRING, 'Create Download File');
    }
 
    public function execute( $process, $event )
    {
        $parameters = $process->attribute( 'parameter_list' );

	$order_id = $parameters['order_id'];
	$user_id = $parameters['user_id'];

	$order = eZShopFunctionCollection::fetchOrder ($order_id);

	$items = eZProductCollectionItem::fetchList( array( 'productcollection_id' => $order['result']->ProductCollectionID ) );

	$order_number = $order['result']->OrderNr;

	$zip_file='/home/account/www/download/'.md5($order_number.$user_id).'.zip';
	$zip = new ZipArchive();
	if ($zip->open($zip_file,ZIPARCHIVE::CREATE)!==true) {
		eZDebug::writeError('Order: '.$order_id.' - could not create zip file '.$zip_file);
	}

	$storageDir = eZSys::storageDirectory();
	$attributes_to_include = array('file','image');

	foreach ($items as $item)
	{
		$product = eZContentObject::fetch($item->ContentObjectID);

		if (in_array($product->ClassIdentifier,array('mp3_product','album')))
		{
			$product_data = $product->dataMap();
			$name = preg_replace('/[^\w]/','_',trim($product_data['name']->DataText));

			foreach ($attributes_to_include as $v)
			{
				if ($product_data[$v]!==null)
				{
					$attribute_data = $product_data[$v]->Content();
					$file = $attribute_data->Filename;
					$mime_type = $attribute_data->MimeType;
				        list( $group, $type ) = explode( '/', $mime_type );
					$filePath = $storageDir . '/original/' . $group . '/' . $file;
					$pathInfo = pathinfo($filePath);
					if ($zip->addFile($filePath,$name.'.'.$pathInfo['extension'])!==true) {
						eZDebug::writeError('Order: '.$order_id.' - failed to add '.$filePath.' to '.$zip_file);
					}
				}
			}
		}
		$zip->close();
	}

        return eZWorkflowType::STATUS_ACCEPTED;
    }
}
eZWorkflowEventType::registerEventType( CreateDownloadFileType::WORKFLOW_TYPE_STRING, 'createdownloadfiletype' );
?>

The zip file created is named with an md5 of the order number and user id, and placed in a download directory. It’s probably not the most secure approach, but I think it is okay for this application, and it can be changed later.

This triggers on shop/checkout/after, which means the person has paid.

The download link for the zip file is placed in both the order view and order email templates.

    {def $current_user=fetch( 'user', 'current_user' )}
    {if $current_user.is_logged_in}
    <h2>{"Download"|i18n("design/base/shop")}:</h2>
    <p><a href="/download/{concat($order.order_nr,$current_user.contentobject_id)|md5()}.zip">{"Click to download"|i18n("design/base/shop")}</
a></p>
    {/if}
    {undef $current_user}

You may need an Alias and a RewriteRule:


Alias download /home/account/www/download
RewriteRule ^download/.*\.zip - [L]

The final step was to create a mechanism to delete the files after they are downloaded. There are at least two ways to do this. The first approach is to use an Apache filter, with a bash script:

Apache configuration code:

ExtFilterDefine delete mode=output
cmd="/home/account/webfilter.sh"

<Directory /home/account/www/download>
SetOutputFilter delete
</Directory>


FILENAME="/home/account/www$DOCUMENT_URI";
cat -
if [ -e $FILENAME ]; then
        rm $FILENAME;
fi

The Apache filter code is courtesy of Lyrix, Inc..

If you can’t use a filter - which is likely if you are using shared hosting, you can also use a PHP script. This uses a RewriteRule to route the request through PHP.

Options -Indexes
<IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteRule ^(.*\.zip)$ zip.php?path=$1 [L]
</IfModule>

Finally, this is the PHP code that will deliver the file, then delete it.

<?php
// downloading a file and then deleting it
$filename = $_GET['path'];
if (is_file($filename))
{
        // fix for IE catching or PHP bug issue
        header("Pragma: public");
        header("Expires: 0");
        header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
        header("Cache-Control: public");
        header("Content-Description: File Transfer");
        // browser must download file from server instead of cache

        // force download dialog
        header("Content-Type: application/zip");

        // use the Content-Disposition header to supply a recommended filename and
        // force the browser to display the save dialog.
        header("Content-Disposition: attachment; filename=".basename($filename).";");

        /*
        The Content-transfer-encoding header should be binary, since the file will be read
        directly from the disk and the raw bytes passed to the downloading computer.
        The Content-length header is useful to set for downloads. The browser will be able to
        show a progress meter as a file downloads. The content-lenght can be determines by
        filesize function returns the size of a file.
        */
        header("Content-Transfer-Encoding: binary");
        header("Content-Length: ".filesize($filename));

        @readfile($filename);
        unlink($filename);
        exit();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Requested Content Not Available</title>
</head>
<body>
<h1>Requested Content Not Available</h1>
<p>The content you requested is not available.</p>
<p>If you were unable to download your purchases, please contact us for assistance.</p>
</body>
</html>

Installing ffmpeg-php on CentOS 5.8

Follow the instructions in the link above, they’re excellent.

If you get a few compile errors related to missing files, add these RPMs (excerpt from /var/log/yum.log):

faad2-devel-2.7-1.el5.rf.i386
x264-devel-0.0.0-0.4.20101111.el5.rf.i386
lame-devel-3.99.5-1.el5.rf.i386
a52dec-devel-0.7.4-8.el5.rf.i386
dirac-devel-1.0.2-1.el5.rf.i386
faac-devel-1.26-1.el5.rf.i386
alsa-lib-devel-1.0.17-1.el5.i386
SDL-devel-1.2.10-9.el5.i386
gsm-devel-1.0.13-1.el5.rf.i386
opencore-amr-devel-0.1.2-1.el5.rf.i386
imlib2-1.4.4-1.el5.rf.i386
imlib2-devel-1.4.4-1.el5.rf.i386
ffmpeg-devel-0.6.5-1.el5.rf.i386
libXpm-devel-3.5.5-3.i386
gd-devel-2.0.33-9.4.el5_4.2.i386

I recommend using the package manager, it will save you a lot of time.

If you get an error referencing PIX_FMT_RGBA32, follow the instructions at: http://blog.hostonnet.com/ffmpeg-php-error-pix_fmt_rgba32