Posted on 1 Comment

Setting up Collabora CODE with NextCloud using Apache reverse proxy on Debian 8 Jessie

Setting up Collabora Online Development Edition (CODE) can be a little tricky. This guide shows the steps needed to get Collabora CODE working using an Apache 2.2 reverse proxy on Debian 8 Jessie.

The steps we are going to have a look at are as follows:

  1. Setup Apache reverse proxy
  2. Setup Collabora CODE based on official Docker image
  3. Install and configure NextCloud Collabora CODE plugin

The basic configuration we are trying to achieve here is:

  1. Setup a secure domain for accessing Collabora from NextCloud
    1. This will be https://office.yourserver.com
    2. I’m assuming that you already have a working SSL certificate for this domain. If not, have a look at Let’s Encrypt in case you want a cheap solution. For Debian 8 Jessie have a look at the certbot guide.
  2. Setup a reverse proxy configuration for this domain that fowards requests to Collabora webservice endpoints
  3. Access Collabora CODE Docker container through Apache reverse proxy which itself exclusively listens an a secure line on Port 9980 (default)
  4. Install the Collabora NextCloud plugin and configure it to access Collabora through our reverse proxy

Setup Collabora CODE based on official Docker image

Collabora can be either installed using a package provided by your distribution or by using the official Docker image collabora/code.

Normally, the Docker container setup should be pretty pain free. Having said that, for Debian 8 Jessie you need to adjust the storage driver to devicemapper as it seems that the default docker storage driver AUFS and Debian 8 do not work together.

Adjust Docker storage driver to devicemapper

The steps required are again pretty straight forward. First get current ExecStart from your docker.service file:

grep ExecStart /lib/systemd/system/docker.service

Example output:
ExecStart=/usr/bin/dockerd -H fd://

Then use this result to create a systemd Docker drop-in configuration file and create the service directory first if it does not yet exist as well:

mkdir /etc/systemd/system/docker.service.d
editor /etc/systemd/system/docker.service.d/execWithDeviceMapper.conf

Put the following content in execWithDeviceMapper.conf:

