$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 (and possible? safe_mode prevents shell_exec) ! SAFE_MODE && $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, ' $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 if ( ! SAFE_MODE ) { 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]; } } 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? Determines if ascii or binary. if ( $this->sniff ) { # Take a sample of 100 chars chosen at random $length = strlen($data); $sample = $length < 150 ? $data : substr($data, rand(0, $length-100), 100); # Assume ASCII if more than 95% of bytes are "normal" text characters if ( strlen(preg_replace('#[^A-Z0-9\!"$%\^&*\(\)=\+\\\\|\[\]\{\};:\\\'\@\#~,\.<>/\?\-]#i', '', $sample)) > 95 ) { # To do: expand this to detect if html/js/css $this->parseType = 'html'; } } # 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 = '' . $fetch->headers['location'] . ''; 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 = '' . $insert . ''; } } # 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 '
', print_r($fetch, 1), '
'; } /***************************************************************** * 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); } }