<?php
/*******************************************************************
* Glype is copyright and trademark 2007-2015 UpsideOut, Inc. d/b/a Glype
* and/or its licensors, successors and assigners. All rights reserved.
*
* Use of Glype is subject to the terms of the Software License Agreement.
* http://www.glype.com/license.php
*******************************************************************
* This file is the main component of the glype proxy application.
* It decodes values contained within the current URI to determine a
* resource to download and pass onto the user.
******************************************************************/

/*****************************************************************
* Initialise
******************************************************************/

require 'includes/init.php';

if (count($adminDetails)===0) {
	header("HTTP/1.1 302 Found"); header("Location: admin.php"); exit;
}

# Debug mode - stores extra information in the cURL wrapper object and prints it
# out. It produces an ugly mess but still a quick tool for debugging.
define('DEBUG_MODE', 0);
define('CURL_LOG', 0);

# Log cURLs activity to file
# Change filename below if desired. Ensure file exists and is writable.
if ( CURL_LOG	&& ( $fh = @fopen('curl.txt', 'w')) ) {
	$toSet[CURLOPT_STDERR] = $fh;
	$toSet[CURLOPT_VERBOSE] = true;
}


/*****************************************************************
* PHP sends some headers by default. Stop them.
******************************************************************/

# Clear the default mime-type
header('Content-Type:');

# And remove the caching headers
header('Cache-Control:');
header('Last-Modified:');


/*****************************************************************
* Find URI of resource to load
* Flag and bitfield already extracted in /includes/init.php
******************************************************************/

switch ( true ) {

	# Try query string for URL
	case ! empty($_GET['u']) && ( $toLoad = deproxyURL($_GET['u'], true) ):
		break;

	# Try path info
	case ! empty($_SERVER['PATH_INFO'])	 && ( $toLoad = deproxyURL($_SERVER['PATH_INFO'], true) ):
		break;

	# Found no valid URL, return to index
	default:
		redirect();
}

# Validate the URL
if ( ! preg_match('#^((https?)://(?:([a-z0-9-.]+:[a-z0-9-.]+)@)?([a-z0-9-.]+)(?::([0-9]+))?)(?:/|$)((?:[^?/]*/)*)([^?]*)(?:\?([^\#]*))?(?:\#.*)?$#i', $toLoad, $tmp) ) {

	# Invalid, show error
	error('invalid_url', htmlentities($toLoad));

}

# Rename parts to more useful names
$URL = array(
	'scheme_host'	=> $tmp[1],
	'scheme'		=> $tmp[2],
	'auth'			=> $tmp[3],
	'host'			=> strtolower($tmp[4]),
	'domain'		=> strtolower(preg_match('#(?:^|\.)([a-z0-9-]+\.(?:[a-z.]{5,6}|[a-z]{2,}))$#', $tmp[4], $domain) ? $domain[1] : $tmp[4]), # Attempt to split off the subdomain (if any)
	'port'			=> $tmp[5],
	'path'			=> '/' . $tmp[6],
	'filename'		=> $tmp[7],
	'extension'		=> pathinfo($tmp[7], PATHINFO_EXTENSION),
	'query'			=> isset($tmp[8]) ? $tmp[8] : ''
);

# Apply encoding on full URL. In theory all parts of the URL need various special
# characters encoding but this needs to be done by the author of the webpage.
# We can make a guess at what needs encoding but some servers will complain when
# receiving the encoded character instead of unencoded and vice versa. We want
# to edit the URL as little as possible so we're only encoding spaces, as this
# seems to 'fix' the majority of cases.
$URL['href'] = str_replace(' ', '%20', $toLoad);


# Add any supplied authentication information to our auth array
if ($URL['auth']) {
	$_SESSION['authenticate'][$URL['scheme_host']] = $URL['auth'];
}



/*****************************************************************
* Protect LAN from access through proxy
* This does not stop all hostnames that resolve to LAN IPs
* unless the line containing "gethostbyname" is uncommented below
******************************************************************/
if (preg_match('#^localhost#i', $URL['host'])) {
	error('banned_site', $URL['host']);
}

$host = $URL['host'];

#$host = gethostbyname($host); # uncomment for more complete protection

if (preg_match('#^\d+$#', $host)) { # decimal IPs
	$host = implode('.', array($host>>24&255, $host>>16&255, $host>>8&255, $host&255));
}
if (preg_match('#^(0|10|127|169\.254|192\.168|172\.(?:1[6-9]|2[0-9]|3[01])|2[2-5][0-9])\.#', $host)) { # special use netblocks
	error('banned_site', $host);
}



/*****************************************************************
* Protect us from hotlinking
******************************************************************/

# Protect only if option is enabled and we don't have a verified session
if ( $CONFIG['stop_hotlinking'] && empty($_SESSION['no_hotlink']) ) {

	# Assume hotlinking to start with, then check against allowed domains
	$tmp = true;

	# Ensure we have valid referrer information to check
	if ( ! empty($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'http') === 0 ) {

		# Examine all the allowed domains (including our current domain)
		foreach ( array_merge( (array) GLYPE_URL, $CONFIG['hotlink_domains'] ) as $domain ) {

			# Do a case-insensitive comparison
			if ( stripos($_SERVER['HTTP_REFERER'], $domain) !== false ) {

				# This referrer is OK
				$tmp = false;
				break;
			}
		}
	}

	# Redirect to index if this is still identified as hotlinking
	if ( $tmp ) {
		error('no_hotlink');
	}

}

# If we're still here, the referrer must be OK so set the session for next time
$_SESSION['no_hotlink'] = true;


/*****************************************************************
* Are we allowed to visit this site? Check whitelist/blacklist
******************************************************************/

# Whitelist - deny IF NOT on list
if ( ! empty($CONFIG['whitelist']) ) {

	$tmp = false;

	# Loop through
	foreach ( $CONFIG['whitelist'] as $domain ) {

		# Check for match
		if ( strpos($URL['host'], $domain) !== false ) {

			# Must be a permitted site
			$tmp = true;

		}

	}

	# Unless $tmp is flagged true, this is an illegal site
	if ( ! $tmp ) {
		error('banned_site', $URL['host']);
	}

}