[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --storage-driver=devicemapper -H fd://.

Finally, restart systemd, docker.service and possibly your existing Collabora container if you had one running:

systemctl daemon-reload
systemctl restart docker.service

Disclaimer: For higher volume production sites you definitely want to optimize this setup (…).

The command the start your Collabora Docker container is as follows:

docker run -t -d -p 127.0.0.1:9980:9980 \
-e 'domain=www\\.yournextcloud1\\.com\|www\\.yournextcloud2\\.com' \
--restart always --cap-add MKNOD collabora/code

Note that I’ve provided two domains in the above command to show how to enable multiple domains to access your Collabora web service.

As always, since the Docker container starts in detached mode make sure to check for possible problems using

docker logs YOUR_CONTAINER_ID

Now that we have Collabora CODE up and running as Docker container we need to make it available to the outside world using an Apache reverse proxy.

Setup Apache reverse proxy

First and foremost, I will not cover the exact steps to setup the base Apache web server here but provide a working vhost configuration.

Required Apache modules

The additional Apache module requirements to get Apache working as reverse proxy for Collabora CODE are:

  1. mod_proxy

  2. mod_proxy_http

  3. mod_proxy_wstunnel

  4. mod_ssl

Apart from mod_proxy_wstunnel the configuration steps should be pretty straight forward. When using Apache 2.2 and mod_proxy_wstunnel on the other hand things can get a little more tricky since you need to apply a patch and compile the module yourself. Have a look at the very handy guide by waleedsamy on github to compile mod_proxy_wstunnel yourself.

Apache Reverse Proxy vhost configuration

Once all requirements are satisfied we can setup the vhost configuration for the Apache reverse proxy domain.

Remember, our internet-facing domain for accessing Collabora CODE will be office.yourserver.com. This will be the basis for your vhost configuration below.

UseCanonicalName off
ServerName office.yourserver.com

# Enable and configure SSL/TLS
SSLEngine on
SSLCertificateFile yourserver-cert
SSLCertificateKeyFile yourserver-key
SSLCertificateChainFile yourserver-cacert

SSLProtocol all -SSLv2 -SSLv3
SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES25$
SSLHonorCipherOrder on
SetEnvIf User-Agent ".*MSIE.*" \
nokeepalive ssl-unclean-shutdown \
downgrade-1.0 force-response-1.0

# Encoded slashes need to be allowed
AllowEncodedSlashes NoDecode

# Enable and configure SSL Proxy
SSLProxyEngine On
SSLProxyVerify None
SSLProxyCheckPeerCN Off

# Make sure to keep the host
ProxyPreserveHost On

# static html, js, images, etc. served from loolwsd
# loleaflet is the client part of LibreOffice Online
ProxyPass /loleaflet https://127.0.0.1:9980/loleaflet retry=0
ProxyPassReverse /loleaflet https://127.0.0.1:9980/loleaflet

# WOPI discovery URL
ProxyPass /hosting/discovery https://127.0.0.1:9980/hosting/discovery retry=0
ProxyPassReverse /hosting/discovery https://127.0.0.1:9980/hosting/discovery

# Main websocket
ProxyPassMatch "/lool/(.*)/ws$" wss://127.0.0.1:9980/lool/$1/ws nocanon

# Admin Console websocket
ProxyPass /lool/adminws wss://127.0.0.1:9980/lool/adminws

# Download as, Fullscreen presentation and Image upload operations
ProxyPass /lool https://127.0.0.1:9980/lool
ProxyPassReverse /lool https://127.0.0.1:9980/lool

Check if your reverse proxy is working by accessing the WOPI discovery URL:

https://office.yourserver.com/hosting/discovery

If that gives you the corresponding XML namespace information you should be good to go.

Install and configure NextCloud Collabora CODE plugin

This is the last step required and should be the easiest one.

  1. Go to the Apps section and choose “Office & Text”
  2. Install the “Collabora Online” app
  3. In Admin -> Collabora Online specific the server you have setup before (https://office.yourserver.com)

Finally, try to create and edit a document via NextCloud. Enjoy your private Collabora setup using NextCloud!

For more information have a look at the official Collabora CODE documentation.

Posted on Leave a comment

Setting up Varnish caching server for pd-admin with Apache and php-fcgi

Deploying Varnish caching server can significantly speed up web-based applications, as well as simple websites. Varnish supports different back-end systems, ranging from the popular Apache web server to the more efficient nginx. Especially when it comes to handling high traffic sites Varnish can bring a considerable uplift to the responsiveness by e.g. caching entire pages (i.e. full-page cache).

In order to improve the responsiveness of a couple of high traffic websites running on pd-admin (an advanced collection of tools to administrate web- and mail hosting on Linux-based servers) Varnish 4 was implemented. The following steps describe the process of setting up Varnish caching server for pd-admin with Apache and php-fcgi on a Debian 7 server.

Setup Varnish on Debian 7 64bit

Let’s have a quick look at the process of installing varnish on a Debian 7 64 bit machine. Basically, you need to do 5 things:

  1. Install https support for apt-get
  2. Add GPG key for apt-get
  3. Add the repository to apt-get packages source list
  4. Refresh package source list
  5. Install Varnish

Thus, in order to setup Varnish on Debian 7 64 bit execute the following commands:

  1. apt-get install apt-transport-https
  2. curl https://repo.varnish-cache.org/GPG-key.txt | apt-key add –
  3. echo “deb https://repo.varnish-cache.org/debian/ wheezy varnish-4.0” >> /etc/apt/sources.list.d/varnish-cache.list
  4. apt-get update
  5. apt-get install varnish

That’s it. Varnish should be installed on ready to be configured.

Configure Varnish through VCL – default.vcl

Now that Varnish is installed it’s time to configure it. This can be done by editing default.vcl – the default configuration file automatically created during the installation:

#
# This is an example VCL file for Varnish.
#
# It does not do anything by default, delegating control to the
# builtin VCL. The builtin VCL is called when there is no explicit
# return statement.
#
# See the VCL chapters in the Users Guide at https://www.varnish-cache.org/docs/
# and http://varnish-cache.org/trac/wiki/VCLExamples for more examples.

vcl 4.0;

backend default {
 .host = "127.0.0.1";
 .port = "80";
}

sub vcl_recv {
 # Happens before we check if we have this in cache already.
 #
 # Typically you clean up the request here, removing cookies you don't need,
 # rewriting the request, etc.
}

sub vcl_backend_response {
 # Happens after we have read the response headers from the backend.
 #
 # Here you clean the response headers, removing silly Set-Cookie headers
 # and other mistakes your backend does.
}

sub vcl_deliver {
 # Happens when we have all the pieces we need, and are about to send the
 # response to the client.
 #
 # You can do accounting or modifying the final object here.
}

As starting point have a look at the very handy Varnish 4.0 configuration template which works out of the box for

  • WordPress
  • Drupal (works decently for Drupal 7, depends on your modules obviously)
  • Joomla (WIP)
  • Fork CMS
  • OpenPhoto

and additional configuration setups like

  • Server-side URL rewriting
  • Clean error pages for debugging
  • Virtual Host implementations
  • Various header normalizations
  • Cookie manipulations
  • 301/302 redirects from within Varnish

Have a look at the Varnish configuration documentation to adjust it to your specific needs. In general, only few adaptions should be required using this template.

Set correct backend hostname to solve 403 forbidden error code

In case you get a 403 forbidden error make sure to set the correct backend hostname or IP in default.vcl:

backend server1 { # Define one backend
  .host = "USE_DOMAIN_SET_IN_HTTPD_CONF"; # IP or Hostname of backend
  ...
}

Have a look at pd-admin’s httpd.conf for the currently set hostname to be used by Varnish, or open up the web administration console and check the server name option. Also make sure to have set the X-Forwarded-For header (especially for Varnish 3):


req.http.x-forwarded-for = client.ip

That should solve the 403 forbidden error for cached domains.

Exclude Domains from Varnish Caching

Varnish by default caches all requests that are not excluded specifically. Thus, in case you want to exclude domains and simple URLs from being cached by Varnish you can specify them in vcl_recv() function like so:


if (req.http.host == "www.domain.com" && req.url == "/") {
return (pass);
}

This will redirect specific domains and/or URLs to the pass() function thus by-passing the caching mechanism.

Change Varnish port

Finally, you need to set Varnish to listen on the default http port (i.e. 80) and change Apache’s listener port to something different, e.g. 81 and set it as the Varnish backend port in default.vcl. Make sure to restart Apache and Varnish. That’s it!

Posted on Leave a comment

Disable SSLv3 support for Apache

In case you haven’t disabled support for SSLv3 for Apache yet – do so now! You can easily disable SSLv3 using your Apache configuration httpd.conf using the option -SSLv3:

SSLHonorCipherOrder on
SSLProtocol -ALL -SSLv3 +TLSv1 +TLSv1.1 +TLSv1.2
SSLCipherSuite ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS

As always, make sure to restart Apache afterwards. Note that depending on your setup you might need to set the list of supported protocols for each vhost entry separately.

Test your configuration

Test your site’s security status to conform to best practice

  1. certificates
  2. protocol support
  3. key exchange
  4. cipher strength

at Qualys SSLLabs. SSL Analyzer. This tool will check various parameters and provide you with an overall rating: Qualys SSL Lab Test Results

Posted on Leave a comment

Remove Passphrase from SSL Keys

Source Code Icon

When it comes to generating SSL keys passphrases play an immanent role. They need to be specified when creating SSL keys and are checked each time the key is being used to ensure authorized access. For instance, when starting your Apache web server with a SSL certificate you will need to enter the original passphrase to verify authorized access. The following simple step shows you how to remove passphrase from SSL keys.

If you don’t already have a SSL key create a 2048 bit RSA key with triple DES block ciphering first and specify your passphrase as usual:

openssl genrsa -des3 -out your-server.key 2048

Of course you can choose any other modulus bits count and ciphering mode to generate your SSL key. Then, make a backup of the original certificate with the passphrase still set just in case:

cp your-server.key your-server.key.WITH_PASS

Remove Passphrase

And finally remove passphrase from your SSL key:

openssl rsa -in your-server.key.WITH_PASS -out your-server.key.WITHOUT_PASS

Now you can use this key without requiring the enter the passphrase on every single use, e.g. when Apache web server starts, etc. That’s it.

Posted on 1 Comment

Enabling Cross-Origin Resource Sharing CORS for Apache

Apache Logo

When trying to share resources across different domains (host-a.com vs. host-b.com) you will come across the concept of Cross-Origin Resource Sharing (CORS). In order to be able to share resources across different domains you will most likely need to enable to CORS manually on your server. For instance, when a script on host-a.com tries to access resources on host-b.com CORS needs to be enabled on host-b.com. This post shows you the steps needed for enabling Cross-Origin Resource Sharing CORS for Apache using a .htaccess file.

Note: Looking for a way to enable CORS for PHP? Have a look at Enabling Cross-Origin Resource Sharing CORS for PHP.

Prepare your .htaccess

In order to to enable CORS on Apache you need mod_headers and mod_setenvif. The example below show you how to prepare Apache for the two types of CORS requests:

  1. Pre-Flight Requests
  2. Simple Requests

Pre-Flight Requests are generated on the client side when not using GET, POST and HEAD requests on external domain resources (or setting custom headers). In this case we need to check Access-Control-Allow-Headers and Access-Control-Allow-Methods in order to allow or disallow CORS requests to our server. Second, simple requests handle GET, POST and HEAD requests across different domains. In this case we need to check the Origin header and set Access-Control-Allow-Origin correspondingly to allow such CORS requests. Below you find examples to allow CORS for method-a Pre-Flight Requests and simple requests from host-a.local or host-b.local, optionally for accessing different remote scripts too (e.g. https://host-b.local/method-a.php).



   ##########################################################################
   # 1.) ENABLE CORS PRE-FLIGHT REQUESTS
   # e.g. PUT, DELETE, OPTIONS, ...
   # we need to set Access-Control-Allow-Headers and 
   # Access-Control-Allow-Methods for allowed domain(s)
   ##########################################################################
   
   # first check for pre-flight headers and set as environment variables
   # e.g. header method-a is set here
   SetEnvIf ^Access-Control-Request-Method$ "method-a" METHOD_A
   SetEnvIf ^Access-Control-Request-Headers$ "^Content-Type$" HEADER_A

   # set corresponding response pre-flight headers for allowed domain(s)
   Header set Access-Control-Request-Methods "method-a" env=METHOD_A
   Header set Access-Control-Request-Headers "content-type" env=HEADER_A
   
   # TODO: add allowed additional pre-flight requests here...
   
   #########################################################################
   # 2.) ENABLE CORS *SIMPLE REQUESTS* (vs. Pre-Flight Requests from above)
   # e.g. GET, POST and HEAD requests
   # we need to set Access-Control-Allow-Origin header for allowed domain(s)
   # also note that POST requests need to match one of the following 
   # Content-Type: 
   # 1) application/x-www-form-urlencoded
   # 2) multipart/form-data
   # 3) text/plain
   #########################################################################
   
   # write line for each domain you would like to enable CORS requests for
   # e.g. origin = http://host-a.local or http://host-b.local 
   SetEnvIfNoCase Origin "((http(s?))?://(www\.)?(host\-a|host\-b)\.local)(:\d+)?$" AccessControlAllowOrigin=$0
   Header set Access-Control-Allow-Origin %{AccessControlAllowOrigin}e env=AccessControlAllowOrigin
   
   # e.g. origin = https://host-b.local/method-a.php
   SetEnvIfNoCase Origin "https://host-b.local/method-a.php" AccessControlAllowOrigin=$0
   Header set Access-Control-Allow-Origin %{AccessControlAllowOrigin}e env=AccessControlAllowOrigin
   
   # generic regexp match for more flexibel use cases
   #SetEnvIfNoCase Origin "((http(s?))?://(www\.)?(host\-a|host\-b)\.local)(:\d+)?$" AccessControlAllowOrigin=$0
   #Header set Access-Control-Allow-Origin %{AccessControlAllowOrigin}e env=AccessControlAllowOrigin
 
   # TODO: add additional allowed simple requests here...
 

Test File

In case you want to test this CORS setup here’s a simple test file:

<script src="//code.jquery.com/jquery-1.11.0.min.js"></script><script>// <![CDATA[
function a() {
var jqxhr = $.get( "http://host-b.local", function() {
})
.done(function() {
alert( "second success" );
})
.fail(function() {
alert( "error" );
})
}

function b() {
var jqxhr = $.get( "http://host-b.local/method-b", function() {
})
.done(function() {
alert( "second success" );
})
.fail(function() {
alert( "error" );
})
}
// ]]></script><a>Host B (GET)</a> <a>Host B / Method A (GET)</a>

Be sure to configure your Apache VHost settings accordingly.

Improvements

Since we don’t want to manually edit .htaccess files a simple configuration script would be nice to set domain configurations automatically for CORS enabled domains/methods.

Posted on 8 Comments

Setup AWStats in XAMPP for Offline Apache Log File Analysis

AWStats Logo

Recently I was asked to do Apache log file analysis for a customer portal. Up until then no log file analysis has been done for this particular site and only raw Apache log data was available. Thus, I needed to find a way to be able to do offline Apache log file analysis to satisfy to following demands:

  1. Unique hits
  2. Time of visit
  3. Amount of pages served
  4. Duration of stay
  5. Download count
  6. Traffic produced
  7. Geographic Distribution

The easiest way to accomplish this task would be do feed the log files to an existing analysis tool, such as AWStats (or Webalizer in case you don’t like AWStats which represents a viable alternative).

Furthermore, with reusability in mind I wanted to accomplish a setup that could be easily re-used for analyzing log files from other sites later on as well. Consequently, I decided to setup AWStats in conjunction with XAMPP based on a virtual host environment.

This article explains the steps required to setup AWStats in XAMPP, as well as how to actually do log analysis based on different sets of log files.

The Requirements

Please bear in mind that some requirements need to be met in order to run this setup:

  1. XAMPP (or standalone Apache installation)
  2. AWStats
  3. Perl
  4. Apache Log File(s)
  5. Optional: Apache virtual host configuration

One of the benefits of using XAMPP instead of a plain Apache HTTP server is that it already contains a working Perl installation, which comes in handy if you are looking for a jump-start.

The Setup

First, you need to setup XAMPP. For information on how to do is please refer to the official setup guide on Windows. Then, you need to install and configure AWStats to be used in your XAMPP environment. In order for AWStats to get to work you also need to setup Perl. Fortunately, XAMPP already includes Perl so you won’t need to install it yourself. In case you decide against XAMPP there are a lot of tutorials explaining the steps to get Perl running with Apache.

So basically, after installing XAMPP everything boils down to setting up AWStats to be used in combination with XAMPP. As there are numerous ways to do so, the following merely represents one possible working solution:

  1. Extract AWStats to a temporary location, e.g. /tmp/awstats-7.0
  2. Create a vhost entry with the following settings:
    <VirtualHost *:80>
      ServerName awstats.local
      DocumentRoot "C:/awstats"
    	
      ScriptAlias /cgi-bin/ "C:/awstats/cgi-bin/"
      Alias /icons "C:/awstats/icon/"
    
      <Directory "C:/awstats">
        AllowOverride All
        Allow from All
      </Directory>
    </VirtualHost>
    
  3. For the vhost entry above to work you need to edit your hosts file, e.g. in Windows add 127.0.0.1 awstats.local to C:\Windows\System32\drivers\etc\hosts
  4. Copy contents of /tmp/awstats-7.0/wwwroot/cgi-bin to the cgi-bin ScriptAlias folder specified in vhost config file, e.g. C:/awstats/cgi-bin/
  5. Copy /tmp/awstats-7.0/wwwroot/icon to the icons Alias folder specified in vhost config file, e.g. C:/awstats/icon/
  6. Edit the path to the Perl install in cgi-bin/awstats.pl, e.g. on Windows instead of #!/usr/bin/perl use #!c:\xampp\perl\bin\perl.exe when using XAMPP
  7. For each site to analyze create an AWStats model file using awstats.model.conf as reference (see cgi-bin folder), e.g. awstats.my-site.conf and be sure to set the following options:
    • LogFile, e.g. “C:/awstats/cgi-bin/logs/access.my-site.log”
    • DirData, e.g. “C:/awstats_data” (make sure this folder exists before running AWStats
    • SiteDomain, this must conform with the domain used in your log file, e.g. “my-site.com”
    • DirIcons, specifies to path to the icons Alias previously set, i.e. “/icons”

That’s should suffice for AWStats to run. Now we are able to generate the analytics data by issuing the following command:

perl awstats.pl -config=my-site -update

Finally, in order to view the nicely formatted output produced by AWStats we can run the web-view:

http://awstats.local/cgi-bin/awstats.pl?config=my-site

Analyzing Multiple Sites

Based on this setup you can easily analyze further log files from other sites by creating separate model files (e.g. awstats.my-other-site.conf) and placing them in the cgi-bin folder of the vhost directory and finnally running the command to generate analysis data.

That’s all it takes to setup AWStats in combination with XAMPP to do offline Apache log file analysis for multiple sites without further configuration.

Posted on Leave a comment

Process Apache Environment Variables using PHP

Apache Logo

Especially when in comes to switching between different working environments such as development, staging and production Apache’s enviroment variables represent a vital solution. But be advised that they do not work out of the box, as certain preconditions must be met.

Enable mod_env

First of all, you need to enable mod_env for your Apache setup. Secondly, if you are running PHP not as Apache module but rather using FastCGI you need to follow the steps provided here. Once enabled, you can easily set your enviroment variables in your httpd.conf or .htaccess file directly:

SetEnv APPLICATION_ENV development

In order to get previously set environment variables from within PHP simply use the following function:

 getenv() 

That’s it! Pretty easy, right?

Posted on 2 Comments

Custom Apache Directory Listing using PHP

Apache Logo

This snippet overrides Apache’s default presentation of folder contents.

/**
 * CustomDirectoryListing
 *
 * A simple script for custom directory listing. Adds support for
 * - maximum amount of items per row
 * - image listing with automatic resizing
 * - displaying images before other file types for a prettier layout
 *
 * The above options can be controlled via the corresponding $_GET variables,
 * @see below.
 *
 * USAGE
 * Simply put this file into the directory you want to be listed. You might want
 * to set the defaultWidth and defaultHeight, as well as maxItemsPerRow to your
 * needs.
 *
 * @author Matthias Kerstner info@kerstner.at
 * @copyright Matthias Kerstner info@kerstner.at
 * @version 1.0
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
 */

mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');

$maxItemsPerRow = (isset($_GET['maxItemsPerRow']) &amp;&amp; (int)$_GET['maxItemsPerRow'] &gt; 0)
        ? (int)$_GET['maxItemsPerRow']-1 : 4;
$defaultWidth = (isset($_GET['defaultWidth']) &amp;&amp; (int)$_GET['defaultWidth'] &gt; 0)
        ? (int)$_GET['defaultWidth'] : 150;
$defaultHeight = (isset($_GET['defaultHeight']) &amp;&amp; (int)$_GET['defaultHeight'] &gt; 0)
        ? (int)$_GET['defaultHeight'] : 100;
$imagesFirst = isset($_GET['imagesFirst']);

/**
 * Calculates resized size based on $dstHeight and $dstWidth by maintaining the
 * original width-height-ratio. Checks which dimension is greater and sets it to
 * a fixed value specified. The remaining dimension is resized using the
 * original ratio to the other dimension's new fixed value.
 * @param &lt;string&gt; $srcFile
 * @param &lt;int&gt; $dstHeight
 * @param &lt;int&gt; $dstWidth
 * @return &lt;array&gt; [0]=width, [1]=height
 */
function getResizeSize($srcFile, $dstHeight, $dstWidth) {

    $srcFileInfo = getimagesize($srcFile); //[0]=width,[1]=height

    if(empty($srcFileInfo))
        throw new Exception('Failed to read image fileinfo.');

    if($srcFileInfo[0] &lt;= (int)$dstWidth &amp;&amp;
            $srcFileInfo[1] &lt;= (int)$dstHeight) {
        return array($srcFileInfo[0], $srcFileInfo[1]); //nothing to do
    }

    $newSize = array();

    if($srcFileInfo[0] &gt;= $srcFileInfo[1]) { //calculate new dimensions while keeping current ratio
        $newSize[0] = $dstWidth;
        $newSize[1] = ($srcFileInfo[1]/$srcFileInfo[0]) * $dstWidth;
    } else {
        $newSize[0] = ($srcFileInfo[0]/$srcFileInfo[1]) * $dstHeight;
        $newSize[1] = $dstHeight;
    }

    return $newSize;
}

?>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Custom Directory Listing</title>
        <style type="text/css">
            html {
                font-family: Courier, Verdana, sans-serif;
                font-size: 9pt;
                border: 0;
            }
        </style>
    </head>
    <body>

        <?php

        try {

            $handle = opendir('.');
            $file = null;

            if(!$handle)
                throw new Exception('Failed to read directory.');

            $nonImageItems = array();
            $itemPerRowCount = 0;

            while(false !== ($file = readdir($handle))) {

                if($itemPerRowCount >= $maxItemsPerRow) {
                    echo '<br/>';
                    $itemPerRowCount = 0;
                }

                if($file !== "." && $file !== "..") {

                    if(mb_eregi('.jpg$', $file) === 1) { //image

                        $filesize = getResizeSize($file, $defaultWidth, $defaultHeight);

                        echo '<a href="'.$file.'">'.
                                '<img src="'.$file.'" width="'.$filesize[0].'" '.
                                'height="'.$filesize[1].'" alt="'.$file.'" '.
                                'border="0"/>'.
                                '</a>&nbsp;&nbsp;';

                        $itemPerRowCount++;
                    } else {
                        if($imagesFirst)
                            $nonImageItems[] = $file;
                        else {
                            echo '<a href="'.$file.'">'.$file.'</a>&nbsp;&nbsp;';
                            $itemPerRowCount++;
                        }
                    }
                }
            }

            closedir($handle);

            $itemPerRowCount = 0;

            foreach($nonImageItems as $v) { //display non-image files afterwards
                if($itemPerRowCount >= $maxItemsPerRow) {
                    echo '<br/>';
                    $itemPerRowCount = 0;
                }

                echo '<a href="'.$v.'">'.$v.'</a>&nbsp;&nbsp;';

                $itemPerRowCount++;
            }

        } catch(Exception $e) {
            echo 'Sorry an error occurred: '.$e->getMessage();
        }

        ?>

    </body>
</html>