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()]));
            }
        }
    }

 

  • Like 1
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

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • 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.