# Blacklist
if ( ! empty($CONFIG['blacklist']) ) {

	# Loop through
	foreach ( $CONFIG['blacklist'] as $domain ) {

		# Check for match
		if ( strpos($URL['host'], $domain) !== false ) {

			# If matched, site is banned
			error('banned_site', $URL['host']);

		}

	}

}


/*****************************************************************
* Show SSL warning
* This warns users if they access a secure site when the proxy is NOT
* on a secure connection and the $CONFIG['ssl_warning'] option is on.
******************************************************************/

if ( $URL['scheme'] == 'https' && $CONFIG['ssl_warning'] && empty($_SESSION['ssl_warned']) && ! HTTPS ) {

	# Remember this page so we can return after agreeing to the warning
	$_SESSION['return'] = currentURL();

	# Don't cache the warning page
	sendNoCache();

	# Show the page
	echo loadTemplate('sslwarning.page');

	# All done!
	exit;

}


/*****************************************************************
* Plugins
* Load any site-specific plugin.
******************************************************************/
global $foundPlugin;
$plugins = explode(',', $CONFIG['plugins']);
if ($foundPlugin = in_array($URL['domain'], $plugins)) {
	include(GLYPE_ROOT.'/plugins/'.$URL['domain'].'.php');
}



/*****************************************************************
* Close session to allow simultaneous transfers
* PHP automatically prevents multiple instances of the script running
* simultaneously to avoid concurrency issues with the session.
* This may be beneficial on high traffic servers but we have the option
* to close the session and thus allow simultaneous transfers.
******************************************************************/

if ( ! $CONFIG['queue_transfers'] ) {

	session_write_close();

}


/*****************************************************************
* Check load limit. This is done now rather than earlier so we
* don't stop serving the (relatively) cheap cached files.
******************************************************************/

if (
	# Option enabled
	$CONFIG['load_limit']

	# Ignore inline elements - when borderline on the server load, if the HTML
	# page downloads fine but the inline images, css and js are blocked, the user
	# may get very frustrated very quickly without knowing about the load issues.
	&& ! in_array($URL['extension'], array('jpg','jpeg','png','gif','css','js','ico'))
	) {

	# Do we need to find the load and regenerate the temp cache file?
	# Try to fetch the load from the temp file (~30 times faster than
	# shell_exec()) and ensure the value is accurate and not outdated,
	if( ! file_exists($file = $CONFIG['tmp_dir'] . 'load.php') || ! (include $file) || ! isset($load, $lastChecked) || $lastChecked < $_SERVER['REQUEST_TIME']-60 ) {

		$load = (float) 0;

		# Attempt to fetch the load
		if ( ($uptime = @shell_exec('uptime')) && preg_match('#load average: ([0-9.]+),#', $uptime, $tmp) ) {
			$load = (float) $tmp[1];

			# And regenerate the file
			file_put_contents($file, '<?php $load = ' . $load . '; $lastChecked = ' . $_SERVER['REQUEST_TIME'] . ';');
		}

	}

	# Load found, (or at least, should be), check against max permitted
	if ( $load > $CONFIG['load_limit'] ) {
		error('server_busy'); # Show error
	}
}


/*****************************************************************
* * * * * * * * * * Prepare the REQUEST * * * * * * * * * * * *
******************************************************************/

/*****************************************************************
* Set cURL transfer options
* These options are merely passed to cURL and our script has no further
* impact or dependence of them. See the libcurl documentation and
* http://php.net/curl_setopt for more details.
*
* The following options are required for the proxy to function or
* inherit values from our config. In short: they shouldn't need changing.
******************************************************************/

# Time to wait for connection
$toSet[CURLOPT_CONNECTTIMEOUT] = $CONFIG['connection_timeout'];

# Time to allow for entire transfer
$toSet[CURLOPT_TIMEOUT] = $CONFIG['transfer_timeout'];

# Show SSL without verifying - we almost definitely don't have an up to date CA cert
# bundle so we can't verify the certificate. See http://curl.haxx.se/docs/sslcerts.html
$toSet[CURLOPT_SSL_VERIFYPEER] = false;
$toSet[CURLOPT_SSL_VERIFYHOST] = false;

# Send an empty Expect header (avoids 100 responses)
$toSet[CURLOPT_HTTPHEADER][] = 'Expect:';

# Can we use "If-Modified-Since" to save a transfer? Server can return 304 Not Modified
if ( isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ) {

	# How to treat the time condition : if un/modified since
	$toSet[CURLOPT_TIMECONDITION] = CURL_TIMECOND_IFMODSINCE;

	# The time value. Requires a timestamp so we can't just forward it raw
	$toSet[CURLOPT_TIMEVALUE] = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);

}

# Resume a transfer?
if ( $CONFIG['resume_transfers'] && isset($_SERVER['HTTP_RANGE']) ) {

	# And give cURL the right part
	$toSet[CURLOPT_RANGE] = substr($_SERVER['HTTP_RANGE'], 6);

}

# cURL has a max filesize option but it's not listed in the PHP manual so check it's available
if ( $CONFIG['max_filesize'] && defined('CURLOPT_MAXFILESIZE') ) {

	# Use the cURL option - should be faster than our implementation
	$toSet[CURLOPT_MAXFILESIZE] = $CONFIG['max_filesize'];

}


/*****************************************************************
* Performance options
* The values below are NOT the result of benchmarking tests. For
* optimum performance, you may want to try adjusting these values.
******************************************************************/

# DNS cache expiry time (seconds)
$toSet[CURLOPT_DNS_CACHE_TIMEOUT] = 600;

# Speed limits - aborts transfer if we're going too slowly
#$toSet[CURLOPT_LOW_SPEED_LIMIT] = 5; # speed limit in bytes per second
#$toSet[CURLOPT_LOW_SPEED_TIME] = 20; # seconds spent under the speed limit before aborting

