Initial commit

This commit is contained in:
Michael Reber 2019-11-12 16:59:19 +01:00
parent 0a00b0b27f
commit 56f53d15ff
69 changed files with 7157 additions and 0 deletions

57
config.php Normal file
View File

@ -0,0 +1,57 @@
<?php
// all possible options will be stored
$config = array();
$config['website-name'] = "Web Proxy";
$config['access-password'] = "MY_PASSWORD_TO_ACCESS";
$config['website-url'] = "MY_FQDN/webproxy";
$config['website-description'] = "Web Proxy";
// a unique key that identifies this application - DO NOT LEAVE THIS EMPTY!
$config['app_key'] = 'ADD_HERE_SOME_RANDOM_STRING';
// a secret key to be used during encryption
$config['encryption_key'] = '';
/*
how unique is each URL that is generated by this proxy app?
0 - no encoding of any sort. People can link to proxy pages directly: ?q=http://www.yahoo.com
1 - Base64 encoding only, people can hotlink to your proxy
2 - unique to the IP address that generated it. A person that generated that URL, can bookmark it and visit it and any point
3 - unique to that session and IP address - URL no longer valid anywhere when that browser session that generated it ends
*/
$config['url_mode'] = 2;
// plugins to load - plugins will be loaded in this exact order as in array
$config['plugins'] = array(
'HeaderRewrite',
'Stream',
// ^^ do not disable any of the plugins above
'Cookie',
'Proxify',
'UrlForm',
// site specific plugins below
'Youtube',
'DailyMotion',
'Twitter'
);
// additional curl options to go with each request
$config['curl'] = array(
// CURLOPT_PROXY => '',
// CURLOPT_CONNECTTIMEOUT => 5
);
//$config['replace_title'] = 'Google Search';
//$config['error_redirect'] = "https://unblockvideos.com/#error={error_msg}";
//$config['index_redirect'] = 'https://unblockvideos.com/';
// $config['replace_icon'] = 'icon_url';
// this better be here other Config::load fails
return $config;
?>

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

145
index.php Normal file
View File

@ -0,0 +1,145 @@
<?php
require(__DIR__.'/login.php');
define('PROXY_START', microtime(true));
require("vendor/autoload.php");
use Proxy\Http\Request;
use Proxy\Http\Response;
use Proxy\Plugin\AbstractPlugin;
use Proxy\Event\FilterEvent;
use Proxy\Config;
use Proxy\Proxy;
// start the session
session_start();
// load config...
Config::load('./config.php');
// custom config file to be written to by a bash script or something
Config::load('./custom_config.php');
if (!Config::get('app_key')) {
die("app_key inside config.php cannot be empty!");
}
if (!function_exists('curl_version')) {
die("cURL extension is not loaded!");
}
// how are our URLs be generated from this point? this must be set here so the proxify_url function below can make use of it
if (Config::get('url_mode') == 2) {
Config::set('encryption_key', md5(Config::get('app_key') . $_SERVER['REMOTE_ADDR']));
} else if (Config::get('url_mode') == 3) {
Config::set('encryption_key', md5(Config::get('app_key') . session_id()));
}
// very important!!! otherwise requests are queued while waiting for session file to be unlocked
session_write_close();
// form submit in progress...
if (isset($_POST['url'])) {
$url = $_POST['url'];
if (strpos ($url, '.') !== false){
$url = add_http($url);
header("HTTP/1.1 302 Found");
header('Location: '.proxify_url($url));
exit;
}
else {
$url = 'http://www.google.com/search?q=' . urlencode($url);
$url = add_http($url);
header("HTTP/1.1 302 Found");
header('Location: '.proxify_url($url));
exit;
}
} else if (!isset($_GET['q'])) {
// must be at homepage - should we redirect somewhere else?
if (Config::get('index_redirect')) {
// redirect to...
header("HTTP/1.1 302 Found");
header("Location: " . Config::get('index_redirect'));
} else {
if (isset($_GET["tos"]) != "") {
echo render_template("./templates/tos.php", array(
'version' => Proxy::VERSION
));
} else {
echo render_template("./templates/main.php", array('version' => Proxy::VERSION));
}
}
exit;
}
// decode q parameter to get the real URL
$url = url_decrypt($_GET['q']);
$proxy = new Proxy();
// load plugins
foreach (Config::get('plugins', array()) as $plugin) {
$plugin_class = $plugin . 'Plugin';
if (file_exists('./plugins/' . $plugin_class . '.php')) {
// use user plugin from /plugins/
require_once('./plugins/' . $plugin_class . '.php');
} else if (class_exists('\\Proxy\\Plugin\\' . $plugin_class)) {
// does the native plugin from php-proxy package with such name exist?
$plugin_class = '\\Proxy\\Plugin\\' . $plugin_class;
}
// otherwise plugin_class better be loaded already through composer.json and match namespace exactly \\Vendor\\Plugin\\SuperPlugin
$proxy->getEventDispatcher()->addSubscriber(new $plugin_class());
}
try {
// request sent to index.php
$request = Request::createFromGlobals();
// remove all GET parameters such as ?q=
$request->get->clear();
// forward it to some other URL
$response = $proxy->forward($request, $url);
// if that was a streaming response, then everything was already sent and script will be killed before it even reaches this line
$response->send();
} catch (Exception $ex) {
// if the site is on server2.proxy.com then you may wish to redirect it back to proxy.com
if (Config::get("error_redirect")) {
$url = render_string(Config::get("error_redirect"), array(
'error_msg' => rawurlencode($ex->getMessage())
));
// Cannot modify header information - headers already sent
header("HTTP/1.1 302 Found");
header("Location: {$url}");
} else {
echo render_template("./templates/main.php", array(
'url' => $url,
'error_msg' => $ex->getMessage(),
'version' => Proxy::VERSION
));
}
}
?>

