Jump to content

Using Slim and Guzzle as a proxy


NotionCommotion

Recommended Posts

Using Slim to route endpoints to my application.  In addition, I have many endpoints (mostly accessed via xhr) which need to be forwarded to another server, and I am using Guzzle to do so.  Note only do I have to transfer text/json, I also have to send and retrieve files (currently only csv files, but will later add pdf).  Accomplishing this was easier than I expected, but expect I may still be doing certain portions wrong.  Anything look off, especially with the multipart forms for file uploads as well as the downloading of files?  Thank you

<?php

$app = new \Slim\App($container);

//Local requests
$app->get('/settings', function (Request $request, Response $response) {
    return $this->view->render($response, 'somePage.html',$this->bla->getData());
});
//more local endpoints...


$proxyEndpoints=[
    '/bla'=>['put'],
    '/bla/bla/{id:[0-9]+}'=>['delete','put'],
    '/foo/{id:[0-9]+}'=>['get','put','delete','post'],
    //more proxy endpoints...
];

foreach ($proxyEndpoints as $route=>$methods) {
    foreach ($methods as $method) {
        $app->$method($route, function(Request $request, Response $response) {
            return $this->remoteServer->proxy($request, $response);	//add content type if desired.
        });
    }
}
<?php
class RemoteServer
{

    protected $httpClient, $contentType;

    public function __construct(\GuzzleHttp\Client $httpClient, string $contentType='application/json')
    {
        $this->httpClient=$httpClient;
        $this->contentType=$contentType;
    }

    public function proxy(\Slim\Http\Request $request, \Slim\Http\Response $response, string $contentType=null, \Closure $callback=null):\Slim\Http\Response {
        $contentType=$contentType??$this->contentType;
        if($contentType!=='application/json' && $callback) {
            throw new \Exception('Callback can only be used with contentType application/json');
        }
        $method=$request->getMethod();
        $bodyParams=in_array($method,['PUT','POST'])?(array)$request->getParsedBody():[];   //Ignore body for GET and DELETE methods
        $queryParams=$request->getQueryParams();
        $data=array_merge($queryParams, $bodyParams);   ///Would be better to write slim's body to guzzle's body so that get parameters are preserved and not overriden by body parameters.
        $path=$request->getUri()->getPath();
        $contentTypeHeader=$request->getContentType();
        if(substr($contentTypeHeader, 0, 19)==='multipart/form-data'){
            syslog(LOG_INFO, 'contentType: '.$contentTypeHeader);
            $files = $request->getUploadedFiles();
            $multiparts=[];
            $errors=[];
            foreach($files as $name=>$file) {
                if ($error=$file->getError()) {
                    $errors[]=[
                        'name'=> $name,
                        'filename'=> $file->getClientFilename(),
                        'error' => $this->getFileErrorMessage($error)
                    ];
                }
                else {
                    $multiparts[]=[
                        'name'=> $name,
                        'filename'=> $file->getClientFilename(),
                        'contents' => $file->getStream(),
                        'headers'  => [
                            //Not needed, right? 'Size' => $file->getSize(),
                            'Content-Type' => $file->getClientMediaType()
                        ]
                    ];
                }
            }
            if($errors) return $response->withJson($errors, 422);
            $multiparts[]=[
                'name'=> 'data',
                'contents' => json_encode($data),
                'headers'  => ['Content-Type' => 'application/json']
            ];
            $options=['multipart' => $multiparts];
        }
        else {
            $options = in_array($method,['PUT','POST'])?['json'=>$data]:['query'=>$data];
        }
        try {
            $curlResponse = $this->httpClient->request($method, $path, $options);
        }
        catch (\GuzzleHttp\Exception\RequestException  $e) {
            //Errors only return JSON
            //Networking error which includes ConnectException and TooManyRedirectsException
            syslog(LOG_ERR, 'Proxy error: '.$e->getMessage());
            if ($e->hasResponse()) {
                $curlResponse=$e->getResponse();
                return $response->withJson(json_decode($curlResponse->getBody()), $curlResponse->getStatusCode());
            }
            else {
                return $response->withJson($e->getMessage(), $e->getMessage());
            }
        }

        $statusCode=$curlResponse->getStatusCode();
        switch($contentType) {
            case 'application/json':
                //Application and server error messages will be returned.  Consider hiding server errors.
                $content=json_decode($curlResponse->getBody());
                if($callback) {
                    $content=$callback($content, $statusCode);
                }
                return $response->withJson($content, $statusCode);
            case 'text/html':
            case 'text/plain':
                //Application and server error messages will be returned.  Consider hiding server errors.
                $response = $response->withStatus($statusCode);
                return $response->getBody()->write($curlResponse->getBody());
            case 'text/csv':
                foreach ($response->getHeaders() as $name => $values) {
                    syslog(LOG_INFO, "headers: $name: ". implode(', ', $values));
                }
                if($statusCode===200) {
                    return $response->withHeader('Content-Type', 'application/force-download')
                    ->withHeader('Content-Type', 'application/octet-stream')
                    ->withHeader('Content-Type', 'application/download')
                    ->withHeader('Content-Description', 'File Transfer')
                    ->withHeader('Content-Transfer-Encoding', 'binary')
                    ->withHeader('Content-Disposition', 'attachment; filename="data.csv"')
                    ->withHeader('Expires', '0')
                    ->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
                    ->withHeader('Pragma', 'public')
                    ->withBody($curlResponse->getBody());
                }
                else {
                    return $response->withJson(json_decode($curlResponse->getBody()), $statusCode);
                }
                break;
            default: throw new \Exception("Invalid proxy contentType: $contentType");
        }
    }

