Filter any HTTP request URI?

I want to filter any HTTP request URI done through the HTTP API.

Use cases:

Read More
  1. The WordPress update check goes to http://api.wordpress.org/core/version-check/1.6/, but https://api.wordpress.org/core/version-check/1.6/ works too, and I want to use this always.
  2. The new WordPress file is taken from http://wordpress.org/wordpress-3.4.2.zip, but https://wordpress.org/wordpress-3.4.2.zip works too.
  3. Sometimes I want to debug requests and redirect those temporary to a custom domain on my local server.
  4. Some plugins make requests to other servers, and I want to replace these requests when the external server goes down.

The update requests are the most important ones for now, because there is still the unfixed bug 16778 (more information), and HTTPS requests lower the risk of a Man-in-the-middle attack.

I have searched thoroughly, I have studied the core code … but ended up like Nacin two years ago:

I thought for sure you could filter the URL of an HTTP request, but now I can’t find one.

What did I miss? Did I? 🙂

Related posts

Leave a Reply

3 comments

  1. Less than an answer, but just a list of things straight from my experience with it – maybe you’ve overlooked something.

    Debugging the request & its results

    Without diggin’ too deep into the update process, but the WP HTTP API uses the WP_HTTP class. It also offers a nice thing: A debug hook.

    do_action( 'http_api_debug', $response, 'response', $class, $args, $url );
    

    Where $response can also be a WP_Error object that maybe tells you more.

    Note: From a brief test, this filter seems to only (for some reason) work if you place it as close to where you’re actually doing the request. So maybe you need to call it from within a callback on one of the below filters.

    WP_HTTP Class arguments

    The Classes arguments itself are filterable, but afaik some get reset by the methods internals back to what WP assumes that is needed.

    apply_filters( 'http_request_args', $r, $url );
    

    One of the arguments is ssl_verify, which is true by default (but for me causes massive problems when updating from – for example – GitHub). Edit: After debugging a test request, I found another argument that is set to verify if SSL is set to true. It’s called sslverify (without separating underscore). No idea where this came into the game, if it is actually in use or abandoned and if you have a chance to influence its value. I found it using the 'http_api_debug' filter.

    Completely custom

    You can also “simply” override the whole internals and go with a custom setup. There’s a filter for that.

    apply_filters( 'pre_http_request', false, $r, $url );
    

    The first arg needs to be set to true. Than you can interact with the arguments inside $r and the result from parse_url( $url );.

    Proxy

    Another thing that might work could be running everything through a custom Proxy. This needs some settings in your wp-config.php. I’ve never tried this before, but I ran through the constants a while back and summed up some examples that should work and included some comments in case I need it one day. You have to define WP_PROXY_HOST and WP_PROXY_PORT as a min. setting. Else nothing will work and it will simply bypass your proxy.

    # HTTP Proxies
    # Used for e.g. in Intranets
    # Fixes Feeds as well
    # Defines the proxy adresse.
    define( 'WP_PROXY_HOST',          '127.0.84.1' );
    # Defines the proxy port.
    define( 'WP_PROXY_PORT',          '8080' );
    # Defines the proxy username.
    define( 'WP_PROXY_USERNAME',      'my_user_name' );
    # Defines the proxy password.
    define( 'WP_PROXY_PASSWORD',      'my_password' );
    # Allows you to define some adresses which
    # shouldn't be passed through a proxy.
    define( 'WP_PROXY_BYPASS_HOSTS',  'localhost, www.example.com' );
    

    EDIT

    The WP_HTTP Class normally acts as base class (will be extended for different scenarios). The extending WP_HTTP_* classes are Fsockopen, Streams, Curl, Proxy, Cookie, Encoding. If you hook a callback to the 'http_api_debug'-action, then the third argument will tell you which class was used for your request.

    Inside the WP_HTTP_curl Class, you’ll find the request() method. This method offers two filters to intercept the SSL behavior: One for local requests 'https_local_ssl_verify' and one for remote requests 'https_ssl_verify'. WP will likely define local as localhost and what you get in return from get_option( 'siteurl' );.

    So what I’d do is to try the following right before you do that request (or from a callback that’s hooked to the closest request:

    add_filter( 'https_ssl_verify', '__return_true' );
    
    # Local requests should be checked with something like
    # 'localhost' === $_SERVER['HTTP_HOST'] or similar
    # add_filter( 'https_local_ssl_verify', '__return_true' );
    

    Sidenote: In most cases WP_HTTP_curl will be used to handle Proxies.

  2. Based on @kaiser’s useful answer I have written some code that seems to work well. That is the reason why I marked it as The Answer.

    Let me explain my solution …

    The logic

    When a request it sent through the API is runs through WP_Http::request(). That’s the method with …

    @todo Refactor this code.

    … in its header. I couldn’t agree more.

    Now, there are some filters. I decided to misuse pre_http_request for my needs:

    add_filter( 'pre_http_request', 't5_update_wp_per_https', 10, 3 );
    

    We get three arguments here: false, $r, $url.

    • false is the expected return value for apply_filters(). If we send anything else back,
      WordPress stops immediately, and the original request will not be sent.

    • $r is an array of arguments for that request. We have to change these too in a minute.

    • $url is – surprise! – the URL.

    So in our callback t5_update_wp_per_https() we look at the URL, and if it is an URL we want to filter, we say NO to WordPress by not saying “no” (false).

    enter image description here

    Side note: It follows you can prevent all HTTP requests with:
    add_filter( 'pre_http_request', '__return_true' );

    We fire our own request instead with a better URL and slightly adjusted arguments
    ($r, renamed to $args for readability).

    The code

    Please read the inline comments, they are important.

    <?php
    /**
     * Plugin Name: T5 Update WP per HTTPS
     * Description: Forces update checks and downloads for WP to use HTTPS.
     * Plugin URI:  http://wordpress.stackexchange.com/questions/72529/filter-any-http-request-uri
     * Version:     2012.11.14
     * Author:      Fuxia Scholz
     * Author URI:  https://fuxia.me
     * Licence:     MIT
     * License URI: http://opensource.org/licenses/MIT
     */
    
    add_filter( 'pre_http_request', 't5_update_wp_per_https', 10, 3 );
    
    /**
     * Force HTTPS requests for update checks and new WP version downloads.
     *
     * @wp-hook pre_http_request
     * @param   bool   $false
     * @param   array  $args
     * @param   string $url
     * @return  FALSE|array|object FALSE if everything is okay, an array of request
     *                            results or an WP_Error instance.
     */
    function t5_update_wp_per_https( $false, $args, $url )
    {
        // Split the URL into useful parts.
        $url_data = parse_url( $url );
    
        // It is already HTTPS.
        if ( 'https' === strtolower( $url_data['scheme'] ) )
            return FALSE;
    
        // Not our host.
        if ( FALSE === stripos( $url_data['host'], 'wordpress.org' ) )
            return FALSE;
    
        // Make that an HTTPS request.
        $new_url = substr_replace( $url, 'https', 0, 4 );
    
        // WP_Http cannot verify the wordpress.org certificate.
        $args['sslverify'] = FALSE;
    
        // It is slow. We wait at least 30 seconds.
        30 > $args['timeout'] and $args['timeout'] = 30;
    
        // Get an instance of WP_Http.
        $http    = _wp_http_get_object();
    
        // Get the result.
        $result = $http->request( $new_url, $args );
    
        /* prepend this line with a '#' to debug like a boss.
        print '<pre>'
        . htmlspecialchars( print_r( $result, TRUE ), ENT_QUOTES, 'utf-8', FALSE )
        . '</pre>';
        die();
        /**/
    
        return $result;
    }
    

    The tests

    Without that plugin WordPress used:

    • http://api.wordpress.org/core/version-check/1.6/ for update checks, and
    • http://wordpress.org/wordpress-3.4.2.zip to download the new files.

    I tested it with two local installations, a single site and a multi-site setup on Win 7.
    To force an update I set $wp_version in wp-includes/version.php to 1 and the version of
    TwentyEleven to 1.3.

    To watch the network traffic I used Wireshark: It is free, it runs on Windows and Linux, and it offers some impressive filter tools.

    Watching HTTPS is a little bit difficult: You see just encrypted data … that’s the idea after all.
    To see if my plugin did what it should do I watched the unencrypted traffic first and noted the IP address used to connect to wordpress.org. That was 72.233.56.138, sometimes 72.233.56.139.
    Not surprising, there is a load balancer and probably many other tools, so we cannot rely on one IP address.

    Then I typed ip.addr == 72.233.56.138 into the filter mask, activated the plugin, went to wp-admin/update-core.php and watched for the traffic in Wireshark. Green lines are requests in plain text – exactly what we don’t want. The red and black lines are a sign of success.

    Wireshark

    The update check went fine: It found the “newer” versions. The actual updates for the theme and the core went fine too. Exactly what I needed.

    And still … that could be easier if there were a simple filter for the URL.

  3.     add_filter('http_request_args', 'http_request_args_custom', 10,2);
        function http_request_args_custom($request,$url){
                if (strpos($url, 'wordpress.org') !== false){
                        global $replaced_url;
                        $replaced_url = 'http://wordpress.local';
                }
                return $request;
        }
    
        add_action('http_api_curl', 'http_api_curl_custom');
        function http_api_curl_custom(&$handle){
                global $replaced_url;
                if (!is_null($replaced_url))
                        curl_setopt( $handle, CURLOPT_URL, $replaced_url);
        }
    
        $http = new WP_Http();
        $response = $http->request('http://wordpress.org', array());
    
        var_dump($response);