9
language.php Normal file
View File

@ -0,0 +1,9 @@
<?php
$lang["slogan"] = "remove the borders";
$lang["wikipedia"] = "Wikipedia";
$lang["back"] = "Back";
$lang["go"] = "Go";
$lang["home"] = "Home";
$lang["tos"] = "This is an private Web Proxy - Unauthorized use is strictly prohibited!";
$lang["agree"] = "michu-IT";
$lang["tos_2"] = "terms of use";

429
login.php Normal file

File diff suppressed because one or more lines are too long

25
plugins/TestPlugin.php Normal file
View File

@ -0,0 +1,25 @@
<?php
use Proxy\Plugin\AbstractPlugin;
use Proxy\Event\ProxyEvent;
class TestPlugin extends AbstractPlugin {
public function onBeforeRequest(ProxyEvent $event){
// fired right before a request is being sent to a proxy
}
public function onHeadersReceived(ProxyEvent $event){
// fired right after response headers have been fully received - last chance to modify before sending it back to the user
}
public function onCurlWrite(ProxyEvent $event){
// fired as the data is being written piece by piece
}
public function onCompleted(ProxyEvent $event){
// fired after the full response=headers+body has been read - will only be called on "non-streaming" responses
}
}
?>

39
plugins/UrlFormPlugin.php Normal file
View File

@ -0,0 +1,39 @@
<?php
use Proxy\Plugin\AbstractPlugin;
use Proxy\Event\ProxyEvent;
class UrlFormPlugin extends AbstractPlugin {
public function onCompleted(ProxyEvent $event){
$request = $event['request'];
$response = $event['response'];
$url = $request->getUri();
// we attach url_form only if this is a html response
if(!is_html($response->headers->get('content-type'))){
return;
}
// this path would be relative to index.php that included it?
$url_form = render_template("./templates/url_form.php", array(
'url' => $url
));
$output = $response->getContent();
// does the html page contain <body> tag, if so insert our form right after <body> tag starts
$output = preg_replace('@<body.*?>@is', '$0'.PHP_EOL.$url_form, $output, 1, $count);
// <body> tag was not found, just put the form at the top of the page
if($count == 0){
$output = $url_form.$output;
}
$response->setContent($output);
}
}
?>

84
templates/main.php Normal file
View File