    private function getFileErrorMessage($code){
        switch ($code) {
            case UPLOAD_ERR_INI_SIZE:
                $message = "The uploaded file exceeds the upload_max_filesize directive in php.ini";
                break;
            case UPLOAD_ERR_FORM_SIZE:
                $message = "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form";
                break;
            case UPLOAD_ERR_PARTIAL:
                $message = "The uploaded file was only partially uploaded";
                break;
            case UPLOAD_ERR_NO_FILE:
                $message = "No file was uploaded";
                break;
            case UPLOAD_ERR_NO_TMP_DIR:
                $message = "Missing a temporary folder";
                break;
            case UPLOAD_ERR_CANT_WRITE:
                $message = "Failed to write file to disk";
                break;
            case UPLOAD_ERR_EXTENSION:
                $message = "File upload stopped by extension";
                break;

            default:
                $message = "Unknown upload error";
                break;
        }
        return $message;
    }

    public function callApi(\GuzzleHttp\Psr7\Request $request, array $data=[]):\GuzzleHttp\Psr7\Response {
        try {
            $response = $this->httpClient->send($request, $data);
        }
        catch (\GuzzleHttp\Exception\ClientException $e) {
            $response=$e->getResponse();
        } catch (\GuzzleHttp\Exception\RequestException  $e) {
            //Networking error which includes ConnectException and TooManyRedirectsException
            if ($e->hasResponse()) {
                $response=$e->getResponse();
            }
            else {
                $response=new \GuzzleHttp\Psr7\Response($e->getCode(), [], $e->getMessage());
            }
        } catch (\GuzzleHttp\Exception\ServerException $e) {
            //Consider not including all information back to client
            $response=$e->getResponse();
        }
        return $response;
    }
}

 

Link to comment
Share on other sites

  • 2 weeks later...

Turns out this is even easier than I thought. My configuration has two http servers on the same machine where one is mainwebsite.com and it makes curl requests to the other which is api.mainwebsite.com .  Now, I am just sending the Slim Request to Guzzle.

Originally, I sent Slim's Request with its original host (mainwebsite.com), and my $_SESSION variable kept on getting deleted.  I am assuming some sort of web browser or PHP security feature?  Any thoughts?  Why wasn't the base_uri added to \GuzzleHttp\Client's constructor not being applied?  Regardless,  I was able to change the host as shown below and all now works.

To use less code, I am using a loop to apply the headers to the response.  Think I should hard code it like $slimResponse->withHeader('header1', $guzzleResponse->getHeader('header1'))->addMoreHeaders()->thenAddStatusAndBody()?  This (I think) will eliminate needing to clone all the responses, however, will will require me to have code for each Content-Type to determine which headers are returned.  But maybe that is good and I shouldn't blacklist headers as I am doing but whitelist them?  If this approach is acceptable, what other headers should I blacklist?

Thanks

    public function proxy(\Slim\Http\Request $slimRequest, \Slim\Http\Response $slimResponse):\Slim\Http\Response {
        //Forwards Slim Request to another server and returns the updated Slim Response.
        $slimRequest=$slimRequest->withUri($slimRequest->getUri()->withHost($this->getHost(false)));  //Change slim's host to API server!
        try {
            $httpClient = new \GuzzleHttp\Client(['base_uri' => "https://api.mainwebsite.com"]); //Will use injection, and just shown this way ease
            $guzzleResponse=$httpClient->send($slimRequest);
            $excludedHeaders=['Date', 'Server', 'X-Powered-By', 'Access-Control-Allow-Origin', 'Access-Control-Allow-Methods', 'Access-Control-Allow-Headers'];
            $headerArrays=array_diff_key($guzzleResponse->getHeaders(), array_flip($excludedHeaders));
            foreach($headerArrays as $headerName=>$headers) {
                foreach($headers as $headerValue) {
                    $slimResponse=$slimResponse->withHeader($headerName, $headerValue);
                }
            }
            return $slimResponse->withStatus($guzzleResponse->getStatusCode())->withBody($guzzleResponse->getBody());
        }
        catch (\GuzzleHttp\Exception\RequestException  $e) {
            if ($e->hasResponse()) {
                $guzzleResponse=$e->getResponse();
                return $slimResponse->withStatus($guzzleResponse->getStatusCode())->withBody($guzzleResponse->getBody());
            }
            else {
                return $slimResponse->withStatus(500)->write(json_encode(['message'=>'RequestException without response: '.$e->getMessage()]));
            }
        }
    }

 

Link to comment
Share on other sites

Ran into some odd issues.  Things were working, but then all of a sudden the api server couldn't parse the content.  Turns out that Guzzle is no longer forwarding the Content-Type header in Slim's request, and I needed to re-apply the Content-Type header.  I have no idea what changed to require this as it could be at the browser client, web server/api client, or api server.  Any ideas?

if((string) $slimRequest->getBody()) $slimRequest=$slimRequest->withHeader('Content-Type', $slimRequest->getContentType());


 

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.