Welcome Guest, Not a member yet? Register   Sign In
Forcing HTTPS for certain routes using customised Config class and hook
#1

[eluser]Andrew G.[/eluser]
It is quite common that on a site there a certain urls that should only be accessible via https. This requires:
- making sure that whenever a link to that page is created it points to the https:// address
- if an attempt is made to access it at its non-https address the user is automatically redirected to the https address

I have implemented such a solution by customising the Config class so that the site_url function will create https urls as required and created a hook so that any http requests to the configured pages are redirected to https.

For this solution to work you must create all the links to <em>page urls</em> using the site_url method defined in the url helper which should be auto loaded. (By <em>page urls</em> I mean urls that people click on or forms post to rather than urls for js, css, image files etc which should be relative so they work on http and https pages).

Both the MY_Config class and the hook use a config file called 'ssl.php'. This file defines routes as follows:

Code:
$config['ssl_routes'] = array(
    'account/settings',
    'account/sign_in',
    'account/create/:any'
    );

Note the use of ':any' which works the same way it does in the routes.php file. (as does :num)

MyConfig.php looks like this:
Code:
class MY_Config extends CI_Config {


    protected $_ssl_routes = null;

    /**
     * Site URL
     *
     * @access    public
     * @param    string    the URI string
     * @return    string
     */
    function site_url($uri = '')
    {
        if (is_array($uri))
        {
            $uri = implode('/', $uri);
        }

        //load routes from config if not already loaded
        $this->_ssl_routes = $this->ssl_routes();

        //if there are no ssl routes then just call the parent.
        if ($this->_ssl_routes === FALSE) return parent::site_url($uri);


        //see if the current url matches any of the routes        
        foreach ($this->_ssl_routes as $route)
        {
            if (preg_match('#^'.$route.'$#', $uri))
            {            
                $ssl_base_url = str_replace('http://', 'https://', $this->slash_item('base_url'));

                if ($uri == '')
                {
                    return $base_url.$this->item('index_page');
                }
                else
                {
                    $suffix = ($this->item('url_suffix') == FALSE) ? '' : $this->item('url_suffix');
                    return $ssl_base_url.$this->slash_item('index_page').preg_replace("|^/*(.+?)/*$|", "\\1", $uri).$suffix;
                }
            }
        }

        return parent::site_url($uri);
    }

    function ssl_routes()
    {
        if (is_null($this->_ssl_routes))
        {
            $this->_ssl_routes = $this->item('ssl_routes');

            if (is_array($this->_ssl_routes))
            {
                for ($i = 0; $i < count($this->_ssl_routes); $i++)
                {
                    // Convert wild-cards to RegEx
                    $this->_ssl_routes[$i] = str_replace(':any', '.+', str_replace(':num', '[0-9]+', $this->_ssl_routes[$i]));
                }
            }
        }

        return $this->_ssl_routes;
    }
}

This file needs to be in application/libraries/ as it overrides the default Config class


Finally, there is the 'post_controller_constructor' hook defined in application/config/hooks.php:
Code:
$hook['post_controller_constructor'][] = array(
                                'function' => 'force_ssl',
                                'filename' => 'ssl.php',
                                'filepath' => 'hooks'
                                );

The function class is implemented in application/hooks/ssl.php as follows:
Code:
function force_ssl()
{
    //if this is a HTTPS request we don't need to test anything
    if (!empty($_SERVER['HTTPS'])) return;

    //get the HTTPS routes from the Config class
    $CI =& get_instance();
    $ssl_routes = $CI->config->ssl_routes();
    if (!is_array($ssl_routes)) return;

    //see if any match the current uri
    $uri = ltrim($CI->uri->uri_string(), '/');
    foreach ($ssl_routes as $route)
    {
        if (preg_match('#^'.$route.'$#', $uri))
        {
            redirect($uri); //will get rewritten by overloaded MY_Config method
        }
    }
}

Why is this a post_controller_constructor hook rather than a pre_controller hook? Because get_instance doesn't seem to work until then. Haven't really looked into the details but if you have stuff in your Controller constructor that is https dependent then you can try to solve that problem.

Final note ... make sure that hooks are enabled in your config.php file :-)
#2

[eluser]Phil Sturgeon[/eluser]
Thumbs up from me.

I would rather have this running pre_controller though. You can access the Config library using:

Code:
$CI_config =& load_class('Config');

I am nowhere near a set-up I can try this out on, could you have a pop and let me know how it goes?
#3

[eluser]Andrew G.[/eluser]
Took slightly more than a <em>pop</em> but here's a new version that works as a <b>precontroller</b> hook:

<b>application/libraries/MY_Config.php</b>
Code:
&lt;?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class MY_Config extends CI_Config {


    protected $_ssl_routes = null;

    function MY_Config()
    {
        parent::CI_Config();
    }

    /**
     * Site URL
     *
     * @access    public
     * @param    string    the URI string
     * @return    string
     */
    function site_url($uri = '')
    {
        if (is_array($uri))
        {
            $uri = implode('/', $uri);
        }

        //load routes from config if not already loaded
        $this->_ssl_routes = $this->ssl_routes();

        //if there are no ssl routes then just call the parent.
        if ($this->_ssl_routes === FALSE) return parent::site_url($uri);


        //see if the current url matches any of the routes        
        foreach ($this->_ssl_routes as $route)
        {
            if (preg_match('#^'.$route.'$#', $uri))
            {            
                $ssl_base_url = str_replace('http://', 'https://', $this->slash_item('base_url'));

                if ($uri == '')
                {
                    return $base_url.$this->item('index_page');
                }
                else
                {
                    $suffix = ($this->item('url_suffix') == FALSE) ? '' : $this->item('url_suffix');
                    return $ssl_base_url.$this->slash_item('index_page').preg_replace("|^/*(.+?)/*$|", "\\1", $uri).$suffix;
                }
            }
        }

        return parent::site_url($uri);
    }

    function ssl_routes()
    {
        if (is_null($this->_ssl_routes))
        {
            $this->load('ssl');
            $this->_ssl_routes = $this->item('ssl_routes');

            if (is_array($this->_ssl_routes))
            {
                for ($i = 0; $i < count($this->_ssl_routes); $i++)
                {
                    // Convert wild-cards to RegEx
                    $this->_ssl_routes[$i] = str_replace(':any', '.+', str_replace(':num', '[0-9]+', $this->_ssl_routes[$i]));
                }
            }
        }

        return $this->_ssl_routes;
    }
}

I had to modify this class to manually load the ssl.php config file as this was originally autoloaded by the Controller.

The hook function now looks like this...

<b>application/hooks/ssl.php</b>
Code:
&lt;?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

function force_ssl()
{
    //if this is a HTTPS request we don't need to test anything
    if (!empty($_SERVER['HTTPS'])) return;

    //get the HTTPS routes from the Config class
    $config =& load_class('Config');
    $uri =& load_class('URI');
    $ssl_routes = $config->ssl_routes();

    if (!is_array($ssl_routes)) return;

    //check to see if the current uri should be https
    $uri_string = ltrim($uri->uri_string(), '/');
    foreach ($ssl_routes as $route)
    {
        if (preg_match('#^'.$route.'$#', $uri_string))
        {
            $redir_to = $config->site_url($uri_string);
            header("Location: ".$redir_to, TRUE, 301);
            exit;
        }
    }
}

Note, in this version I can't use the redirect function from the url_helper because the helpers are loaded by the Controller.

Update hook configuration is:

<b>application/config/hooks.php</b>
Code:
$hook['pre_controller'][] = array(
                                'function' => 'force_ssl',
                                'filename' => 'ssl.php',
                                'filepath' => 'hooks'
                                );
#4

[eluser]KeithB[/eluser]
This is exactly what I needed, thanks for posting. I altered the code slightly, by creating a new config item called "secure_base_url" and setting this to "https://[url]", and then changed the overloaded site_url() function to perform the replacement on the result of the parent function if necessary, so:

Code:
foreach ($this->item('secure_functions') as $fn)
        {
            if (preg_match('#^'.$fn.'#', $uri))
            {
                return preg_replace('#^'.$this->item('base_url').'#',
                                    $this->item('secure_base_url'),
                                    parent::site_url($uri),
                                    1);
            }
        }

        return parent::site_url($uri);

My intention with this is to allow the CodeIgniter implementation of site_url() to change without having to alter my own library. If there's anything wrong with doing it this way though, please let me know. I used the post constructor hook that you posted with very few changes. The site now comprises two index.php pages, one in the standard folder and one in the secure folder, but both use the same application and system directories, so the only duplication of code is the index page itself. All pages can be accessed through https if required, but the secure pages get redirected from http if entered into the address bar. Also, all anchor links to secure functions are automatically prefixed with https. Very cool.

Thanks again for posting this. It's proven far cleaner than any other solutions that I've found.
#5

[eluser]Andrew G.[/eluser]
Keith,
Nice work, it's always good to call the parent function when overriding. I just submitted to the temptation to cut and paste code on the first hack ;-)

Glad you found this useful ... makes me think I should start posting more of my code here :-)
#6

[eluser]mact1079[/eluser]
Referring to thread http://ellislab.com/forums/viewthread/83154/P15 and post by huyl on Nov 16, 2010, s/he noticed that any POST data would get lost with the redirect taking place from http to https using the force_ssl_helper.

Quote:When the login form is submitted, browser sends a request using POST method to http://192.168.1.1/index.php/welcome/authenticate, and it is redirected to https://192.168.1.1/index.php/welcome/authenticate at once, because this action requires SSL. Unfortunately, I can not retrieve email and password any more.

Does the method in this thread address this issue? I could probably implement and test but figured I'd ask first.

Hopefully in a future version of CI there is a nice class that does all this. It seems like something so many sites need that it should be built in.

#7

[eluser]Aken[/eluser]
If you require HTTPS for your POST data, you should be ON an https page FIRST before sending the data. Sending it over HTTP and then redirecting to HTTPS is just as insecure as not using HTTPS in the first place.




Theme © iAndrew 2016 - Forum software by © MyBB