@ -0,0 +1,84 @@
<?php
require(__DIR__ . "/../language.php");
require(__DIR__ . "/../config.php");
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title><?php echo $config['website-name']; ?></title>
<meta charset="utf-8">
<meta name="description" content="<?php echo $config["website-description"]; ?>">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel='shortcut icon' type='image/x-icon' href='favicon.ico' />
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css">
<style type="text/css">
body {
background-color: #373737 !important;
background-image: url(https://www.blackgate.org/wood.jpg);
}
.jumbotron {
background-color: #e9ecefc9 !important;
}
</style>
</head>
<body>
<div class="container text-center" style="margin-top:7%;">
<div class="row">
<div class="col-md-12">
<?php if (isset($error_msg)) { ?>
<div class="alert alert-danger" role="alert">
<p><?php echo $error_msg; ?></p>
</div>
<?php } ?>
<div class="jumbotron">
<h1 class="display-3"><?php echo $config['website-name']; ?></h1>
<p class="lead"><?php echo $lang["slogan"]; ?></p>
<div class="btn-group" role="group">
<a id="twitter" class="btn btn-info" href="#">Twitter</a>
<a id="youtube" class="btn btn-danger" href="#">YouTube</a>
<a id="facebook" class="btn btn-primary" href="#">Facebook</a>
</div>
<br>
<br>
<div class="btn-group" role="group">
<a id="vikipedi" class="btn btn-secondary" href="#"><?php echo $lang["wikipedia"]; ?></a>
<a id="google" class="btn btn-success" href="#">Google</a>
</div>
<p class="lead">
<form class="form-group" action="index.php" method="post">
<input id="url" name="url" type="text" class="form-control" autocomplete="on" placeholder="URL or Search"
autofocus required/>
<br>
<input class="btn btn-primary btn-lg" type="submit" value="<?php echo $lang["go"]; ?>"/>
</form>
</div>
<div class="text-center">
<p style="font-size:15px; color:white">
<small><?php echo $lang["agree"]; ?> <a
href="<?php echo $config['website-url']; ?>/?tos"><?php echo $lang["tos_2"]; ?></a>
</small>
</p>
</div>
</div>
</div>
<script src="//code.jquery.com/jquery-1.9.1.js"></script>
<script>
$('#twitter').click(function () {
$('#url').val('https://twitter.com');
});
$('#youtube').click(function () {
$('#url').val('https://youtube.com');
});
$('#facebook').click(function () {
$('#url').val('https://facebook.com');
});
$('#google').click(function () {
$('#url').val('https://google.com');
});
$('#vikipedi').click(function () {
$('#url').val('https://wikipedia.org');
});
</script>
</div>
</body>
</html>

BIN
templates/proxy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

30
templates/tos.php Normal file
View File

@ -0,0 +1,30 @@
<?php
require(__DIR__ . "/../language.php");
require(__DIR__ . "/../config.php");
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title><?php echo $config['website-name']; ?></title>
<meta charset="utf-8">
<meta name="description" content="<?php echo $config["website-description"]; ?>">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel='shortcut icon' type='image/x-icon' href='favicon.ico' />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css">
</head>
<body>
<div class="container text-center" style="margin-top:5%;">
<div class="row">
<div class="col-md-12">
<div class="jumbotron">
<h1 class="display-3"><?php echo $config['website-name']; ?></h1>
<p class="lead"><?php echo $lang["slogan"]; ?></p>
<p><?php echo $lang["tos"]; ?></p>
<a href="<?php echo $config['website-url']; ?>"
class="btn btn-primary btn-lg"><?php echo $lang["back"]; ?></a>
</div>
</div>
</div>
</div>
</body>
</html>

80
templates/url_form.php Normal file
View File

@ -0,0 +1,80 @@
<?php
require(__DIR__ . "/../language.php");
?>
<style type="text/css">
html body {
margin-top: 50px !important;
position: relative;
}
#top_form {
position: fixed;
top: 0;
left: 0;
width: 100%;
margin: 0;
z-index: 2100000000;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
border-bottom: 1px solid #151515;
background: #CCCCCC;
height: 45px;
line-height: 45px;
}
#top_form input[name=url] {
width: 550px;
height: 20px;
padding: 5px;
font: 13px "Helvetica Neue", Helvetica, Arial, sans-serif;
border: 0px none;
background: none repeat scroll 0% 0% #FFF;
}
</style>
<script>
var url_text_selected = false;
function smart_select(ele) {
ele.onblur = function () {
url_text_selected = false;
};
ele.onclick = function () {
if (url_text_selected == false) {
this.focus();
this.select();
url_text_selected = true;
}
};
}
</script>
<div id="top_form">
<div style="width:800px; margin:0 auto;">
<form method="post" action="index.php" target="_top" style="margin:0; padding:0;">
<input type="button" value="<?php echo $lang["home"]; ?>" onclick=" window.location.href='index.php'">
<input type="text" name="url" value="<?php echo $url; ?>" autocomplete="off">
<input type="hidden" name="form" value="1">
<input type="submit" value="<?php echo $lang["go"]; ?>">
</form>
</div>
</div>
<script type=" text/javascript">
smart_select(document.getElementsByName("url")[0]);
</script>