# Number of max connections (no idea what this should be)
# $toSet[CURLOPT_MAXCONNECTS] = 100;

# Accept encoding in any format (allows compressed pages to be downloaded)
# Any bandwidth savings are likely to be minimal so better to save on load by
# downloading pages uncompressed. Use blank string for any compression or
# 'identity' to explicitly ask for uncompressed.
# $toSet[CURLOPT_ENCODING] = '';

# Undocumented in PHP manual (added 5.2.1) but allows uploads to some sites
# (e.g. imageshack) when without this option, an error occurs. Less efficient
# so probably best not to set this unless you need it.
#	 $toSet[CURLOPT_TCP_NODELAY] = true;


/*****************************************************************
* "Accept" headers
* No point sending back a file that the browser won't understand.
* Forward all the "Accept" headers. For each, check if it exists
* and if yes, add to the custom headers array.
* These may cause problems if the target server provides different
* content for the same URI based on these headers and we cache the response.
******************************************************************/

# Language (geotargeting will find the location of the server -
# forwarding this header can help avoid incorrect localisation)
if ( isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ) {
	$toSet[CURLOPT_HTTPHEADER][] = 'Accept-Language: ' . $_SERVER['HTTP_ACCEPT_LANGUAGE'];
}

# Accepted filetypes
if ( isset($_SERVER['HTTP_ACCEPT']) ) {
	$toSet[CURLOPT_HTTPHEADER][] = 'Accept: ' . $_SERVER['HTTP_ACCEPT'];
}

# Accepted charsets
if ( isset($_SERVER['HTTP_ACCEPT_CHARSET']) ) {
	$toSet[CURLOPT_HTTPHEADER][] = 'Accept-Charset: ' . $_SERVER['HTTP_ACCEPT_CHARSET'];
}


/*****************************************************************
* Browser options
* Allows customization of a "virtual" browser via /extras/edit-browser.php
******************************************************************/

# Send user agent
if ( $_SESSION['custom_browser']['user_agent'] ) {
	$toSet[CURLOPT_USERAGENT] = $_SESSION['custom_browser']['user_agent'];
}

# Set referrer
if ( $_SESSION['custom_browser']['referrer'] == 'real' ) {

	# Automatically determine referrer
	if ( isset($_SERVER['HTTP_REFERER']) && $flag != 'norefer' && strpos($tmp = deproxyURL($_SERVER['HTTP_REFERER']), GLYPE_URL) === false ) {
		$toSet[CURLOPT_REFERER] = $tmp;
	}

} else if ( $_SESSION['custom_browser']['referrer'] ) {

	# Send custom referrer
	$toSet[CURLOPT_REFERER] = $_SESSION['custom_browser']['referrer'];

}

# Clear the norefer flag
if ( $flag == 'norefer' ) {
	$flag = '';
}


/*****************************************************************
* Authentication
******************************************************************/

