├── ProxyHelperFacade.php ├── example.php ├── LICENSE ├── README.md └── ProxyHelper.php /ProxyHelperFacade.php: -------------------------------------------------------------------------------- 1 | withHeaders(['x-proxy' => 'laravel']) 21 | ->withToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Imp1YW5AZ21haWwuY29tIiwic2oxNTkwMTkyNTE1fQ.db5AZuw3eSAHqjdaRn9AZX8LPbNAxPmuO8BZlEmIGk4') 22 | ->preserveQuery(true) 23 | ->toHost('http://dockerserver.test','api/proxy'); 24 | 25 | })->where('slug', '([A-Za-z0-9\-\/]+)'); 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 juanal98 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel-Proxy-Helper 2 | This is a basic helper for using laravel controllers as proxy servers. 3 | It allows you to forward exact requests to another server and return their response. 4 | Supports all methods, and also files. 5 | 6 | # Required 7 | This helper require Laravel 7 or later. 8 | If you need a lower version, just replace the http client with Guzzle. 9 | 10 | ## What it does 11 | 12 | Its main function is to make requests from one point to another using laravel as a proxy. 13 | 14 | **Normal request:** 15 | 16 | Client ---> Server2 17 | 18 | ``GET server2.test/api/avatar/color`` 19 | 20 | **Request with this helper:** 21 | 22 | Client ---> Laravel ---> Server2 23 | 24 | ``GET laravel.test/api/proxy/api/avatar/color`` 25 | 26 | - Works with all GET, POST, PUT, HEAD, PATCH, DELETE methods 27 | - Works with all formats: JSON / Multipart / www-form-urlencoded 28 | - Supports files and any type of parameters supported by the original requests 29 | - Allows you to add headers, authorization, and custom options. 30 | - Returns the actual request (both content and status code) 31 | - Allows calls to be made dynamically (i.e., any request the customer may have) 32 | 33 | 34 | ## How to add it 35 | Since it is not a package, it may seem more tedious, but it is very simple. 36 | 37 | ### 1. Copy both files: 38 | `ProxyHelperFacade.php` y `ProxyHelper.php` 39 | in the folder you prefer from your laravel project, I've used: `App\Helpers`. 40 | **If you use a diferent folder, change the namespaces of both files.** 41 | 42 | ### 2. Add the facade to your laravel project. 43 | Go to the file: `App\Providers\AppServiceProvider.php` and in the method `register()` adds the facade: 44 | ```php 45 | $this->app->bind('ProxyHelper', function($app) { 46 | return new ProxyHelper(); 47 | }); 48 | ``` 49 | remember that you need to import the class (not the one called facade) in `App\Providers\AppServiceProvider.php` 50 | ```php 51 | use App\Helpers\ProxyHelper; //or your path 52 | ``` 53 | ### 3. Import it wherever you're going to use it. 54 | Now you're gonna use the facade. 55 | ```php 56 | use App\Helpers\ProxyHelperFacade; 57 | ``` 58 | to call him: 59 | ```php 60 | ProxyHelperFacade::CreateProxy($request) 61 | ``` 62 | ## How to use 63 | 64 | It's pretty basic, but you can modify it according to your needs. 65 | 66 | The constructor requires an object ``Illuminate\Http\Request``. 67 | 68 | From there you can forward the request to another server, or a specific url, or if you want to modify/add some options. 69 | 70 | I'll show you an example: 71 | 72 | #### 1. Create controller (or could be middleware) 73 | 74 | In my API drivers: ``routes\api.php`` (could be web, doesn't matter) 75 | 76 | I have created a route to receive any kind of request, and redirect it as to another host. 77 | 78 | ```php 79 | Route::match(['get', 'post', 'head', 'patch', 'put', 'delete'] , 'proxy/{slug}', function(Request $request){ 80 | 81 | 82 | })->where('slug', '([A-Za-z0-9\-\/]+)'); 83 | ``` 84 | This route accepts any call (POST,GET..) and any route from /proxy, i.e. ``my.laravel.test/api/proxy/*`` 85 | 86 | So, when you want to call my ``server2``, I'll do it this way: 87 | 88 | Suppose I want to call GET ``my.server2.test/api/avatar/color``, I'll do it like this: 89 | 90 | GET ``my.laravel.test/proxy/api/avatar/color`` 91 | 92 | #### 2. Use ProxyHelper (Basic) 93 | 94 | I will continue to show the example inside the controller we have created. 95 | 96 | ```php 97 | Route::match(['get', 'post', 'head', 'patch', 'put', 'delete'] , 'proxy/{slug}', function(Request $request){ 98 | 99 | // To redirect the request to a different host, the first parameter will be the host. 100 | // the second, will be the current path that we want to ignore, it must be the url of the controller (api/proxy) 101 | //so we're telling you that the new url will be: 102 | // (host) http://my.server2.test + (deleted)[api/proxy] + ({slug}) /api/avatar/color 103 | return ProxyHelperFacade::CreateProxy($request)->toHost('http://my.server2.test','api/proxy'); 104 | 105 | //other way is to tell him the url directly. 106 | return ProxyHelperFacade::CreateProxy($request)->toUrl('http://my.server2.test/api/avatar/color'); 107 | 108 | // this second way will no longer be dynamic. 109 | 110 | 111 | })->where('slug', '([A-Za-z0-9\-\/]+)'); 112 | ``` 113 | #### 3. Options 114 | 115 | Once we've seen how to make it dynamic or static, there are a few options: 116 | 117 | ```php 118 | Route::match(['get', 'post', 'head', 'patch', 'put', 'delete'] , 'proxy/{slug}', function(Request $request){ 119 | 120 | return ProxyHelperFacade::CreateProxy($request) 121 | // add a header before sending the request 122 | ->withHeaders(['x-custom' => 'customHeader']) 123 | // add a Bearer token (this is useful for the client not to have the token, and from the intermediary proxy we add it. 124 | ->withToken('eyJhbGcLPbNA...') 125 | //Maintain the query of the url. 126 | ->preserveQuery(true) 127 | ->toHost('http://my.server2.test','api/proxy'); 128 | 129 | })->where('slug', '([A-Za-z0-9\-\/]+)'); 130 | ``` 131 | some more: 132 | 133 | ``->withBasicAuth('user','pass')``, 134 | 135 | ``->withDigestAuth('user','pass')``, 136 | 137 | ``->withMethod('POST'`)``, 138 | 139 | And that's it, I hope it helps you, if you want to add more things like custom cookies, follow the same dynamics of the file, it's very simple. 140 | -------------------------------------------------------------------------------- /ProxyHelper.php: -------------------------------------------------------------------------------- 1 | withCookies 28 | // - Request->hasCookie 29 | // - or custom 30 | 31 | //Settings 32 | private $useDefaultAuth; 33 | //It's recommandable check manually (for multipart exceptions and other things) 34 | // private $useDefaultHeaders; 35 | 36 | public function CreateProxy(Request $request, $useDefaultAuth = true /*, $useDefaultHeaders = false*/){ 37 | $this->originalRequest = $request; 38 | $this->multipartParams = $this->GetMultipartParams(); 39 | $this->useDefaultAuth = $useDefaultAuth; 40 | // $this->useDefaultHeaders = $useDefaultHeaders; 41 | return $this; 42 | } 43 | 44 | public function withHeaders($headers){ $this->headers = $headers; return $this; } 45 | 46 | public function withBasicAuth($user, $secret){ $this->authorization = ['type' => 'basic', 'user' => $user, 'secret' => $secret ]; return $this; } 47 | 48 | public function withDigestAuth($user, $secret){ $this->authorization = ['type' => 'digest', 'user' => $user, 'secret' => $secret ]; return $this; } 49 | 50 | public function withToken($token){ $this->authorization = ['type' => 'token', 'token' => $token ]; return $this; } 51 | 52 | public function withMethod($method = 'POST'){ $this->customMethod = $method; return $this; } 53 | 54 | public function preserveQuery($preserve){ $this->addQuery = $preserve; return $this; } 55 | 56 | public function getResponse($url){ 57 | 58 | $info = $this->getRequestInfo(); 59 | 60 | $http = $this->createHttp($info['type']); 61 | $http = $this->setAuth($http, $info['token']); 62 | $http = $this->setHeaders($http); 63 | 64 | if($this->addQuery && $info['query']) 65 | $url = $url.'?'.http_build_query($info['query']); 66 | 67 | $response = $this->call($http, $info['method'], $url, $this->getParams($info)); 68 | 69 | return response($this->isJson($response) ? $response->json() : $response->body(), $response->status()); 70 | } 71 | 72 | public function toUrl($url){ return $this->getResponse($url); } 73 | 74 | public function toHost($host, $proxyController){ 75 | return $this->getResponse($host.str_replace($proxyController, '', $this->originalRequest->path())); 76 | } 77 | 78 | private function getParams($info){ 79 | $defaultParams = []; 80 | if($info['method'] == 'GET') 81 | return $info['params']; 82 | if($info['type'] == 'multipart') 83 | $defaultParams = $this->multipartParams; 84 | else 85 | $defaultParams = $info['params']; 86 | if($info['query']) 87 | foreach ($info['query'] as $key => $value) 88 | unset($defaultParams[array_search(['name' => $key,'contents' => $value], $defaultParams)]); 89 | return $defaultParams; 90 | } 91 | 92 | private function setAuth(PendingRequest $request, $currentAuth = null){ 93 | if(!$this->authorization) 94 | return $request; 95 | switch ($this->authorization['type']) { 96 | case 'basic': 97 | return $request->withBasicAuth($this->authorization['user'],$this->authorization['secret']); 98 | case 'digest': 99 | return $request->withDigestAuth($this->authorization['user'],$this->authorization['secret']); 100 | case 'token': 101 | return $request->withToken($this->authorization['token']); 102 | default: 103 | if($currentAuth && $this->useDefaultAuth) 104 | return $request->withToken($currentAuth); 105 | return $request; 106 | } 107 | } 108 | 109 | private function setHeaders(PendingRequest $request){ 110 | if(!$this->headers) 111 | return $request; 112 | return $request->withHeaders($this->headers); 113 | } 114 | 115 | private function createHttp($type){ 116 | switch ($type) { 117 | case 'multipart': 118 | return Http::asMultipart(); 119 | case 'form': 120 | return Http::asForm(); 121 | case 'json': 122 | return Http::asJson(); 123 | case null: 124 | return new PendingRequest(); 125 | default: 126 | return Http::contentType($type); 127 | } 128 | } 129 | 130 | private function call(PendingRequest $request, $method, $url, $params){ 131 | if($this->customMethod) 132 | $method = $this->customMethod; 133 | switch ($method) { 134 | case 'GET': 135 | return $request->get($url, $params); 136 | case 'HEAD': 137 | return $request->head($url, $params); 138 | default: 139 | case 'POST': 140 | return $request->post($url, $params); 141 | case 'PATCH': 142 | return $request->patch($url, $params); 143 | case 'PUT': 144 | return $request->put($url, $params); 145 | case 'DELETE': 146 | return $request->delete($url, $params); 147 | } 148 | } 149 | 150 | private function getRequestInfo(){ 151 | return [ 152 | 'type' => ($this->originalRequest->isJson() ? 'json' : 153 | (strpos($this->originalRequest->header('Content-Type'),'multipart') !== false ? 'multipart' : 154 | ($this->originalRequest->header('Content-Type') == 'application/x-www-form-urlencoded' ? 'form' : $this->originalRequest->header('Content-Type')))), 155 | 'agent' => $this->originalRequest->userAgent(), 156 | 'method' => $this->originalRequest->method(), 157 | 'token' => $this->originalRequest->bearerToken(), 158 | 'full_url'=>$this->originalRequest->fullUrl(), 159 | 'url'=>$this->originalRequest->url(), 160 | 'format'=>$this->originalRequest->format(), 161 | 'query' =>$this->originalRequest->query(), 162 | 'params' => $this->originalRequest->all(), 163 | ]; 164 | } 165 | 166 | private function GetMultipartParams(){ 167 | $multipartParams = []; 168 | if ($this->originalRequest->isMethod('post')) { 169 | $formParams = $this->originalRequest->all(); 170 | $fileUploads = []; 171 | foreach ($formParams as $key => $param) 172 | if ($param instanceof HttpUploadedFile) { 173 | $fileUploads[$key] = $param; 174 | unset($formParams[$key]); 175 | } 176 | if (count($fileUploads) > 0){ 177 | $multipartParams = []; 178 | foreach ($formParams as $key => $value) 179 | $multipartParams[] = [ 180 | 'name' => $key, 181 | 'contents' => $value 182 | ]; 183 | foreach ($fileUploads as $key => $value) 184 | $multipartParams[] = [ 185 | 'name' => $key, 186 | 'contents' => fopen($value->getRealPath(), 'r'), 187 | 'filename' => $value->getClientOriginalName(), 188 | 'headers' => [ 189 | 'Content-Type' => $value->getMimeType() 190 | ] 191 | ]; 192 | } 193 | } 194 | return $multipartParams; 195 | } 196 | 197 | private function isJson(Response $response){ 198 | return strpos($response->header('Content-Type'),'json') !== false; 199 | } 200 | } 201 | --------------------------------------------------------------------------------