@ -0,0 +1 @@
Subproject commit 172202c9b7913dc397b0a2eeacb68984a73f5c6e

View File

@ -0,0 +1,3 @@
/vendor/*
composer.lock
.htaccess

21
vendor/athlon1600/php-proxy/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

99
vendor/athlon1600/php-proxy/README.md vendored Normal file
View File

@ -0,0 +1,99 @@
php-proxy
=========
Proxy script built on PHP, Symfony and cURL.
This library borrows ideas from Glype, Jenssegers proxy, and Guzzle.
PHP-Proxy Web Application
-------
If you're looking for a **project** version of this script that functions as a Web Application similar to Glype, then visit
[**php-proxy-app**](https://github.com/Athlon1600/php-proxy-app)
See this php-proxy in action:
<a href="https://unblockvideos.com/" target="_blank">UnblockVideos.com</a>
Installation
-------
Install it using [Composer](http://getcomposer.org):
```bash
composer require athlon1600/php-proxy
```
Example
--------
```php
require('vendor/autoload.php');
use Proxy\Http\Request;
use Proxy\Proxy;
$request = Request::createFromGlobals();
$proxy = new Proxy();
$proxy->getEventDispatcher()->addListener('request.before_send', function($event){
$event['request']->headers->set('X-Forwarded-For', 'php-proxy');
});
$proxy->getEventDispatcher()->addListener('request.sent', function($event){
if($event['response']->getStatusCode() != 200){
die("Bad status code!");
}
});
$proxy->getEventDispatcher()->addListener('request.complete', function($event){
$content = $event['response']->getContent();
$content .= '<!-- via php-proxy -->';
$event['response']->setContent($content);
});
$response = $proxy->forward($request, "https://www.yahoo.com");
// send the response back to the client
$response->send();
```
Plugin Example
--------
```php
namespace Proxy\Plugin;
use Proxy\Plugin\AbstractPlugin;
use Proxy\Event\ProxyEvent;
use Proxy\Html;
class MultiSiteMatchPlugin extends AbstractPlugin {
// Matches multiple domain names (abc.com, abc.de, abc.pl) using regex (you MUST use / character)
protected $url_pattern = '/^abc\.(com|de|pl)$/is';
// Matches a single domain name
//protected $url_pattern = 'abc.com';
public function onCompleted(ProxyEvent $event){
$response = $event['response'];
$html = $response->getContent();
// do your stuff here...
$response->setContent($html);
}
}
```
Notice that you must use the **/** character for regexes on ```$url_pattern```

View File