# Check for stored credentials for this site
if ( isset($_SESSION['authenticate'][$URL['scheme_host']]) ) {

	# Found credentials so use them!
	$toSet[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
	$toSet[CURLOPT_USERPWD] = $_SESSION['authenticate'][$URL['scheme_host']];

}


/*****************************************************************
* Cookies
* Find the relevant cookies for this request. All cookies get sent
* to the proxy, but we only want to forward the ones that were set
* for the current domain.
*
* Cookie storage methods:
* (1) Server-side - cookies stored server-side and handled
*		(mostly) internally by cURL
* (2) Encoded	- cookies forwarded to client but encoded
* (3) Normal - cookies forwarded without encoding
******************************************************************/

# Are cookies allowed?
if ( $options['allowCookies'] ) {

	# Option (1): cookies stored server-side
	if ( $CONFIG['cookies_on_server'] ) {

		# Check cookie folder exists or try to create it
		if ( $s = checkTmpDir($CONFIG['cookies_folder'], 'Deny from all') ) {

			# Set cURL to use this as the cookie jar
			$toSet[CURLOPT_COOKIEFILE] = $toSet[CURLOPT_COOKIEJAR] = $CONFIG['cookies_folder'] . glype_session_id();

		}

	} else if ( isset($_COOKIE[COOKIE_PREFIX]) ) {

		# Encoded or unencoded?
		if ( $CONFIG['encode_cookies'] ) {

			# Option (2): encoded cookies stored client-side
			foreach ( $_COOKIE[COOKIE_PREFIX] as $attributes => $value ) {

				# Decode cookie to [domain,path,name]
				$attributes = explode(' ', base64_decode($attributes));

				# Check successful decoding and skip if failed
				if ( ! isset($attributes[2]) ) {
					continue;
				}

				# Extract parts
				list($domain, $path, $name) = $attributes;

				# Check for a domain match and skip if no match
				if ( stripos($URL['host'], $domain) === false ) {
					continue;
				}

				# Check for match and skip to next path if fail
				if ( stripos($URL['path'], $path) !== 0 ) {
					continue;
				}

				# Multiple cookies of the same name are permitted if different paths
				# so use path AND name as the key in the temp array
				$key = $path . $name;

				# Check for existing cookie with same domain, same path and same name
				if ( isset($toSend[$key]) && $toSend[$key]['path'] == $path && $toSend[$key]['domain'] > strlen($domain) ) {

					# Conflicting cookies so ignore the one with the less complete tail match
					# (i.e. the current one)
					continue;

				}

				# Domain and path OK, decode cookie value
				$value = base64_decode($value);

				# Only send secure cookies on https connection - secure cookies marked by !SEC suffix
				# so remove the suffix
				$value = str_replace('!SEC', '', $value, $tmp);

				# And if secure cookie but not https site, do not send
				if ( $tmp && $URL['scheme'] != 'https' ) {
					continue;
				}


				# Everything checked and verified, add to $toSend for further processing later
				$toSend[$key] = array('path_size' => strlen($path), 'path' => $path, 'domain' => strlen($domain), 'send' => $name . '=' . $value);

			}

		} else {

			# Option (3): unencoded cookies stored client-side
			foreach ( $_COOKIE[COOKIE_PREFIX] as $domain => $paths ) {

				# $domain holds the domain (surprisingly) and $path is an array
				# of keys (paths) and more arrays (each child array of $path = one cookie)
				# e.g. Array('domain.com' => Array('/' => Array('cookie_name' => 'value')))

				# First check for domain match and skip to next domain if no match
				if ( stripos($URL['host'], $domain) === false ) {
					continue;
				}

				# If conflicting cookies with same name and same path,
				# send the one with the more complete tail match. To do this we
				# need to know how long each match is/was so record domain length.
				$domainSize = strlen($domain);

				# Now look at all the available paths
				foreach ( $paths as $path => $cookies ) {

					# Check for match and skip to next path if fail
					if ( stripos($URL['path'], $path) !== 0 ) {
						continue;
					}

					# In final header, cookies are ordered with most specific path
					# matches first so include the length of match in temp array
					$pathSize = strlen($path);

					# All cookies in $cookies array should be sent
					foreach ( $cookies as $name => $value ) {

						# Multiple cookies of the same name are permitted if different paths
						# so use path AND name as the key in the temp array
						$key = $path . $name;

						# Check for existing cookie with same domain, same path and same name
						if ( isset($toSend[$key]) && $toSend[$key]['path'] == $path && $toSend[$key]['domain'] > $domainSize ) {

							# Conflicting cookies so ignore the one with the less complete tail match
							# (i.e. the current one)
							continue;

						}

						# Only send secure cookies on https connection - secure cookies marked by !SEC suffix
						# so remove the suffix
						$value = str_replace('!SEC', '', $value, $tmp);

						# And if secure cookie but not https site, do not send
						if ( $tmp && $URL['scheme'] != 'https' ) {
							continue;
						}

						# Add to $toSend for further processing later
						$toSend[$key] = array('path_size' => $pathSize, 'path' => $path, 'domain' => $domainSize, 'send' => $name . '=' . $value);

					}

				}

			}

		}

		# Ensure we have found cookies
		if ( ! empty($toSend) ) {

			# Order by path specificity (as per Netscape spec)
			function compareArrays($a, $b) {
				return ( $a['path_size'] > $b['path_size'] ) ? -1 : 1;
			}

			# Apply the sort to order by path_size descending
			uasort($toSend, 'compareArrays');

			# Go through the ordered array and generate the Cookie: header
			$tmp = '';

			foreach ( $toSend as $cookie ) {
				$tmp .= $cookie['send'] . '; ';
			}

			# Give the string to cURL
			$toSet[CURLOPT_COOKIE] = $tmp;

		}

		# And clear the toSend array
		unset($toSend);

	}

}


/*****************************************************************
* Post
* Forward the post data. Usually very simple but complicated by
* multipart forms because in those cases, the raw post is not available.
******************************************************************/

if ( ! empty($_POST) ) {

	# enable backward compatibility with cURL's @ option for uploading files in PHP 5.5 and 5.6
	if (version_compare(PHP_VERSION, '5.5')>=0) {
		$toSet[CURLOPT_SAFE_UPLOAD] = false;
	}

	# Attempt to get raw POST from the input wrapper
	if ( ! ($tmp = file_get_contents('php://input')) ) {

		# Raw data not available (probably multipart/form-data).
		# cURL will do a multipart post if we pass an array as the
		# POSTFIELDS value but this array can only be one deep.

		# Recursively flatten array to one level deep and rename keys
		# as firstLayer[second][etc]. Also apply the input decode to all
		# array keys.
		function flattenArray($array, $prefix='') {

			# Start with empty array
			$stack = array();

			# Loop through the array to flatten
			foreach ( $array as $key => $value ) {

				# Decode the input name
				$key = inputDecode($key);

				# Determine what the new key should be - add the current key to
				# the prefix and surround in []
				$newKey = $prefix ? $prefix . '[' . $key . ']' : $key;

				if ( is_array($value) ) {

					# If it's an array, recurse and merge the returned array
					$stack = array_merge($stack, flattenArray($value, $newKey));

				} else {

					# Otherwise just add it to the current stack
					$stack[$newKey] = clean($value);

				}

			}

			# Return flattened
			return $stack;

		}

		$tmp = flattenArray($_POST);

		# Add any file uploads?
		if ( ! empty($_FILES) ) {

			# Loop through and add the files
			foreach ( $_FILES as $name => $file ) {

				# Is this an array?
				if ( is_array($file['tmp_name']) ) {

					# Flatten it - file arrays are in the slightly odd format of
					# $_FILES['layer1']['tmp_name']['layer2']['layer3,etc.'] so add
					# layer1 onto the start.
					$flattened = flattenArray(array($name => $file['tmp_name']));

					# And add all files to the post
					foreach ( $flattened as $key => $value ) {
						$tmp[$key] = '@' . $value;
					}

				} else {

					# Not another array. Check if the file uploaded successfully?
					if ( ! empty($file['error']) || empty($file['tmp_name']) ) {
						continue;
					}

					# Add to array with @ - tells cURL to upload this file
					$tmp[$name] = '@' . $file['tmp_name'];

				}

				# To do: rename the temp file to it's real name before
				# uploading it to the target? Otherwise, the target receives
				# the temp name instead of the original desired name
				# but doing this may be a security risk.

			}

		}

		}

	# Convert back to GET if required
	if ( isset($_POST['convertGET']) ) {

		# Remove convertGET from POST array and update our location
		$URL['href'] .= ( empty($URL['query']) ? '?' : '&' ) . str_replace('convertGET=1', '', $tmp);

	} else {

		# Genuine POST so set the cURL post value
		$toSet[CURLOPT_POST] = 1;
		$toSet[CURLOPT_POSTFIELDS] = $tmp;

	}

}


/*****************************************************************
* Apply pre-request code from plugins
******************************************************************/

if ( $foundPlugin && function_exists('preRequest') ) {
	preRequest();
}


/*****************************************************************
* Make the request
* This request object uses custom header/body reading functions
* so we can start processing responses on the fly - e.g. we don't
* need to wait till the whole file has downloaded before deciding
* if it needs parsing or can be sent out unchanged.
******************************************************************/

class Request {

	# Response status code
	public $status = 0;

	# Headers received and read by our callback
	public $headers = array();

	# Returned data (if saved)
	public $return;

	# Reason for aborting transfer (or empty to continue downloading)
	public $abort;

	# The error (if any) returned by curl_error()
	public $error;

	# Type of resource downloaded [html, js, css] or empty if no parsing needed
	public $parseType;

	# Automatically detect(ed) content type?
	public $sniff = false;

	# Forward cookies or not
	private $forwardCookies = false;

	# Limit filesize?
	private $limitFilesize = 0;

	# Speed limit (bytes per second)
	private $speedLimit = 0;

	# URL array split into pieces
	private $URL;

	# = $options from the global scope
	private $browsingOptions;

	# Options to pass to cURL
	private $curlOptions;


	# Constructor - takes the parameters and saves them
	public function __construct($curlOptions) {

		global $options, $CONFIG;

		# Set our reading callbacks
		$curlOptions[CURLOPT_HEADERFUNCTION] = array(&$this, 'readHeader');
		$curlOptions[CURLOPT_WRITEFUNCTION] = array(&$this, 'readBody');

		# Determine whether or not to forward cookies
		if ( $options['allowCookies'] && ! $CONFIG['cookies_on_server'] ) {
			$this->forwardCookies = $CONFIG['encode_cookies'] ? 'encode' : 'normal';
		}

		# Determine a filesize limit
		if ( $CONFIG['max_filesize'] ) {
			$this->limitFilesize = $CONFIG['max_filesize'];
		}

		# Determine speed limit
		if ( $CONFIG['download_speed_limit'] ) {
			$this->speedLimit = $CONFIG['download_speed_limit'];
		}

		# Set options
		$this->browsingOptions = $options;
		$this->curlOptions = $curlOptions;

		# Extend the PHP timeout
		set_time_limit($CONFIG['transfer_timeout']);

		# Record debug information
		if ( DEBUG_MODE ) {
			$this->cookiesSent = isset($curlOptions[CURLOPT_COOKIE]) ? $curlOptions[CURLOPT_COOKIE] : ( isset($curlOptions[CURLOPT_COOKIEFILE]) ? 'using cookie jar' : 'none');
			$this->postSent = isset($curlOptions[CURLOPT_POSTFIELDS]) ? $curlOptions[CURLOPT_POSTFIELDS] : '';
		}

	}

	# Make the request and return the downloaded file if parsing is needed
	public function go($URL) {

		# Save options
		$this->URL = $URL;

		# Get a cURL handle
		$ch = curl_init($this->URL['href']);

		# Set the options
		curl_setopt_array($ch, $this->curlOptions);

		# Make the request
		curl_exec($ch);

		# Save any errors (but not if we caused the error by aborting!)
		if ( ! $this->abort ) {
			$this->error = curl_error($ch);
		}

		# And close the curl handle
		curl_close($ch);

		# And return the document (will be empty if no parsing needed,
		# because everything else is outputted immediately)
		return $this->return;

	}


	/*****************************************************************
	* * * * * * * * * * Manage the RESPONSE * * * * * * * * * * * *
	******************************************************************/


	/*****************************************************************
	* Read headers - receives headers line by line (cURL callback)
	******************************************************************/

	public function readHeader($handle, $header) {

		# Extract the status code (can occur more than once if 100 continue)
		if ( $this->status == 0 || ( $this->status == 100 && ! strpos($header, ':') ) ) {
			$this->status = substr($header, 9, 3);
		}

		# Attempt to extract header name and value
		$parts = explode(':', $header, 2);

		# Did it split successfully? (i.e. was there a ":" in the header?)
		if ( isset($parts[1]) ) {

			# Header names are case insensitive
			$headerType = strtolower($parts[0]);

			# And header values will have trailing newlines and prevailing spaces
			$headerValue = trim($parts[1]);

			# Set any cookies
			if ( $headerType == 'set-cookie' && $this->forwardCookies ) {

				$this->setCookie($headerValue);

			}

			# Everything else, store as associative array
			$this->headers[$headerType] = $headerValue;

			# Do we want to forward this header? First list the headers we want:
			$toForward = array('last-modified',
									 'content-disposition',
									 'content-type',
									 'content-range',
									 'content-language',
									 'expires',
									 'cache-control',
									 'pragma');

			# And check for a match before forwarding the header.
			if ( in_array($headerType, $toForward) ) {
				header($header);
			}

		} else {

			# Either first header or last 'header' (more precisely, the 2 newlines
			# that indicate end of headers)

			# No ":", so save whole header. Also check for end of headers.
			if ( ( $this->headers[] = trim($header) ) == false ) {

				# Must be end of headers so process them before reading body
				$this->processHeaders();

				# And has that processing given us any reason to abort?
				if ( $this->abort ) {
					return -1;
				}

			}

		}

		# cURL needs us to return length of data read
		return strlen($header);

	}


	/*****************************************************************
	* Process headers after all received and before body is read
	******************************************************************/

	private function processHeaders() {

		# Ensure we only run this function once
		static $runOnce;

		# Check for flag and if found, stop running function
		if ( isset($runOnce) ) {
			return;
		}

		# Set flag for next time
		$runOnce = true;

		# Send the appropriate status code
		header(' ', true, $this->status);

		# Find out if we want to abort the transfer
		switch ( true ) {

			# Redirection
			case isset($this->headers['location']):

				$this->abort = 'redirect';

				return;

			# 304 Not Modified
			case $this->status == 304:

				$this->abort = 'not_modified';

				return;

			# 401 Auth required
			case $this->status == 401:

				$this->abort = 'auth_required';

				return;

			# Error code (>=400)
			case $this->status >= 400:

				$this->abort = 'http_status_error';

				return;

			# Check for a content-length above the filesize limit
			case isset($this->headers['content-length']) && $this->limitFilesize && $this->headers['content-length'] > $this->limitFilesize:

				$this->abort = 'filesize_limit';

				return;

		}

		# Still here? No need to abort so next we determine parsing mechanism to use (if any)
		if ( isset($this->headers['content-type']) ) {

			# Define content-type to parser type relations
			$types = array(
				'text/javascript'			=> 'javascript',
				'text/ecmascript'			=> 'javascript',
				'application/javascript'	=> 'javascript',
				'application/x-javascript'	=> 'javascript',
				'application/ecmascript'	=> 'javascript',
				'application/x-ecmascript'	=> 'javascript',
				'text/livescript'			=> 'javascript',
				'text/jscript'				=> 'javascript',
				'application/xhtml+xml'		=> 'html',
				'text/html'					=> 'html',
				'text/css'					=> 'css',
			#	'text/xml'					=> 'rss',
			#	'application/rss+xml'		=> 'rss',
			#	'application/rdf+xml'		=> 'rss',
			#	'application/atom+xml'		=> 'rss',
			#	'application/xml'			=> 'rss',
			);

			# Extract mimetype from charset (if exists)
			global $charset;
			$content_type = explode(';', $this->headers['content-type'], 2);
			$mime = isset($content_type[0]) ? trim($content_type[0]) : '';
			if (isset($content_type[1])) {
				$charset = preg_match('#charset\s*=\s*([^"\'\s]*)#is', $content_type[1], $tmp, PREG_OFFSET_CAPTURE) ? $tmp[1][0] : null;
			}

			# Look for that mimetype in our array to find the parsing mechanism needed
			if ( isset($types[$mime]) ) {
				$this->parseType = $types[$mime];
			}

			# validate mimetypes
			if (!preg_match('#^(application|audio|image|text|video)/#i', $mime)) {
				header('Content-Type: text/plain');
			}

		} else {

			# Tell our read body function to 'sniff' the data to determine type
			$this->sniff = true;

		}

		# If no content-disposition sent, send one with the correct filename
		if ( ! isset($this->headers['content-disposition']) && $this->URL['filename'] ) {
			header('Content-Disposition: filename="' . $this->URL['filename'] . '"');
		}

		# If filesize limit exists, content-length received and we're still here, the
		# content-length is OK. If we assume the content-length is accurate (and since
		# clients [and possibly libcurl too] stop downloading after reaching the limit,
		# it's probably safe to assume that),we can save on load by not checking the
		# limit with each chunk received.
		if ( $this->limitFilesize && isset($this->headers['content-length']) ) {
			$this->limitFilesize = 0;
		}

	}


	/*****************************************************************
	* Read body - takes chunks of data (cURL callback)
	******************************************************************/

	public function readBody($handle, $data) {

		# Static var to tell us if this function has been run before
		static $first;

		# Check for set variable
		if ( ! isset($first) ) {

			# Run the pre-body code
			$this->firstBody($data);

			# Set the variable so we don't run this code again
			$first = false;

		}

		# Find length of data
		$length = strlen($data);

		# Limit speed to X bytes/second
		if ( $this->speedLimit ) {

			# Limit download speed
			# Speed		 = Amount of data / Time
			# [bytes/s] = [bytes]			/ [s]
			# We know the desired speed (defined earlier in bytes per second)
			# and we know the number of bytes we've received. Now we need to find
			# the time that it should take to receive those bytes.
			$time = $length / $this->speedLimit; # [s]

			# Convert time to microseconds and sleep for that value
			usleep(round($time * 1000000));
		}

		# Monitor length if desired
		if ( $this->limitFilesize ) {

			# Set up a static downloaded-bytes value
			static $downloadedBytes;

			if ( ! isset($downloadedBytes) ) {
				$downloadedBytes = 0;
			}

			# Add length to downloadedBytes
			$downloadedBytes += $length;

			# Is downloadedBytes over the limit?
			if ( $downloadedBytes > $this->limitFilesize ) {

				# Set the abort variable and return -1 (so cURL aborts)
				$this->abort = 'filesize_limit';
				return -1;

			}

		}

		# If parsing is required, save as $return
		if ( $this->parseType ) {

			$this->return .= $data;

		} else {
			echo $data; # No parsing so print immediately
		}

		# cURL needs us to return length of data read
		return $length;

	}




	/*****************************************************************
	* Process first chunk of data in body
	* Sniff the content if no content-type was sent and create the file
	* handle if caching this.
	******************************************************************/

	private function firstBody($data) {

		# Do we want to sniff the data to gues the mimetype?
		if ( $this->sniff ) {
			if (stripos($data, '<html')!==false && stripos($data, '<head')!==false) {
				header('Content-Type: text/html');
				$this->parseType = 'html';
			} else {
				header('Content-Type: text/plain');
			}
		}

		# Now we know if parsing is required, we can forward content-length
		if ( ! $this->parseType && isset($this->headers['content-length']) ) {
			header('Content-Length: ' . $this->headers['content-length']);
		}

	}


	/*****************************************************************
	* Accept cookies - takes the value from Set-Cookie: [COOKIE STRING]
	* and forwards cookies to the client
	******************************************************************/

	private function setCookie($cookieString) {

		# The script can handle cookies following the Netscape specification
		# (or close enough!) and supports "Max-Age" from RFC2109

		# Split parts by ;
		$cookieParts = explode(';', $cookieString);

		# Process each line
		foreach ( $cookieParts as $part ) {

			# Split attribute/value pairs by =
			$pair = explode('=', $part, 2);

			# Ensure we have a second part
			$pair[1] = isset($pair[1]) ? $pair[1] : '';

			# First pair must be name/cookie value
			if ( ! isset($cookieName) ) {

				# Name is first pair item, value is second
				$cookieName = $pair[0];
				$cookieValue = $pair[1];

				# Skip rest of loop and start processing attributes
				continue;

			}

			# If still here, must be an attribute (case-insensitive so lower it)
			$pair[0] = strtolower($pair[0]);

			# And save in array
			if ( $pair[1] ) {

				# We have a attribute/value pair so save as associative
				$attr[ltrim($pair[0])] = $pair[1];

			} else {

				# Not a pair, just a value
				$attr[] = $pair[0];

			}

		}

		# All cookies need to be sent to this script (and then we choose
		# the correct cookies to forward to the client) so the extra attributes
		# (path, domain, etc.) must be stored in the cookie itself

		# Cookies stored as c[domain.com][path][cookie_name] with values of
		# cookie_value;secure;
		# If encoded, cookie name becomes c[base64_encode(domain.com path cookie_name)]

		# Find the EXPIRES date
		if ( isset($attr['expires']) ) {

			# From the "Expires" attribute (original Netscape spec)
			$expires = strtotime($attr['expires']);

		} else if ( isset($attr['max-age']) ) {

			# From the "Max-Age" attribute (RFC2109)
			$expires = $_SERVER['REQUEST_TIME']+$attr['max-age'];

		} else {

			# Default to temp cookies
			$expires = 0;

		}

		# If temp cookies, override expiry date to end of session unless time
		# is in the past since that means the cookie should be deleted
		if ( $this->browsingOptions['tempCookies'] && $expires > $_SERVER['REQUEST_TIME'] ) {
			$expires = 0;
		}

		# Find the PATH. The spec says if none found, default to the current path.
		# Certain browsers default to the the root path so we'll do the same.
		if ( ! isset($attr['path']) ) {
			$attr['path'] = '/';
		}

		# Were we sent a DOMAIN?
		if ( isset($attr['domain']) ) {

			# Ensure it's valid and we can accept this cookie
			if ( stripos($attr['domain'], $this->URL['domain']) === false ) {

				# Our current domain does not match the specified domain
				# so we reject the cookie
				return;

			}

			# Some cookies will be sent with the domain starting with . as per RFC2109
			# The . then has to be stripped off by us when doing the tail match to determine
			# which cookies to send since ".glype.com" should match "glype.com". It's more
			# efficient to do any manipulations while forwarding cookies than on every request
			if ( $attr['domain'][0] == '.' ) {
				$attr['domain'] = substr($attr['domain'], 1);
			}

		} else {

			# No domain sent so use current domain
			$attr['domain'] = $this->URL['domain'];

		}

		# Check for SECURE cookie
		$sentSecure = in_array('secure', $attr);

		# Append "[SEC]" to cookie value if we should only forward to secure connections
		if ( $sentSecure ) {
			$cookieValue .= '!SEC';
		}

		# If we're on HTTPS, we can also send this cookie back as secure
		$secure = HTTPS && $sentSecure;

		# If the PHP version is recent enough, we can also forward the httponly flag
		$httponly = in_array('httponly', $attr) && version_compare(PHP_VERSION,'5.2.0','>=') ? true : false;

		# Prepare cookie name/value to save as
		$name = COOKIE_PREFIX . '[' . $attr['domain'] . '][' . $attr['path'] . '][' . inputEncode($cookieName) . ']';
		$value = $cookieValue;

		# Add encodings
		if ( $this->forwardCookies == 'encode' ) {

			$name = COOKIE_PREFIX . '[' . urlencode(base64_encode($attr['domain'] . ' ' . $attr['path'] . ' ' . urlencode($cookieName))) . ']';
			$value = base64_encode($value);

		}

		# Send cookie ...
		if ( $httponly ) {

			# ... with httponly flag
			setcookie($name, $value, $expires, '/', '', $secure, true);

		} else {

			# ... without httponly flag
			setcookie($name, $value, $expires, '/', '', $secure);

		}

		# And log if in debug mode
		if ( DEBUG_MODE ) {

			$this->cookiesReceived[] = array('name'			=> $cookieName,
														'value'			=> $cookieValue,
														'attributes'	=> $attr);

		}

	}

}


/*****************************************************************
* Execute the request
******************************************************************/

# Initiate cURL wrapper request object with our cURL options
$fetch = new Request($toSet);

# And make the request
$document = $fetch->go($URL);


/*****************************************************************
* Handle aborted transfers
******************************************************************/

if ( $fetch->abort ) {

	switch ( $fetch->abort ) {

		# Do a redirection
		case 'redirect':

			# Proxy the location
			$location = proxyURL($fetch->headers['location'], $flag);

			# Do not redirect in debug mode
			if ( DEBUG_MODE ) {
				$fetch->redirected = '<a href="' . $location . '">' . $fetch->headers['location'] . '</a>';
				break;
			}

			# Go there
			header('Location: ' . $location, true, $fetch->status);
			exit;


		# Send back a 304 Not modified and stop running the script
		case 'not_modified':
			header("HTTP/1.1 304 Not Modified", true, 304);
			exit;


		# 401 Authentication (HTTP authentication hooks not available in all PHP versions
		# so we have to use our method)
		case 'auth_required':

			# Ensure we have some means of authenticating and extract details about the type of authentication
			if ( ! isset($fetch->headers['www-authenticate']) ) {
				break;
			}

			# Realm to display to the user
			$realm = preg_match('#\brealm="([^"]*)"#i', $fetch->headers['www-authenticate'], $tmp) ? $tmp[1] : '';

			# Prevent caching
			sendNoCache();

			# Prepare template variables (session may be closed at this point so send via form)
			$tmp = array('site'	 => $URL['scheme_host'],
							 'realm'	 => $realm,
							 'return' => currentURL());

			# Show our form and quit
			echo loadTemplate('authenticate.page', $tmp);
			exit;


		# File request above filesize limit
		case 'filesize_limit':

			# If already sent some of the file, we can't display an error
			# so just stop running
			if ( ! $fetch->parseType ) {
				exit;
			}

			# Send to error page with filesize limit expressed in MB
			error('file_too_large', round($CONFIG['max_filesize']/1024/1024, 3));
			exit;


		# >=400 response code (some sort of HTTP error)
		case 'http_status_error':

			# Provide a friendly message
			$explain = isset($httpErrors[$fetch->status]) ? $httpErrors[$fetch->status] : '';

			# Simply forward the error with details
			error('http_error', $fetch->status, trim(substr($fetch->headers[0], 12)), $explain);
			exit;


		# Unknown (shouldn't happen)
		default:
			error('cURL::$abort (' . $fetch->abort .')');
	}

}

# Any cURL errors?
if ( $fetch->error ) {

	error('curl_error', $fetch->error);

}


/*****************************************************************
* Transfer finished and errors handle. Process the file.
******************************************************************/

# Is this AJAX? If so, don't cache, log or parse.
# Also, assume ajax if return is VERY short.
if ( $flag == 'ajax' || ( $fetch->parseType && strlen($document) < 10 ) ) {

	# Print if not already printed
	if ( $fetch->parseType ) {
		echo $document;
	}

	# And exit
	exit;
}

# Do we want to parse the file?
if ( $fetch->parseType ) {

	/*****************************************************************
	* Apply the relevant parsing methods to the document
	******************************************************************/

	# Decode gzip compressed content
	if (isset($fetch->headers['content-encoding']) && $fetch->headers['content-encoding']=='gzip') {
		if (function_exists('gzinflate')) {
			unset($fetch->headers['content-encoding']);
			$document=gzinflate(substr($document,10,-8));
		}
	}

	# Apply preparsing from plugins
	if ( $foundPlugin && function_exists('preParse') ) {
		$document = preParse($document, $fetch->parseType);
	}

	# Load the main parser
	require GLYPE_ROOT . '/includes/parser.php';

	# Create new instance, passing in the options that affect parsing
	$parser = new parser($options, $jsFlags);

	# Method of parsing depends on $parseType
	switch ( $fetch->parseType ) {

		# HTML document
		case 'html':

			# Do we want to insert our own code into the document?
			$inject = 
			$footer = 
			$insert = false;

			# Mini-form only if NOT frame or sniffed
			if ( $flag != 'frame' && $fetch->sniff == false ) {

				# Showing the mini-form?
				if ( $options['showForm'] ) {
					$toShow = array();

					# Prepare the options
					foreach ( $CONFIG['options'] as $name => $details ) {

						# Ignore if forced
						if ( ! empty($details['force']) )  {
							continue;
						}

						# Add to array
						$toShow[] = array(
							'name'		=> $name,
							'title'		=> $details['title'],
							'checked'	=> $options[$name] ? ' checked="checked" ' : ''
						);

					}

					# Prepare variables to pass to template
					if ($options['encodePage']) {
						$vars['url'] = ''; # Currently visited URL
					} else {
						$vars['url'] = $URL['href']; # Currently visited URL
					}
					$vars['toShow']	= $toShow; # Options
					$vars['return']	= rawurlencode(currentURL()); # Return URL (for clearcookies) (i.e. current URL proxied)
					$vars['proxy']	= GLYPE_URL; # Base URL for proxy directory

					# Load the template
					$insert = loadTemplate('framedForm.inc', $vars);

					# Wrap in enable/disble override to prevent the overriden functions
					# affecting anything in the mini-form (like ad codes)
					if ( $CONFIG['override_javascript'] ) {
						$insert = '<script type="text/javascript">disableOverride();</script>'
								  . $insert
								  . '<script type="text/javascript">enableOverride();</script>';
					}
				}

				# And load the footer
				$footer = $CONFIG['footer_include'];

			}

			# Inject javascript unless sniffed
			if ( $fetch->sniff == false ) {
				$inject = true;
			}

			# Run through HTML parser
			$document = $parser->HTMLDocument($document, $insert, $inject, $footer);

			break;


		# CSS file
		case 'css':

			# Run through CSS parser
			$document = $parser->CSS($document);

			break;


		# Javascript file
		case 'javascript':

			# Run through javascript parser
			$document = $parser->JS($document);

			break;

	}

	# Apply postparsing from plugins
	if ( $foundPlugin && function_exists('postParse') ) {
		$document = postParse($document, $fetch->parseType);
	}

	# Send output
	if ( ! DEBUG_MODE ) {

		# Do we want to gzip this? Yes, if all of the following are true:
		#	  - gzip option enabled
		#	  - client supports gzip
		#	  - zlib extension loaded
		#	  - output compression not automated
		if ( $CONFIG['gzip_return'] && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'],'gzip') !== false && extension_loaded('zlib') && ! ini_get('zlib.output_compression') ) {

			# Send compressed (using level 3 compression - can be adjusted
			# to give smaller/larger files but will take longer/shorter time!)
			header('Content-Encoding: gzip');
			echo gzencode($document, 3);

		} else {

			# Send uncompressed
			echo $document;

		}

	}

}

if ( DEBUG_MODE ) {
	# Just dump the $fetch object in DEBUG_MODE
	$fetch->return = $document;
	echo '<pre>', print_r($fetch, 1), '</pre>';
}


/*****************************************************************
* Log the request
******************************************************************/

# Do we want to log? Check we want to log this type of request.
if ( $CONFIG['enable_logging'] && ( $CONFIG['log_all'] || $fetch->parseType == 'html' ) ) {

	# Is the log directory writable?
	if ( checkTmpDir($CONFIG['logging_destination'], 'Deny from all') ) {

		# Filename to save as
		$file = $CONFIG['logging_destination'] . '/' . date('Y-m-d') . '.log';

		# Line to write
		$write = str_pad($_SERVER['REMOTE_ADDR'] . ', ' , 17) . date('d/M/Y:H:i:s O') . ', ' . $URL['href'] . "\r\n";

		# Do it
		file_put_contents($file, $write, FILE_APPEND);

	}

}