@ -0,0 +1,21 @@
{
"name": "athlon1600/php-proxy",
"type": "library",
"keywords": ["php proxy", "proxy script", "php web proxy", "web proxy", "php proxy script"],
"homepage": "https://www.php-proxy.com/",
"require": {
"ext-curl": "*",
"symfony/event-dispatcher": "~3.2"
},
"suggest": {
"predis/predis": "For caching purposes"
},
"autoload": {
"psr-4": {
"Proxy\\": "src/"
},
"files": [
"src/helpers.php"
]
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Proxy;
// based off of this:
// http://v3.golaravel.com/api/source-class-Laravel.Config.html#3-235
class Config {
private static $config = array();
public static function get($key, $default = null){
return self::has($key) ? static::$config[$key] : $default;
}
public static function set($key, $value){
self::$config[$key] = $value;
}
public static function has($key){
return isset(static::$config[$key]);
}
public static function load($path){
if(file_exists($path)){
// Successful includes, unless overridden by the included file, return 1.
$data = require($path);
if(is_array($data)){
self::$config = array_merge(self::$config, $data);
}
}
}
}
?>

View File

@ -0,0 +1,39 @@
<?php
namespace Proxy\Event;
use Symfony\Component\EventDispatcher\Event;
// http://symfony.com/doc/current/components/event_dispatcher/generic_event.html
class ProxyEvent extends Event implements \ArrayAccess {
private $data;
public function __construct($data = array()){
$this->data = $data;
}
public function offsetSet($offset, $value){
if(is_null($offset)) {
$this->data[] = $value;
} else {
$this->data[$offset] = $value;
}
}
public function offsetExists($offset){
return isset($this->data[$offset]);
}
public function offsetUnset($offset){
unset($this->data[$offset]);
}
public function offsetGet($offset){
return isset($this->data[$offset]) ? $this->data[$offset] : null;
}
}
?>

182
vendor/athlon1600/php-proxy/src/Html.php vendored Normal file
View File

@ -0,0 +1,182 @@
<?php
namespace Proxy;
class Html {
public static function remove_scripts($html){
$html = preg_replace('/<\s*script[^>]*>(.*?)<\s*\/\s*script\s*>/is', '', $html);
return $html;
}
public static function remove_styles($html){
$html = preg_replace('/<\s*style[^>]*>(.*?)<\s*\/\s*style\s*>/is', '', $html);
return $html;
}
public static function remove_comments($html){
return preg_replace('/<!--(.*?)-->/s', '', $html);
}
private static function find($selector, $html, $start_from = 0){
$html = substr($html, $start_from);
$inner_start = 0;
$inner_end = 0;
$pattern = '//';
if(substr($selector, 0, 1) == '#'){
$pattern = '/<(\w+)[^>]+id="'.substr($selector, 1).'"[^>]*>/is';
} else if(substr($selector, 0, 1) == '.'){
$pattern = '/<(\w+)[^>]+class="'.substr($selector, 1).'"[^>]*>/is';
} else {
return false;
}
if(preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE)){
$outer_start = $matches[0][1];
$inner_start = $matches[0][1] + strlen($matches[0][0]);
// tag stuff
$tag_name = $matches[1][0];
$tag_len = strlen($tag_name);
$run_count = 300;
// "open" <tag elements we found so far
$open_count = 1;
$start = $inner_start;
while($open_count != 0 && $run_count-- > 0){
$open_tag = strpos($html, "<{$tag_name}", $start);
$close_tag = strpos($html, "</{$tag_name}", $start);
// nothing was found?
if($open_tag === false && $close_tag === false){
break;
}
//echo "open_tag: {$open_tag}, close_tag {$close_tag}\r\n";
// found OPEN tag
if($close_tag === false || ($open_tag !== false && $open_tag < $close_tag) ){
$open_count++;
$start = $open_tag + $tag_len + 1;
//echo "found open tag: ".substr($html, $open_tag, 20)." at {$open_tag} \r\n";
// found CLOSE tag
} else if($open_tag === false || ($close_tag !== false && $close_tag < $open_tag) ){
$open_count--;
$start = $close_tag + $tag_len + 2;
//echo "found close tag: ".substr($html, $close_tag, 20)." at {$close_tag} \r\n";
}
}
// something went wrong... don't bother returning anything
if($open_count != 0){
return false;
}
$outer_end = $close_tag + $tag_len + 3;
$inner_end = $close_tag;
return array(
'outer_start' => $outer_start + $start_from,
'inner_start' => $inner_start + $start_from,
'inner_end' => $inner_end + $start_from,
'outer_end' => $outer_end + $start_from
);
}
return false;
}
public static function extract_inner($selector, $html){
return self::extract($selector, $html, true);
}
public static function extract_outer($selector, $html){
return self::extract($selector, $html, false);
}
private static function extract($selector, $html, $inner = false){
$pos = 0;
$limit = 300;
$result = array();
$data = false;
do {
$data = self::find($selector, $html, $pos);
if($data){
$code = substr($html, $inner ? $data['inner_start'] : $data['outer_start'],
$inner ? $data['inner_end'] - $data['inner_start'] : $data['outer_end'] - $data['outer_start']);
$result[] = $code;
$pos = $data['outer_end'];
}
} while ($data && --$limit > 0);
return $result;
}
public static function remove($selector, $html){
return self::replace($selector, '', $html, false);
}
public static function replace_outer($selector, $replace, $html, &$matches = NULL){
return self::replace($selector, $replace, $html, false, $matches);
}
public static function replace_inner($selector, $replace, $html, &$matches = NULL){
return self::replace($selector, $replace, $html, true, $matches);
}
private static function replace($selector, $replace, $html, $replace_inner = false, &$matches = NULL){
$start_from = 0;
$limit = 300;
$data = false;
$replace = (array)$replace;
do {
$data = self::find($selector, $html, $start_from);
if($data){
$r = array_shift($replace);
// from where to where will we be replacing?
$replace_space = $replace_inner ? $data['inner_end'] - $data['inner_start'] : $data['outer_end'] - $data['outer_start'];
$replace_len = strlen($r);
if($matches !== NULL){
$matches[] = substr($html, $replace_inner ? $data['inner_start'] : $data['outer_start'], $replace_space);
}
$html = substr_replace($html, $r, $replace_inner ? $data['inner_start'] : $data['outer_start'], $replace_space);
// next time we resume search at position right at the end of this element
$start_from = $data['outer_end'] + ($replace_len - $replace_space);
}
} while ($data && --$limit > 0);
return $html;
}
}
?>

View File

@ -0,0 +1,84 @@
<?php
namespace Proxy\Http;
/*
heavily borrowed from Symfony's ParameterBag and Guzzle Collection
https://github.com/guzzle/guzzle/blob/v3.5.0/src/Guzzle/Common/Collection.php
*/
class ParamStore {
protected $data = array();
protected $case_sensitive;
public function __construct($parameters = array(), $case_sensitive = false){
$this->data = $parameters;
$this->case_sensitive = $case_sensitive;
}
private function normalizeKey($key){
return $this->case_sensitive ? $key : strtolower($key);
}
public function set($key, $value, $replace = true){
$key = $this->normalizeKey($key);
// replacing or does not have existing key filled yet
if($replace || !$this->has($key)){
$this->data[$key] = $value;
} else {
if(is_array($this->data[$key])){
$this->data[$key][] = $value;
} else {
$this->data[$key] = array($this->data[$key], $value);
}
}
}
public function replace(array $data){
// remove all existing items first
$this->clear();
foreach($data as $key => $value){
$this->set($key, $value);
}
}
public function remove($key){
unset($this->data[$this->normalizeKey($key)]);
}
public function clear(){
$this->data = array();
}
public function has($key){
return isset($this->data[$this->normalizeKey($key)]);
}
public function get($key, $default = null){
$key = $this->normalizeKey($key);
return $this->has($key) ? $this->data[$key] : $default;
}
// Returns an array of all values currently stored
public function all(){
return $this->data;
}
public function __toString(){
return json_encode($this->data, true);
}
}
?>

View File

@ -0,0 +1,362 @@
<?php
namespace Proxy\Http;
use Proxy\Http\ParamStore;
class Request {
private $method;
private $url;
private $protocol_version = '1.1';
// Custom attributes to go with each Request instance - has nothing to do with HTTP
public $params; // parameters
// HTTP headers for this request - all in lowercase
public $headers;
// Here we store cookies for that request
//public $cookies;
// Collection of POST fields to be submitted
public $post;
// Query string parameters for URL
public $get;
// Files to be uploaded with POST
public $files;
// User set body contents
private $body = null;
// Library generated body that is regenerated through prepare method
private $prepared_body = null;
public function __construct($method, $url, $headers = array(), $body = null){
$this->params = new ParamStore();
$this->headers = new ParamStore();
// http params
$this->post = new ParamStore(null, true);
$this->get = new ParamStore(null, true);
$this->files = new ParamStore(null, true);
$this->setMethod($method);
$this->setUrl($url);
$this->setBody($body);
// make the request ready to be sent right from the start - prepare must be called manually from this point on if you ever add post or file parameters
$this->prepare();
}
/*
Does multiple things
- regenerate content body based on $post and $files parameters
- set content-type, content-length headers
- set transfer-encoding, expect headers
*/
public function prepare(){
/*
Any HTTP/1.1 message containing an entity-body SHOULD include a Content-Type header field defining the media type of that body.
http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
*/
// Must be a multipart request
if($this->files->all()){
$boundary = self::generateBoundary();
$this->prepared_body = Request::buildPostBody($this->post->all(), $this->files->all(), $boundary);
$this->headers->set('content-type', 'multipart/form-data; boundary='.$boundary);
} else if($this->post->all()){
$this->prepared_body = http_build_query($this->post->all());
$this->headers->set('content-type', 'application/x-www-form-urlencoded');
} else {
$this->headers->set('content-type', $this->detectContentType($this->body));
$this->prepared_body = $this->body;
}
/*
The transfer-length of a message is the length of the message-body as it appears in the message; that is, after any transfer-codings have been applied.
http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
*/
$len = strlen($this->prepared_body);
if($len > 0){
$this->headers->set('content-length', $len);
} else {
$this->headers->remove('content-length');
$this->headers->remove('content-type');
}
}
public function __toString(){
$str = $this->getMethod().' '.$this->getUrl().' HTTP/'.$this->getProtocolVersion()."\r\n";
return $str.$this->getRawHeaders()."\r\n\r\n".$this->getRawBody();
}
public function setMethod($method){
$this->method = strtoupper($method);
}
public function getMethod(){
return $this->method;
}
// this was no longer working --- https://github.com/guzzle/psr7/blob/master/src/functions.php
public static function parseQuery($query){
$result = array();
parse_str($query, $result);
return $result;
}
public function setUrl($url){
// remove hashtag - preg_replace so we don't have to check for its existence first - is it possible preserving hashtag?
$url = preg_replace('/#.*/', '', $url);
// check if url has any query parameters
$query = parse_url($url, PHP_URL_QUERY);
// remove it and add the query params to get collection
if($query){
//$url = str_replace('?'.$query, '', $url);
$url = preg_replace('/\?.*/', '', $url);
$result = self::parseQuery($query);
$this->get->replace($result);
}
// url without query params - those will be appended later
$this->url = $url;
$this->headers->set('host', parse_url($url, PHP_URL_HOST));
}
public function getRawHeaders(){
$result = array();
$headers = $this->headers->all();
// Sort headers by name
//ksort($headers);
// Turn this into name=value pairs
foreach($headers as $name => $values){
// could be an array if multiple headers are sent with the same name?
foreach( (array)$values as $value){
$name = implode('-', array_map('ucfirst', explode('-', $name)));
$result[] = sprintf("%s: %s", $name, $value);
}
}
return implode("\r\n", $result);
}
public function getUrl(){
// does this URL have any query parameters?
if($this->get->all()){
return $this->url.'?'.http_build_query($this->get->all());
}
return $this->url;
}
public function getUri(){
return call_user_func_array(array($this, "getUrl"), func_get_args());
}
public function setProtocolVersion($version){
$this->protocol_version = $version;
}
public function getProtocolVersion(){
return $this->protocol_version;
}
// Set raw contents of the body
// this will clear all the values currently stored in POST and FILES
// will be ignored during PREPARE if post or files contain any values
public function setBody($body, $content_type = false){
// clear old body data
$this->post->clear();
$this->files->clear();
// is this form data?
if(is_array($body)){
$body = http_build_query($body);
}
$this->body = (string)$body;
// plain text should be: text/plain; charset=UTF-8
if($content_type){
$this->headers->set('content-type', $content_type);
}
// do it!
$this->prepare();
}
private static function generateBoundary(){
return '-----'.md5(microtime().rand());
}
// can be $_POST and $_FILES
public static function buildPostBody($fields, $files, $boundary = null){
// the reason BODY part is not included in sprintf pattern is because of limits
$part_field = "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n";
$part_file = "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n";
// each part should be preceeded by this line
if(!$boundary){
$boundary = self::generateBoundary();
}
$body = '';
foreach($fields as $name => $value){
$body .= sprintf($part_field, $boundary, $name, $value);
$body .= "{$value}\r\n";
}
// data better have [name, tmp_name, and optional type]
foreach($files as $name => $values) {
// Multiple files can be uploaded using different name for input.
// See http://php.net/manual/en/features.file-upload.multiple.php
if (!is_array($values['tmp_name'])) {
$multiValues = array_map(function ($a) {
return (array)$a;
}, $values);
$fieldName = $name;
} else {
$multiValues = $values;
$fieldName = "{$name}[]";
}
foreach (array_keys($multiValues['tmp_name']) as $key) {
// There must be no error http://php.net/manual/en/features.file-upload.errors.php
if (!$multiValues['tmp_name'][$key] || $multiValues['error'][$key] !== 0 || !is_readable($multiValues['tmp_name'][$key])) {
continue;
}
$body .= sprintf($part_file, $boundary, $fieldName, $multiValues['name'][$key], $multiValues['type'][$key]);
$body .= file_get_contents($multiValues['tmp_name'][$key]);
$body .= "\r\n";
}
}
$body .= "--{$boundary}--\r\n\r\n";
return $body;
}
private function detectContentType($data){
// http://www.w3.org/Protocols/rfc1341/4_Content-Type.html
// If the media type remains unknown, the recipient SHOULD treat it as type "application/octet-stream".
$content_type = 'application/octet-stream';
if(preg_match('/^{\s*"[^"]+"\s*:/', $data)){
$content_type = 'application/json';
} else if(preg_match('/^(?:<\?xml[^?>]+\?>)\s*<[^>]+>/i', $data)){
$content_type = 'application/xml';
} else if(preg_match('/^[a-zA-Z0-9_.~-]+=[^&]*&/', $data)){
$content_type = 'application/x-www-form-urlencoded';
}
return $content_type;
}
// Returns a parsed version of the body
/*
public function getBody(){
// what is the content type?
$content_type = $this->headers->get('content-type', '');
switch($content_type){
case 'application/x-www-form-urlencoded':
$result = array();
mb_parse_str($this->body, $result);
return $result;
case 'application/json':
return json_decode($this->body);
case 'text/xml':
case 'application/xml':
case 'application/x-xml':
return simplexml_load_string($this->body);
}
return null;
}
*/
// Returns raw body string exactly as it appears in the HTTP request
public function getRawBody(){
return $this->prepared_body;
}
public static function createFromGlobals(){
$method = $_SERVER['REQUEST_METHOD'];
$scheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']) ? 'https' : 'http';
$url = $scheme.'://'. $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
$request = new Request($method, $url);
// fill in headers
foreach($_SERVER as $name => $value){
if(strpos($name, 'HTTP_') === 0){
$name = substr($name, 5);
$name = str_replace('_', ' ', $name);
$name = ucwords(strtolower($name));
$name = str_replace(' ', '-', $name);
$request->headers->set($name, $value);
}
}
// for extra convenience
//$request->params->set('user-ip', $_SERVER['REMOTE_ADDR']);
// will be empty if content-type is multipart
$input = file_get_contents("php://input");
if(count($_FILES) > 0){
$request->post->replace($_POST);
$request->files->replace($_FILES);
} else if(count($_POST) > 0){
$request->post->replace($_POST);
} else {
$request->setBody($input);
}
// for extra convenience
$request->prepare();
return $request;
}
}
?>

View File

@ -0,0 +1,120 @@
<?php
namespace Proxy\Http;
use Proxy\Http\ParamStore;
class Response {
protected $statusCodes = array(
100 => 'Continue',
101 => 'Switching Protocols',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Time-out',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Large',
415 => 'Unsupported Media Type',
416 => 'Requested range not satisfiable',
417 => 'Expectation Failed',
429 => 'Too Many Requests',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Time-out',
505 => 'Unsupported Version'
);
public $status;
public $headers;
private $content;
// getHeaderLines
public function __construct($content = '', $status = 200, $headers = array()){
$this->headers = new ParamStore($headers);
$this->setContent($content);
$this->setStatusCode($status);
}
public function setStatusCode($code){
$this->status = $code;
}
public function getStatusCode(){
return $this->status;
}
public function getStatusText(){
return $this->statusCodes[$this->getStatusCode()];
}
public function setContent($content){
$this->content = (string)$content;
}
public function getContent(){
return $this->content;
}
public function sendHeaders(){
if(headers_sent()){
return;
}
header(sprintf('HTTP/1.1 %s %s', $this->status, $this->getStatusText()), true, $this->status);
foreach($this->headers->all() as $name => $value){
/*
Multiple message-header fields with the same field-name MAY be present in a message
if and only if the entire field-value for that header field is defined as a comma-separated list
http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
*/
$values = is_array($value) ? $value : array($value);
// false = do not replace previous identical header
foreach($values as $value){
header("{$name}: {$value}", false);
}
}
}
public function send(){
$this->sendHeaders();
echo $this->content;
}
}
?>

View File

@ -0,0 +1,80 @@
<?php
namespace Proxy\Plugin;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Proxy\Event\ProxyEvent;
abstract class AbstractPlugin implements EventSubscriberInterface {
// apply these methods only to those events whose request URL passes this filter
protected $url_pattern;
public function onBeforeRequest(ProxyEvent $event){
// fired right before a request is being sent to a proxy
}
public function onHeadersReceived(ProxyEvent $event){
// fired right after response headers have been fully received - last chance to modify before sending it back to the user