├── .styleci.yml ├── stubs ├── httpRuntime.php ├── php-fpm.conf ├── runtime.php ├── runtime-with-vendor-download.php ├── 503.html ├── httpHandler.php ├── cliRuntime.php ├── octaneRuntime.php └── fpmRuntime.php ├── src ├── debug.php ├── Contracts │ ├── LambdaResponse.php │ ├── LambdaEventHandler.php │ └── SignedStorageUrlController.php ├── Runtime │ ├── Fpm │ │ ├── FpmRequest.php │ │ ├── FpmResponseHeaders.php │ │ ├── FpmResponse.php │ │ ├── FpmHttpHandlerFactory.php │ │ ├── FpmApplication.php │ │ ├── ActsAsFastCgiDataProvider.php │ │ └── Fpm.php │ ├── Handlers │ │ ├── WarmerPingHandler.php │ │ ├── LoadBalancedFpmHandler.php │ │ ├── LoadBalancedOctaneHandler.php │ │ ├── PayloadFormatVersion2FpmHandler.php │ │ ├── PayloadFormatVersion2OctaneHandler.php │ │ ├── UnknownEventHandler.php │ │ ├── FpmHandler.php │ │ ├── OctaneHandler.php │ │ ├── WarmerHandler.php │ │ ├── CliHandler.php │ │ └── QueueHandler.php │ ├── Http │ │ └── Middleware │ │ │ ├── EnsureVanityUrlIsNotIndexed.php │ │ │ ├── RedirectStaticAssets.php │ │ │ ├── EnsureBinaryEncoding.php │ │ │ └── EnsureOnNakedDomain.php │ ├── CliHandlerFactory.php │ ├── ArrayLambdaResponse.php │ ├── LambdaContainer.php │ ├── StorageDirectories.php │ ├── Response.php │ ├── Octane │ │ ├── OctaneHttpHandlerFactory.php │ │ ├── OctaneRequestContextFactory.php │ │ └── Octane.php │ ├── Logger.php │ ├── PayloadFormatVersion2LambdaResponse.php │ ├── LoadBalancedLambdaResponse.php │ ├── LambdaRuntime.php │ ├── Secrets.php │ ├── NotifiesLambda.php │ ├── LambdaInvocation.php │ ├── Environment.php │ ├── LambdaResponse.php │ ├── HttpKernel.php │ └── Request.php ├── VaporJobTimedOutException.php ├── DefinesRoutes.php ├── ConfiguresAssets.php ├── ConfiguresSqs.php ├── ConfiguresDynamoDb.php ├── Logging │ └── JsonFormatter.php ├── Queue │ ├── VaporWorker.php │ ├── VaporConnector.php │ ├── VaporQueue.php │ ├── VaporJob.php │ └── JobAttempts.php ├── Console │ └── Commands │ │ ├── OctaneStatusCommand.php │ │ ├── VaporHealthCheckCommand.php │ │ ├── WritesQueueEventMessages.php │ │ ├── VaporScheduleCommand.php │ │ ├── VaporQueueListFailedCommand.php │ │ └── VaporWorkCommand.php ├── Vapor.php ├── ConfiguresQueue.php ├── Http │ ├── Middleware │ │ └── ServeStaticAssets.php │ └── Controllers │ │ └── SignedStorageUrlController.php ├── ConfiguresRedis.php ├── Events │ └── LambdaEvent.php ├── Arr.php └── VaporServiceProvider.php ├── LICENSE.md ├── README.md ├── config └── vapor.php ├── composer.json └── CHANGELOG.md /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /stubs/httpRuntime.php: -------------------------------------------------------------------------------- 1 | 'Warmer ping handled.', 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Runtime/Handlers/LoadBalancedFpmHandler.php: -------------------------------------------------------------------------------- 1 | status, 19 | $response->headers, 20 | $response->body 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Runtime/Handlers/LoadBalancedOctaneHandler.php: -------------------------------------------------------------------------------- 1 | status, 19 | $response->headers, 20 | $response->body 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Runtime/Http/Middleware/EnsureVanityUrlIsNotIndexed.php: -------------------------------------------------------------------------------- 1 | getHttpHost() === $_ENV['APP_VANITY_URL']) { 19 | $response->headers->set('X-Robots-Tag', 'noindex, nofollow', true); 20 | } 21 | 22 | return $response; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Runtime/Fpm/FpmResponseHeaders.php: -------------------------------------------------------------------------------- 1 | job ?? null; 21 | 22 | return $messageId && $job 23 | ? new QueueHandler 24 | : new CliHandler; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/DefinesRoutes.php: -------------------------------------------------------------------------------- 1 | app->routesAreCached()) { 17 | return; 18 | } 19 | 20 | if (config('vapor.signed_storage.enabled', true)) { 21 | Route::post( 22 | config('vapor.signed_storage.url', '/vapor/signed-storage-url'), 23 | Contracts\SignedStorageUrlController::class.'@store' 24 | )->middleware(config('vapor.middleware', 'web')); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ConfiguresAssets.php: -------------------------------------------------------------------------------- 1 | status, 20 | $response->headers, 21 | $response->body 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Runtime/Handlers/PayloadFormatVersion2OctaneHandler.php: -------------------------------------------------------------------------------- 1 | status, 20 | $response->headers, 21 | $response->body 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Runtime/Handlers/UnknownEventHandler.php: -------------------------------------------------------------------------------- 1 | $event, 21 | ]); 22 | 23 | return new ArrayLambdaResponse([ 24 | 'output' => 'Unknown event type.', 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Runtime/ArrayLambdaResponse.php: -------------------------------------------------------------------------------- 1 | response = $response; 25 | } 26 | 27 | /** 28 | * Convert the response to API Gateway's supported format. 29 | * 30 | * @return array 31 | */ 32 | public function toApiGatewayFormat() 33 | { 34 | return $this->response; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ConfiguresSqs.php: -------------------------------------------------------------------------------- 1 | 'sqs', 23 | 'key' => $_ENV['AWS_ACCESS_KEY_ID'] ?? null, 24 | 'secret' => $_ENV['AWS_SECRET_ACCESS_KEY'] ?? null, 25 | 'prefix' => $_ENV['SQS_PREFIX'] ?? null, 26 | 'queue' => $_ENV['SQS_QUEUE'] ?? 'default', 27 | 'region' => $_ENV['AWS_DEFAULT_REGION'] ?? 'us-east-1', 28 | ], Config::get('queue.connections.sqs') ?? [])); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ConfiguresDynamoDb.php: -------------------------------------------------------------------------------- 1 | 'dynamodb', 23 | 'key' => $_ENV['AWS_ACCESS_KEY_ID'] ?? null, 24 | 'secret' => $_ENV['AWS_SECRET_ACCESS_KEY'] ?? null, 25 | 'region' => $_ENV['AWS_DEFAULT_REGION'] ?? 'us-east-1', 26 | 'table' => $_ENV['DYNAMODB_CACHE_TABLE'] ?? 'cache', 27 | 'endpoint' => $_ENV['DYNAMODB_ENDPOINT'] ?? null, 28 | ], Config::get('cache.stores.dynamodb') ?? [])); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Runtime/LambdaContainer.php: -------------------------------------------------------------------------------- 1 | = $invocationLimit) { 23 | if (interface_exists(\Laravel\Octane\Contracts\Client::class)) { 24 | Octane::terminate(); 25 | } 26 | 27 | function_exists('__vapor_debug') && __vapor_debug('Killing container. Container has processed '.$invocationLimit.' invocations. ('.$_ENV['AWS_REQUEST_ID'].')'); 28 | 29 | exit(0); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Runtime/StorageDirectories.php: -------------------------------------------------------------------------------- 1 | ($_ENV['AWS_REQUEST_ID'] ?? null)]; 16 | 17 | if ($record instanceof LogRecord) { 18 | $record = new LogRecord( 19 | $record->datetime, 20 | $record->channel, 21 | $record->level, 22 | $record->message, 23 | array_merge($record->context, $context), 24 | $record->extra, 25 | $record->formatted 26 | ); 27 | } else { 28 | $record['context'] = array_merge( 29 | $record['context'] ?? [], $context 30 | ); 31 | } 32 | 33 | return parent::format($record); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Runtime/Http/Middleware/RedirectStaticAssets.php: -------------------------------------------------------------------------------- 1 | path() === 'favicon.ico') { 19 | return new RedirectResponse($_ENV['ASSET_URL'].'/favicon.ico', 302, [ 20 | 'Cache-Control' => 'public, max-age=3600', 21 | ]); 22 | } 23 | 24 | if (config('vapor.redirect_robots_txt') && $request->path() === 'robots.txt') { 25 | return new RedirectResponse($_ENV['ASSET_URL'].'/robots.txt', 302, [ 26 | 'Cache-Control' => 'public, max-age=3600', 27 | ]); 28 | } 29 | 30 | return $next($request); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /stubs/runtime.php: -------------------------------------------------------------------------------- 1 | forgetScopedInstances(); 22 | 23 | pcntl_async_signals(true); 24 | 25 | pcntl_signal(SIGALRM, function () use ($job) { 26 | throw new VaporJobTimedOutException($job->resolveName()); 27 | }); 28 | 29 | pcntl_alarm( 30 | max($this->timeoutForJob($job, $options), 0) 31 | ); 32 | 33 | app(JobAttempts::class)->increment($job); 34 | 35 | $this->runJob($job, $connectionName, $options); 36 | 37 | pcntl_alarm(0); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Console/Commands/OctaneStatusCommand.php: -------------------------------------------------------------------------------- 1 | isEnvironmentRunningOnOctane() 31 | ? $this->info('Octane server is running.') 32 | : $this->info('Octane server is not running.'); 33 | } 34 | 35 | /** 36 | * Determine if the environment is running on Octane. 37 | * 38 | * @return bool 39 | */ 40 | protected function isEnvironmentRunningOnOctane() 41 | { 42 | return isset($_ENV['OCTANE_DATABASE_SESSION_TTL']); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Runtime/Fpm/FpmResponse.php: -------------------------------------------------------------------------------- 1 | getOutput()); 19 | 20 | parent::__construct( 21 | $response->getBody(), 22 | $headers, 23 | $this->prepareStatus($headers) 24 | ); 25 | } 26 | 27 | /** 28 | * Prepare the status code of the response. 29 | * 30 | * @return int 31 | */ 32 | protected function prepareStatus(array $headers) 33 | { 34 | $headers = array_change_key_case($headers, CASE_LOWER); 35 | 36 | return isset($headers['status'][0]) 37 | ? (int) explode(' ', $headers['status'][0])[0] 38 | : 200; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Runtime/Response.php: -------------------------------------------------------------------------------- 1 | body = $body; 39 | $this->status = $status; 40 | 41 | $this->headers = $this->prepareHeaders($headers); 42 | } 43 | 44 | /** 45 | * Prepare the given response headers. 46 | * 47 | * @param array $headers 48 | * @return array 49 | */ 50 | protected function prepareHeaders(array $headers) 51 | { 52 | $headers = array_change_key_case($headers, CASE_LOWER); 53 | 54 | unset($headers['status']); 55 | 56 | return $headers; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Vapor.php: -------------------------------------------------------------------------------- 1 | headers->set('X-Vapor-Base64-Encode', 'True'); 23 | } 24 | 25 | return $response; 26 | } 27 | 28 | /** 29 | * Determine if base64 encoding is required for the response. 30 | * 31 | * @param \Illuminate\Http\Response $response 32 | * @return bool 33 | */ 34 | public static function isBase64EncodingRequired(Response $response): bool 35 | { 36 | $contentType = strtolower($response->headers->get('Content-Type', 'text/html')); 37 | 38 | if (Str::startsWith($contentType, 'text/') || 39 | Str::contains($contentType, ['xml', 'json'])) { 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Vapor Core / Runtime 2 | 3 |

4 | Build Status 5 | Total Downloads 6 | Latest Stable Version 7 | License 8 |

9 | 10 | [Laravel Vapor](https://vapor.laravel.com) is an auto-scaling, serverless deployment platform for Laravel, powered by AWS Lambda. Manage your Laravel infrastructure on Vapor and fall in love with the scalability and simplicity of serverless. 11 | 12 | Vapor abstracts the complexity of managing Laravel applications on AWS Lambda, as well as interfacing those applications with SQS queues, databases, Redis clusters, networks, CloudFront CDN, and more. 13 | 14 | This repository contains the core service providers and runtime client used to make Laravel applications run smoothly in a serverless environment. To learn more about Vapor and how to use this repository, please consult the [official documentation](https://docs.vapor.build). 15 | -------------------------------------------------------------------------------- /src/Runtime/Fpm/FpmHttpHandlerFactory.php: -------------------------------------------------------------------------------- 1 | client = $client; 34 | $this->socketConnection = $socketConnection; 35 | } 36 | 37 | /** 38 | * Handle the given FPM request. 39 | * 40 | * @param \Laravel\Vapor\Runtime\Fpm\FpmRequest $request 41 | * @return \Laravel\Vapor\Runtime\Fpm\FpmResponse 42 | */ 43 | public function handle(FpmRequest $request) 44 | { 45 | return new FpmResponse( 46 | $this->client->sendRequest($this->socketConnection, $request) 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Runtime/Octane/OctaneHttpHandlerFactory.php: -------------------------------------------------------------------------------- 1 | app->bound('queue.vaporWorker')) { 21 | return; 22 | } 23 | 24 | $this->app->singleton('queue.vaporWorker', function () { 25 | $isDownForMaintenance = function () { 26 | return $this->app->isDownForMaintenance(); 27 | }; 28 | 29 | return new VaporWorker( 30 | $this->app['queue'], 31 | $this->app['events'], 32 | $this->app[ExceptionHandler::class], 33 | $isDownForMaintenance 34 | ); 35 | }); 36 | 37 | $this->app->singleton(JobAttempts::class, function () { 38 | return new JobAttempts( 39 | isset($_ENV['VAPOR_CACHE_JOB_ATTEMPTS']) && $_ENV['VAPOR_CACHE_JOB_ATTEMPTS'] === 'true' 40 | ? $this->app['cache']->driver() 41 | : new Repository(new NullStore()) 42 | ); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Queue/VaporConnector.php: -------------------------------------------------------------------------------- 1 | getDefaultConfiguration($config); 20 | 21 | if ($config['key'] && $config['secret']) { 22 | $config['credentials'] = Arr::only($config, ['key', 'secret', 'token']); 23 | } 24 | 25 | return new VaporQueue( 26 | new SqsClient($config), 27 | $config['queue'], 28 | $config['prefix'] ?? '', 29 | $config['suffix'] ?? '', 30 | $config['after_commit'] ?? null 31 | ); 32 | } 33 | 34 | /** 35 | * Get the default configuration for SQS. 36 | * 37 | * @param array $config 38 | * @return array 39 | */ 40 | protected function getDefaultConfiguration(array $config) 41 | { 42 | return array_merge([ 43 | 'version' => 'latest', 44 | 'http' => [ 45 | 'timeout' => 60, 46 | 'connect_timeout' => 60, 47 | ], 48 | ], $config); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Http/Middleware/ServeStaticAssets.php: -------------------------------------------------------------------------------- 1 | getStatusCode() === 404) { 24 | $requestUri = $request->getRequestUri(); 25 | 26 | if (! in_array(ltrim($requestUri, '/'), config('vapor.serve_assets', []))) { 27 | return $response; 28 | } 29 | 30 | $asset = null; 31 | 32 | try { 33 | $asset = (new Client)->get(asset($requestUri)); 34 | } catch (ClientException $e) { 35 | report($e); 36 | } 37 | 38 | if ($asset && $asset->getStatusCode() === 200) { 39 | $headers = collect($asset->getHeaders()) 40 | ->only(['Content-Length', 'Content-Type']) 41 | ->all(); 42 | 43 | return response($asset->getBody())->withHeaders($headers); 44 | } 45 | } 46 | 47 | return $response; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Queue/VaporQueue.php: -------------------------------------------------------------------------------- 1 | sqs->receiveMessage([ 18 | 'QueueUrl' => $queue = $this->getQueue($queue), 19 | 'AttributeNames' => ['ApproximateReceiveCount'], 20 | ]); 21 | 22 | if (! is_null($response['Messages']) && count($response['Messages']) > 0) { 23 | return tap(new VaporJob( 24 | $this->container, $this->sqs, $response['Messages'][0], 25 | $this->connectionName, $queue 26 | ), function ($job) { 27 | $this->container 28 | ->make(JobAttempts::class) 29 | ->increment($job); 30 | }); 31 | } 32 | } 33 | 34 | /** 35 | * Create a payload string from the given job and data. 36 | * 37 | * @param string $job 38 | * @param string $queue 39 | * @param mixed $data 40 | * @return array 41 | */ 42 | protected function createPayloadArray($job, $queue, $data = '') 43 | { 44 | return array_merge(parent::createPayloadArray($job, $queue, $data), [ 45 | 'attempts' => 0, 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Runtime/Http/Middleware/EnsureOnNakedDomain.php: -------------------------------------------------------------------------------- 1 | getHttpHost() === $_ENV['APP_VANITY_URL']) { 20 | return $next($request); 21 | } 22 | 23 | if (config('vapor.redirect_to_root') === true && 24 | strpos($request->getHost(), 'www.') === 0) { 25 | return new RedirectResponse(Str::replaceFirst( 26 | 'www.', '', $request->fullUrl() 27 | ), 301); 28 | } 29 | 30 | if (config('vapor.redirect_to_root') === false) { 31 | $url = parse_url(config('app.url')); 32 | 33 | $nakedHost = preg_replace('#^www\.(.+\.)#i', '$1', $url[ 34 | 'host' 35 | ]); 36 | 37 | if ($request->getHost() === $nakedHost) { 38 | return new RedirectResponse(str_replace( 39 | $request->getScheme().'://', 40 | $request->getScheme().'://www.', 41 | $request->fullUrl() 42 | ), 301); 43 | } 44 | } 45 | 46 | return $next($request); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Runtime/Logger.php: -------------------------------------------------------------------------------- 1 | info($message, $context); 30 | } 31 | 32 | /** 33 | * Write error information to the log. 34 | * 35 | * @param string $message 36 | * @param array $context 37 | * @return void 38 | */ 39 | public static function error($message, array $context = []) 40 | { 41 | static::ensureLoggerIsAvailable(); 42 | 43 | static::$logger->error($message, $context); 44 | } 45 | 46 | /** 47 | * Ensure the logger has been instantiated. 48 | * 49 | * @return void 50 | */ 51 | protected static function ensureLoggerIsAvailable() 52 | { 53 | if (isset(static::$logger)) { 54 | return; 55 | } 56 | 57 | static::$logger = new MonologLogger('vapor', [ 58 | (new StreamHandler('php://stderr'))->setFormatter(new JsonFormatter), 59 | ]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /config/vapor.php: -------------------------------------------------------------------------------- 1 | true, 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Redirect robots.txt 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When this option is enabled, Vapor will redirect requests for your 24 | | application's "robots.txt" file to the location of the S3 asset 25 | | bucket or CloudFront's asset URL instead of serving directly. 26 | | 27 | */ 28 | 29 | 'redirect_robots_txt' => true, 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Servable Assets 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Here you can configure list of public assets that should be servable 37 | | from your application's domain instead of only being servable via 38 | | the public S3 "asset" bucket or the AWS CloudFront CDN network. 39 | | 40 | */ 41 | 42 | 'serve_assets' => [ 43 | // 44 | ], 45 | 46 | ]; 47 | -------------------------------------------------------------------------------- /src/Queue/VaporJob.php: -------------------------------------------------------------------------------- 1 | payload()['attempts'] ?? 0) + 1, 18 | $this->container->make(JobAttempts::class)->get($this) 19 | ); 20 | } 21 | 22 | /** 23 | * Delete the job from the queue. 24 | * 25 | * @return void 26 | */ 27 | public function delete() 28 | { 29 | parent::delete(); 30 | 31 | $this->container 32 | ->make(JobAttempts::class) 33 | ->forget($this); 34 | } 35 | 36 | /** 37 | * Release the job back into the queue. 38 | * 39 | * @param int $delay 40 | * @return void 41 | */ 42 | public function release($delay = 0) 43 | { 44 | $this->released = true; 45 | 46 | $payload = $this->payload(); 47 | 48 | $payload['attempts'] = $this->attempts(); 49 | 50 | $this->sqs->deleteMessage([ 51 | 'QueueUrl' => $this->queue, 52 | 'ReceiptHandle' => $this->job['ReceiptHandle'], 53 | ]); 54 | 55 | $jobId = $this->sqs->sendMessage([ 56 | 'QueueUrl' => $this->queue, 57 | 'MessageBody' => json_encode($payload), 58 | 'DelaySeconds' => $this->secondsUntil($delay), 59 | ])->get('MessageId'); 60 | 61 | $this->container 62 | ->make(JobAttempts::class) 63 | ->transfer($this, $jobId); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /stubs/runtime-with-vendor-download.php: -------------------------------------------------------------------------------- 1 | response( 20 | Fpm::resolve()->handle($this->request($event)) 21 | ); 22 | } 23 | 24 | /** 25 | * Create a new fpm request from the incoming event. 26 | * 27 | * @param array $event 28 | * @return \Laravel\Vapor\Runtime\Fpm\FpmRequest 29 | */ 30 | public function request($event) 31 | { 32 | return FpmRequest::fromLambdaEvent( 33 | $event, $this->serverVariables(), Fpm::resolve()->handler() 34 | ); 35 | } 36 | 37 | /** 38 | * Covert a response to Lambda-ready response. 39 | * 40 | * @param \Laravel\Vapor\Runtime\Response $response 41 | * @return \Laravel\Vapor\Runtime\LambdaResponse 42 | */ 43 | public function response($response) 44 | { 45 | return new LambdaResponse( 46 | $response->status, 47 | $response->headers, 48 | $response->body 49 | ); 50 | } 51 | 52 | /** 53 | * Get the server variables. 54 | * 55 | * @return array 56 | */ 57 | public function serverVariables() 58 | { 59 | return array_merge(Fpm::resolve()->serverVariables(), array_filter([ 60 | 'AWS_REQUEST_ID' => $_ENV['AWS_REQUEST_ID'] ?? null, 61 | ])); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Runtime/PayloadFormatVersion2LambdaResponse.php: -------------------------------------------------------------------------------- 1 | headers['x-vapor-base64-encode'][0]); 18 | 19 | return [ 20 | 'isBase64Encoded' => $requiresEncoding, 21 | 'statusCode' => $this->status, 22 | 'cookies' => isset($this->headers['set-cookie']) ? $this->headers['set-cookie'] : [], 23 | 'headers' => empty($this->headers) ? new stdClass : $this->prepareHeaders($this->headers), 24 | 'body' => $requiresEncoding ? base64_encode($this->body) : $this->body, 25 | ]; 26 | } 27 | 28 | /** 29 | * Prepare the given response headers for API Gateway. 30 | * 31 | * @param array $responseHeaders 32 | * @return array 33 | */ 34 | protected function prepareHeaders(array $responseHeaders) 35 | { 36 | $headers = []; 37 | 38 | foreach ($responseHeaders as $name => $values) { 39 | $name = $this->normalizeHeaderName($name); 40 | 41 | if ($name == 'Set-Cookie') { 42 | continue; 43 | } 44 | 45 | foreach ($values as $value) { 46 | $headers[$name] = $value; 47 | } 48 | } 49 | 50 | if (! isset($headers['Content-Type'])) { 51 | $headers['Content-Type'] = 'text/html'; 52 | } 53 | 54 | return $headers; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /stubs/503.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Service Unavailable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 50 | 51 | 52 |
53 |
54 | 503 55 |
56 | 57 |
58 | Service Unavailable 59 |
60 |
61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/ConfiguresRedis.php: -------------------------------------------------------------------------------- 1 | $_ENV['REDIS_CLIENT'] ?? 'phpredis', 23 | 'options' => array_merge( 24 | Config::get('database.redis.options', []), 25 | array_filter([ 26 | 'cluster' => $_ENV['REDIS_CLUSTER'] ?? 'redis', 27 | 'scheme' => $_ENV['REDIS_SCHEME'] ?? null, 28 | 'context' => array_filter(['cafile' => $_ENV['REDIS_SSL_CA'] ?? null]), 29 | ]) 30 | ), 31 | 'clusters' => array_merge(Config::get('database.redis.clusters', []), [ 32 | 'default' => [ 33 | [ 34 | 'host' => $_ENV['REDIS_HOST'] ?? '127.0.0.1', 35 | 'password' => null, 36 | 'port' => 6379, 37 | 'database' => 0, 38 | ], 39 | ], 40 | 'cache' => [ 41 | [ 42 | 'host' => $_ENV['REDIS_HOST'] ?? '127.0.0.1', 43 | 'password' => null, 44 | 'port' => 6379, 45 | 'database' => 0, 46 | ], 47 | ], 48 | ]), 49 | ])); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Runtime/Handlers/OctaneHandler.php: -------------------------------------------------------------------------------- 1 | request($event); 24 | 25 | return $this->response( 26 | Octane::handle($request) 27 | ); 28 | } 29 | 30 | /** 31 | * Create a new Octane request from the incoming event. 32 | * 33 | * @param array $event 34 | * @return \Laravel\Octane\RequestContext 35 | */ 36 | protected function request($event) 37 | { 38 | return OctaneRequestContextFactory::fromEvent($event, $this->serverVariables()); 39 | } 40 | 41 | /** 42 | * Covert a response to Lambda-ready response. 43 | * 44 | * @param \Laravel\Vapor\Runtime\Response $response 45 | * @return \Laravel\Vapor\Runtime\LambdaResponse 46 | */ 47 | protected function response($response) 48 | { 49 | return new LambdaResponse( 50 | $response->status, 51 | $response->headers, 52 | $response->body 53 | ); 54 | } 55 | 56 | /** 57 | * Get the server variables. 58 | * 59 | * @return array 60 | */ 61 | public function serverVariables() 62 | { 63 | return [ 64 | 'AWS_REQUEST_ID' => $_ENV['AWS_REQUEST_ID'] ?? null, 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /stubs/httpHandler.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | define('LARAVEL_START', microtime(true)); 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Register The Auto Loader 16 | |-------------------------------------------------------------------------- 17 | | 18 | | Composer provides a convenient, automatically generated class loader for 19 | | our application. We just need to utilize it! We'll simply require it 20 | | into the script here so that we don't have to worry about manual 21 | | loading any of our classes later on. It feels great to relax. 22 | | 23 | */ 24 | 25 | require __DIR__.'/vendor/autoload.php'; 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Turn On The Lights 30 | |-------------------------------------------------------------------------- 31 | | 32 | | We need to illuminate PHP development, so let us turn on the lights. 33 | | This bootstraps the framework and gets it ready for use, then it 34 | | will load up this application so that we can run it and send 35 | | the responses back to the browser and delight our users. 36 | | 37 | */ 38 | 39 | $app = require_once __DIR__.'/bootstrap/app.php'; 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Run The Application 44 | |-------------------------------------------------------------------------- 45 | | 46 | | Once we have the application, we can handle the incoming request 47 | | through the kernel, and send the associated response back to 48 | | the client's browser allowing them to enjoy the creative 49 | | and wonderful application we have prepared for them. 50 | | 51 | */ 52 | 53 | $handler = new HttpKernel($app); 54 | 55 | $response = $handler->handle(Request::capture()); 56 | 57 | $response->send(); 58 | -------------------------------------------------------------------------------- /src/Events/LambdaEvent.php: -------------------------------------------------------------------------------- 1 | event = $event; 27 | } 28 | 29 | /** 30 | * Determine if an item exists at an offset. 31 | * 32 | * @param string $key 33 | * @return bool 34 | */ 35 | #[\ReturnTypeWillChange] 36 | public function offsetExists($key) 37 | { 38 | return Arr::exists($this->event, $key); 39 | } 40 | 41 | /** 42 | * Get an item at a given offset. 43 | * 44 | * @param string $key 45 | * @return array|string|int 46 | */ 47 | #[\ReturnTypeWillChange] 48 | public function offsetGet($key) 49 | { 50 | return Arr::get($this->event, $key); 51 | } 52 | 53 | /** 54 | * Set the item at a given offset. 55 | * 56 | * @param string $key 57 | * @param array|string|int $value 58 | * @return void 59 | */ 60 | #[\ReturnTypeWillChange] 61 | public function offsetSet($key, $value) 62 | { 63 | Arr::set($this->event, $key, $value); 64 | } 65 | 66 | /** 67 | * Unset the item at a given offset. 68 | * 69 | * @param string $key 70 | * @return void 71 | */ 72 | #[\ReturnTypeWillChange] 73 | public function offsetUnset($key) 74 | { 75 | Arr::forget($this->event, $key); 76 | } 77 | 78 | /** 79 | * Get the instance as an array. 80 | * 81 | * @return array 82 | */ 83 | public function toArray() 84 | { 85 | return $this->event; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Console/Commands/VaporHealthCheckCommand.php: -------------------------------------------------------------------------------- 1 | ensureBaseConfigurationFilesWereHarmonized(); 41 | 42 | $this->ensureCacheIsWorking(); 43 | 44 | return $this->info('Health check complete!'); 45 | } 46 | 47 | /** 48 | * Ensure the configuration files were harmonized. 49 | * 50 | * @return void 51 | */ 52 | protected function ensureBaseConfigurationFilesWereHarmonized() 53 | { 54 | if (! file_exists($filename = __DIR__.'/../../../../framework/config/cache.php')) { 55 | return; 56 | } 57 | 58 | $configuration = file_get_contents($filename); 59 | 60 | if (! Str::contains($configuration, "'key' => env('NULL_AWS_ACCESS_KEY_ID')")) { 61 | throw new Exception( 62 | 'Laravel 11 or later requires the latest version of Vapor CLI.' 63 | ); 64 | } 65 | } 66 | 67 | /** 68 | * Ensure cache calls are working as expected. 69 | * 70 | * @return void 71 | */ 72 | protected function ensureCacheIsWorking() 73 | { 74 | Cache::get('vapor-health-check'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Console/Commands/WritesQueueEventMessages.php: -------------------------------------------------------------------------------- 1 | laravel['events']->listen(JobProcessing::class, function ($event) { 20 | $this->writeOutput($event->job, 'starting'); 21 | }); 22 | 23 | $this->laravel['events']->listen(JobProcessed::class, function ($event) { 24 | $this->writeOutput($event->job, 'success'); 25 | }); 26 | 27 | $this->laravel['events']->listen(JobFailed::class, function ($event) { 28 | $this->writeOutput($event->job, 'failed'); 29 | 30 | $this->logFailedJob($event); 31 | }); 32 | } 33 | 34 | /** 35 | * Write the status output for the queue worker. 36 | * 37 | * @param \Illuminate\Contracts\Queue\Job $job 38 | * @param string $status 39 | * @return void 40 | */ 41 | protected function writeOutput(Job $job, $status) 42 | { 43 | switch ($status) { 44 | case 'starting': 45 | return $this->writeStatus($job, 'Processing', 'comment'); 46 | case 'success': 47 | return $this->writeStatus($job, 'Processed', 'info'); 48 | case 'failed': 49 | return $this->writeStatus($job, 'Failed', 'error'); 50 | } 51 | } 52 | 53 | /** 54 | * Format the status output for the queue worker. 55 | * 56 | * @param \Illuminate\Contracts\Queue\Job $job 57 | * @param string $status 58 | * @param string $type 59 | * @return void 60 | */ 61 | protected function writeStatus(Job $job, $status, $type) 62 | { 63 | $this->output->writeln(sprintf( 64 | "<{$type}>%s %s", 65 | str_pad("{$status}:", 11), $job->resolveName() 66 | )); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Runtime/Handlers/WarmerHandler.php: -------------------------------------------------------------------------------- 1 | buildPromises($this->lambdaClient(), $event) 26 | )->wait(); 27 | } catch (Throwable $e) { 28 | Logger::error($e->getMessage(), ['exception' => $e]); 29 | } 30 | 31 | return new ArrayLambdaResponse([ 32 | 'output' => 'Warmer event handled.', 33 | ]); 34 | } 35 | 36 | /** 37 | * Build the array of warmer invocation promises. 38 | * 39 | * @return array 40 | */ 41 | protected function buildPromises(LambdaClient $lambda, array $event) 42 | { 43 | return collect(range(1, $event['concurrency'] - 1)) 44 | ->mapWithKeys(function ($i) use ($lambda, $event) { 45 | return ['warmer-'.$i => $lambda->invokeAsync([ 46 | 'FunctionName' => $event['functionName'], 47 | 'Qualifier' => $event['functionAlias'], 48 | 'LogType' => 'None', 49 | 'Payload' => json_encode(['vaporWarmerPing' => true]), 50 | ])]; 51 | })->all(); 52 | } 53 | 54 | /** 55 | * Get the Lambda client instance. 56 | * 57 | * @return \Aws\Lambda\LambdaClient 58 | */ 59 | protected function lambdaClient() 60 | { 61 | return new LambdaClient([ 62 | 'region' => $_ENV['AWS_DEFAULT_REGION'], 63 | 'version' => 'latest', 64 | 'http' => [ 65 | 'timeout' => 5, 66 | 'connect_timeout' => 5, 67 | ], 68 | ]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Runtime/LoadBalancedLambdaResponse.php: -------------------------------------------------------------------------------- 1 | headers['x-vapor-base64-encode'][0]); 17 | 18 | return [ 19 | 'isBase64Encoded' => $requiresEncoding, 20 | 'statusCode' => $this->status, 21 | 'statusDescription' => $this->status.' '.$this->statusText($this->status), 22 | 'multiValueHeaders' => empty($this->headers) ? [] : $this->prepareHeaders($this->headers), 23 | 'body' => $requiresEncoding ? base64_encode($this->body) : $this->body, 24 | ]; 25 | } 26 | 27 | /** 28 | * Get the status text for the given status code. 29 | * 30 | * @param int $status 31 | * @return string 32 | */ 33 | public function statusText($status) 34 | { 35 | $statusTexts = SymfonyResponse::$statusTexts; 36 | 37 | $statusTexts[419] = 'Authentication Timeout'; 38 | 39 | return $statusTexts[$status]; 40 | } 41 | 42 | /** 43 | * Prepare the given response headers. 44 | * 45 | * @param array $responseHeaders 46 | * @return array 47 | */ 48 | protected function prepareHeaders(array $responseHeaders) 49 | { 50 | $headers = []; 51 | 52 | foreach ($responseHeaders as $name => $values) { 53 | $headers[static::normalizeHeaderName($name)] = static::normalizeHeaderValues($values); 54 | } 55 | 56 | if (! isset($headers['Content-Type']) || empty($headers['Content-Type'])) { 57 | $headers['Content-Type'] = ['text/html']; 58 | } 59 | 60 | return $headers; 61 | } 62 | 63 | /** 64 | * Normalize the given header values into strings. 65 | * 66 | * @param array $values 67 | * @return array 68 | */ 69 | protected function normalizeHeaderValues($values) 70 | { 71 | return array_map(function ($value) { 72 | return (string) $value; 73 | }, $values); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Runtime/LambdaRuntime.php: -------------------------------------------------------------------------------- 1 | apiUrl = $apiUrl; 28 | } 29 | 30 | /** 31 | * Create new Lambda runtime from the API environment variable. 32 | * 33 | * @return static 34 | */ 35 | public static function fromEnvironmentVariable() 36 | { 37 | return new static(getenv('AWS_LAMBDA_RUNTIME_API')); 38 | } 39 | 40 | /** 41 | * Handle the next Lambda invocation. 42 | * 43 | * @return void 44 | */ 45 | public function nextInvocation(callable $callback) 46 | { 47 | [$invocationId, $event] = LambdaInvocation::next($this->apiUrl); 48 | 49 | $_ENV['AWS_REQUEST_ID'] = $invocationId; 50 | 51 | try { 52 | $this->notifyLambdaOfResponse($invocationId, $callback($invocationId, $event)); 53 | } catch (Throwable $error) { 54 | $this->handleException($invocationId, $error); 55 | 56 | exit(1); 57 | } 58 | } 59 | 60 | /** 61 | * Inform Lambda of an invocation failure. 62 | * 63 | * @return void 64 | */ 65 | public function handleException(string $invocationId, Throwable $error) 66 | { 67 | $errorMessage = $error instanceof Exception 68 | ? 'Uncaught '.get_class($error).': '.$error->getMessage() 69 | : $error->getMessage(); 70 | 71 | function_exists('__vapor_debug') && __vapor_debug(sprintf( 72 | "Fatal error: %s in %s:%d\nStack trace:\n%s", 73 | $errorMessage, 74 | $error->getFile(), 75 | $error->getLine(), 76 | $error->getTraceAsString() 77 | )); 78 | 79 | $this->notifyLambdaOfError($invocationId, [ 80 | 'errorMessage' => $error->getMessage(), 81 | 'errorType' => get_class($error), 82 | 'stackTrace' => explode(PHP_EOL, $error->getTraceAsString()), 83 | ]); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/vapor-core", 3 | "description": "The kernel and invocation handlers for Laravel Vapor", 4 | "keywords": [ 5 | "laravel", 6 | "vapor" 7 | ], 8 | "homepage": "https://github.com/laravel/vapor-core", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Taylor Otwell", 13 | "email": "taylor@laravel.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^7.2|^8.0", 18 | "aws/aws-sdk-php": "^3.80", 19 | "guzzlehttp/promises": "^1.4|^2.0", 20 | "guzzlehttp/guzzle": "^6.3|^7.0", 21 | "hollodotme/fast-cgi-client": "^3.0", 22 | "illuminate/container": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 23 | "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 24 | "illuminate/queue": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 25 | "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 26 | "monolog/monolog": "^1.12|^2.0|^3.2", 27 | "nyholm/psr7": "^1.0", 28 | "riverline/multipart-parser": "^2.0.9", 29 | "symfony/process": "^4.3|^5.0|^6.0|^7.0", 30 | "symfony/psr-http-message-bridge": "^1.0|^2.0|^6.4|^7.0" 31 | }, 32 | "require-dev": { 33 | "laravel/octane": "*", 34 | "mockery/mockery": "^1.2", 35 | "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", 36 | "phpstan/phpstan": "^1.10|^2.1", 37 | "phpunit/phpunit": "^8.0|^9.0|^10.4|^11.5.3" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "Laravel\\Vapor\\": "src" 42 | }, 43 | "files": [ 44 | "src/debug.php" 45 | ] 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "Laravel\\Vapor\\Tests\\": "tests" 50 | } 51 | }, 52 | "scripts": { 53 | "test": "vendor/bin/phpunit" 54 | }, 55 | "extra": { 56 | "branch-alias": { 57 | "dev-master": "2.0-dev" 58 | }, 59 | "laravel": { 60 | "providers": [ 61 | "Laravel\\Vapor\\VaporServiceProvider" 62 | ], 63 | "aliases": { 64 | "Vapor": "Laravel\\Vapor\\Vapor" 65 | } 66 | } 67 | }, 68 | "config": { 69 | "sort-packages": true 70 | }, 71 | "minimum-stability": "dev", 72 | "prefer-stable": true 73 | } 74 | -------------------------------------------------------------------------------- /src/Runtime/Secrets.php: -------------------------------------------------------------------------------- 1 | $value) { 25 | $_ENV[$key] = $value; 26 | $_SERVER[$key] = $value; 27 | } 28 | }); 29 | } 30 | 31 | /** 32 | * Get all of the secret parameters (AWS SSM) at the given path. 33 | * 34 | * @param string $path 35 | * @return array 36 | */ 37 | public static function all($path, array $parameters = []) 38 | { 39 | if (empty($parameters)) { 40 | return []; 41 | } 42 | 43 | $ssm = SsmClient::factory([ 44 | 'region' => $_ENV['AWS_DEFAULT_REGION'], 45 | 'version' => 'latest', 46 | ]); 47 | 48 | return collect($parameters)->chunk(10)->reduce(function ($carry, $parameters) use ($ssm, $path) { 49 | $ssmResponse = $ssm->getParameters([ 50 | 'Names' => collect($parameters)->map(function ($version, $parameter) use ($path) { 51 | return $path.'/'.$parameter.':'.$version; 52 | })->values()->all(), 53 | 'WithDecryption' => true, 54 | ]); 55 | 56 | return array_merge($carry, static::parseSecrets( 57 | $ssmResponse['Parameters'] ?? [] 58 | )); 59 | }, []); 60 | } 61 | 62 | /** 63 | * Parse the secret names and values into an array. 64 | * 65 | * @return array 66 | */ 67 | protected static function parseSecrets(array $secrets) 68 | { 69 | return collect($secrets)->mapWithKeys(function ($secret) { 70 | $segments = explode('/', $secret['Name']); 71 | 72 | return [$segments[count($segments) - 1] => $secret['Value']]; 73 | })->all(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Runtime/NotifiesLambda.php: -------------------------------------------------------------------------------- 1 | lambdaRequest( 19 | "http://{$this->apiUrl}/2018-06-01/runtime/invocation/{$invocationId}/response", $data 20 | ); 21 | } 22 | 23 | /** 24 | * Send the error response data to Lambda. 25 | * 26 | * @param string $invocationId 27 | * @param mixed $data 28 | * @return void 29 | */ 30 | protected function notifyLambdaOfError($invocationId, $data) 31 | { 32 | return $this->lambdaRequest( 33 | "http://{$this->apiUrl}/2018-06-01/runtime/invocation/{$invocationId}/error", $data 34 | ); 35 | } 36 | 37 | /** 38 | * Send the given data to the given URL as JSON. 39 | * 40 | * @param string $url 41 | * @param mixed $data 42 | * @return void 43 | */ 44 | protected function lambdaRequest($url, $data) 45 | { 46 | $json = json_encode($data); 47 | 48 | if ($json === false) { 49 | throw new Exception('Error encoding runtime JSON response: '.json_last_error_msg()); 50 | } 51 | 52 | $handler = curl_init($url); 53 | 54 | curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'POST'); 55 | curl_setopt($handler, CURLOPT_RETURNTRANSFER, true); 56 | curl_setopt($handler, CURLOPT_POSTFIELDS, $json); 57 | 58 | curl_setopt($handler, CURLOPT_HTTPHEADER, [ 59 | 'Content-Type: application/json', 60 | 'Content-Length: '.strlen($json), 61 | ]); 62 | 63 | curl_exec($handler); 64 | 65 | if (curl_error($handler)) { 66 | $errorMessage = curl_error($handler); 67 | 68 | throw new Exception('Error calling the runtime API: '.$errorMessage); 69 | } 70 | 71 | curl_setopt($handler, CURLOPT_HEADERFUNCTION, null); 72 | curl_setopt($handler, CURLOPT_READFUNCTION, null); 73 | curl_setopt($handler, CURLOPT_WRITEFUNCTION, null); 74 | curl_setopt($handler, CURLOPT_PROGRESSFUNCTION, null); 75 | 76 | curl_reset($handler); 77 | 78 | curl_close($handler); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Runtime/Handlers/CliHandler.php: -------------------------------------------------------------------------------- 1 | &1', 25 | $_ENV['LAMBDA_TASK_ROOT'], 26 | trim($event['cli'] ?? 'vapor:handle '.base64_encode(json_encode($event))) 27 | ) 28 | )->setTimeout(null); 29 | 30 | $process->run(function ($type, $line) use (&$output) { 31 | if (! Str::containsAll($line, ['{"message":', '"level":'])) { 32 | $output .= $line; 33 | } else { 34 | echo $line.PHP_EOL; 35 | } 36 | }); 37 | 38 | echo $output = json_encode([ 39 | 'output' => $output, 40 | 'context' => [ 41 | 'command' => $command, 42 | 'aws_request_id' => $_ENV['AWS_REQUEST_ID'] ?? null, 43 | ], 44 | ]); 45 | 46 | return new ArrayLambdaResponse(tap([ 47 | 'requestId' => $_ENV['AWS_REQUEST_ID'] ?? null, 48 | 'logGroup' => $_ENV['AWS_LAMBDA_LOG_GROUP_NAME'] ?? null, 49 | 'logStream' => $_ENV['AWS_LAMBDA_LOG_STREAM_NAME'] ?? null, 50 | 'statusCode' => $process->getExitCode(), 51 | 'output' => base64_encode($output), 52 | ], function ($response) use ($event) { 53 | $this->ping($event['callback'] ?? null, $response); 54 | })); 55 | } 56 | 57 | /** 58 | * Ping the given callback URL. 59 | * 60 | * @param string $callback 61 | * @param array $response 62 | * @return void 63 | */ 64 | protected function ping($callback, $response) 65 | { 66 | if (! isset($callback)) { 67 | return; 68 | } 69 | 70 | try { 71 | (new Client)->post($callback, ['json' => $response]); 72 | } catch (Throwable $e) { 73 | // 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Console/Commands/VaporScheduleCommand.php: -------------------------------------------------------------------------------- 1 | ensureValidCacheDriver()) { 38 | $this->call('schedule:run'); 39 | 40 | return 0; 41 | } 42 | 43 | $key = (string) Str::uuid(); 44 | $lockObtained = false; 45 | 46 | while (true) { 47 | if (! $lockObtained) { 48 | $lockObtained = $this->obtainLock($cache, $key); 49 | } 50 | 51 | if ($lockObtained && now()->second === 0) { 52 | $this->releaseLock($cache); 53 | 54 | $this->call('schedule:run'); 55 | 56 | return 0; 57 | } 58 | 59 | if (! $lockObtained && now()->second === 0) { 60 | return 1; 61 | } 62 | 63 | usleep(10000); 64 | } 65 | } 66 | 67 | /** 68 | * Ensure the cache driver is valid. 69 | */ 70 | protected function ensureValidCacheDriver(): ?Repository 71 | { 72 | $manager = $this->laravel['cache']; 73 | 74 | if (in_array($manager->getDefaultDriver(), ['memcached', 'redis', 'dynamodb', 'database'])) { 75 | return $manager->driver(); 76 | } 77 | 78 | return null; 79 | } 80 | 81 | /** 82 | * Obtain the lock for the schedule. 83 | */ 84 | protected function obtainLock(Repository $cache, string $key): bool 85 | { 86 | return $key === $cache->remember('vapor:schedule:lock', 60, function () use ($key) { 87 | return $key; 88 | }); 89 | } 90 | 91 | /** 92 | * Release the lock for the schedule. 93 | */ 94 | protected function releaseLock(Repository $cache): void 95 | { 96 | $cache->forget('vapor:schedule:lock'); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Runtime/LambdaInvocation.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 31 | } 32 | 33 | /** 34 | * Determine if the job have been attempted before. 35 | * 36 | * @param \Illuminate\Contracts\Queue\Job|string $job 37 | * @return bool 38 | */ 39 | protected function has($job) 40 | { 41 | return ! is_null($this->cache->get($this->key($job))); 42 | } 43 | 44 | /** 45 | * Get the number of times the job has been attempted. 46 | * 47 | * @param \Illuminate\Contracts\Queue\Job|string $job 48 | * @return int 49 | */ 50 | public function get($job) 51 | { 52 | return (int) $this->cache->get($this->key($job), 0); 53 | } 54 | 55 | /** 56 | * Increment the number of times the job has been attempted. 57 | * 58 | * @param \Illuminate\Contracts\Queue\Job $job 59 | * @return void 60 | */ 61 | public function increment($job) 62 | { 63 | if ($this->has($job)) { 64 | $this->cache->increment($this->key($job)); 65 | 66 | return; 67 | } 68 | 69 | $this->cache->put($this->key($job), 1, static::TTL); 70 | } 71 | 72 | /** 73 | * Transfer the job attempts from one job to another. 74 | * 75 | * @param \Illuminate\Contracts\Queue\Job|string $from 76 | * @param \Illuminate\Contracts\Queue\Job|string $to 77 | * @return void 78 | */ 79 | public function transfer($from, $to) 80 | { 81 | $this->cache->put($this->key($to), $this->get($from), static::TTL); 82 | 83 | $this->cache->forget($this->key($from)); 84 | } 85 | 86 | /** 87 | * Forget the number of times the job has been attempted. 88 | * 89 | * @param \Illuminate\Contracts\Queue\Job|string $job 90 | * @return null 91 | */ 92 | public function forget($job) 93 | { 94 | $this->cache->forget($this->key($job)); 95 | } 96 | 97 | /** 98 | * Gets the cache key for the given job. 99 | * 100 | * @param \Illuminate\Contracts\Queue\Job|string $job 101 | * @return string 102 | */ 103 | protected function key($job) 104 | { 105 | $jobId = $job instanceof Job ? $job->getJobId() : $job; 106 | 107 | return 'laravel_vapor_job_attempts:'.$jobId; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Runtime/Handlers/QueueHandler.php: -------------------------------------------------------------------------------- 1 | useStoragePath(StorageDirectories::PATH); 52 | 53 | $consoleKernel = static::$app->make(Kernel::class); 54 | 55 | static::$app->bind(LambdaEvent::class, function () use ($event) { 56 | return new LambdaEvent($event); 57 | }); 58 | 59 | $consoleInput = new StringInput( 60 | 'vapor:work '.$commandOptions.' --no-interaction' 61 | ); 62 | 63 | $status = $consoleKernel->handle( 64 | $consoleInput, $output = new BufferedOutput 65 | ); 66 | 67 | $consoleKernel->terminate($consoleInput, $status); 68 | 69 | return new ArrayLambdaResponse([ 70 | 'requestId' => $_ENV['AWS_REQUEST_ID'] ?? null, 71 | 'logGroup' => $_ENV['AWS_LAMBDA_LOG_GROUP_NAME'] ?? null, 72 | 'logStream' => $_ENV['AWS_LAMBDA_LOG_STREAM_NAME'] ?? null, 73 | 'statusCode' => $status, 74 | 'output' => base64_encode($output->fetch()), 75 | ]); 76 | } finally { 77 | unset(static::$app[LambdaEvent::class]); 78 | 79 | $this->terminate(); 80 | } 81 | } 82 | 83 | /** 84 | * Terminate any relevant application services. 85 | * 86 | * @return void 87 | */ 88 | protected function terminate() 89 | { 90 | if (static::$app->resolved('db') && ($_ENV['VAPOR_QUEUE_DATABASE_SESSION_PERSIST'] ?? false) !== 'true') { 91 | collect(static::$app->make('db')->getConnections())->each->disconnect(); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /stubs/cliRuntime.php: -------------------------------------------------------------------------------- 1 | useStoragePath(StorageDirectories::PATH); 57 | 58 | if (isset($_ENV['VAPOR_MAINTENANCE_MODE']) && 59 | $_ENV['VAPOR_MAINTENANCE_MODE'] === 'true') { 60 | file_put_contents($app->storagePath().'/framework/down', '[]'); 61 | } 62 | 63 | function_exists('__vapor_debug') && __vapor_debug('Caching Laravel configuration'); 64 | 65 | try { 66 | $app->make(ConsoleKernelContract::class)->call('config:cache'); 67 | } catch (Throwable $e) { 68 | function_exists('__vapor_debug') && __vapor_debug('Failing caching Laravel configuration: '.$e->getMessage()); 69 | } 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Listen For Lambda Invocations 74 | |-------------------------------------------------------------------------- 75 | | 76 | | When receiving Lambda requests to the CLI environment, we simply send 77 | | them to the appropriate handlers based on if they are CLI commands 78 | | or queued jobs. Then we'll return a response back to the Lambda. 79 | | 80 | */ 81 | 82 | $invocations = 0; 83 | 84 | $lambdaRuntime = LambdaRuntime::fromEnvironmentVariable(); 85 | 86 | while (true) { 87 | $lambdaRuntime->nextInvocation(function ($invocationId, $event) { 88 | return CliHandlerFactory::make($event) 89 | ->handle($event) 90 | ->toApiGatewayFormat(); 91 | }); 92 | 93 | LambdaContainer::terminateIfInvocationLimitHasBeenReached( 94 | ++$invocations, (int) ($_ENV['VAPOR_MAX_REQUESTS'] ?? 250) 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/Arr.php: -------------------------------------------------------------------------------- 1 | 31 | * @author Matthieu Napoli 32 | */ 33 | class Arr 34 | { 35 | /** 36 | * Set a multi-part body array value in the given array. 37 | * 38 | * @param array $array 39 | * @param string $name 40 | * @param mixed $value 41 | * @return array 42 | */ 43 | public static function setMultipartArrayValue(array $array, string $name, $value) 44 | { 45 | $segments = explode('[', $name); 46 | 47 | $pointer = &$array; 48 | 49 | foreach ($segments as $key => $segment) { 50 | // If this is our first time through the loop we will just grab the initial 51 | // key's part of the array. After this we will start digging deeper into 52 | // the array as needed until we get to the correct depth in the array. 53 | if ($key === 0) { 54 | $pointer = &$pointer[$segment]; 55 | 56 | continue; 57 | } 58 | 59 | // If this segment is malformed, we will just use the key as-is since there 60 | // is nothing we can do with it from here. We will return the array back 61 | // to the caller with the value set. We cannot continue looping on it. 62 | if (static::malformedMultipartSegment($segment)) { 63 | $array[$name] = $value; 64 | 65 | return $array; 66 | } 67 | 68 | $segment = substr($segment, 0, -1); 69 | 70 | // If the segment is empty after trimming off the closing bracket, it means 71 | // we are at the end of the segment and are ready to set the value so we 72 | // can grab a pointer to the array location and set it after the loop. 73 | if ($segment === '') { 74 | $pointer = &$pointer[]; 75 | } else { 76 | $pointer = &$pointer[$segment]; 77 | } 78 | } 79 | 80 | $pointer = $value; 81 | 82 | return $array; 83 | } 84 | 85 | /** 86 | * Determine if the given multi-part value segment is malformed. 87 | * 88 | * This can occur when there are two [[ or no closing bracket. 89 | * 90 | * @param string $segment 91 | * @return bool 92 | */ 93 | protected static function malformedMultipartSegment($segment) 94 | { 95 | return $segment === '' || substr($segment, -1) !== ']'; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Runtime/Fpm/ActsAsFastCgiDataProvider.php: -------------------------------------------------------------------------------- 1 | serverVariables['REQUEST_METHOD']; 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function getScriptFilename(): string 27 | { 28 | return $this->serverVariables['SCRIPT_FILENAME']; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function getServerSoftware(): string 35 | { 36 | return 'vapor'; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function getRemoteAddress(): string 43 | { 44 | return $this->serverVariables['REMOTE_ADDR']; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function getRemotePort(): int 51 | { 52 | return $this->serverVariables['SERVER_PORT']; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getServerAddress(): string 59 | { 60 | return $this->serverVariables['SERVER_ADDR']; 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function getServerPort(): int 67 | { 68 | return $this->serverVariables['SERVER_PORT']; 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function getServerName(): string 75 | { 76 | return $this->serverVariables['SERVER_NAME']; 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function getServerProtocol(): string 83 | { 84 | return $this->serverVariables['SERVER_PROTOCOL']; 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function getContentType(): string 91 | { 92 | return $this->serverVariables['CONTENT_TYPE']; 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function getContentLength(): int 99 | { 100 | $contentLength = $this->serverVariables['CONTENT_LENGTH'] ?: 0; 101 | 102 | return is_numeric($contentLength) ? (int) $contentLength : 0; 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | */ 108 | public function getContent(): string 109 | { 110 | return $this->body; 111 | } 112 | 113 | /** 114 | * {@inheritdoc} 115 | */ 116 | public function getCustomVars(): array 117 | { 118 | return $this->serverVariables; 119 | } 120 | 121 | /** 122 | * {@inheritdoc} 123 | */ 124 | public function getParams(): array 125 | { 126 | return $this->serverVariables; 127 | } 128 | 129 | /** 130 | * {@inheritdoc} 131 | */ 132 | public function getRequestUri(): string 133 | { 134 | return $this->serverVariables['PATH_INFO']; 135 | } 136 | 137 | /** 138 | * {@inheritdoc} 139 | */ 140 | public function getResponseCallbacks(): array 141 | { 142 | return []; 143 | } 144 | 145 | /** 146 | * {@inheritdoc} 147 | */ 148 | public function getFailureCallbacks(): array 149 | { 150 | return []; 151 | } 152 | 153 | /** 154 | * {@inheritdoc} 155 | */ 156 | public function getPassThroughCallbacks(): array 157 | { 158 | return []; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /stubs/octaneRuntime.php: -------------------------------------------------------------------------------- 1 | useStoragePath(StorageDirectories::PATH); 60 | 61 | function_exists('__vapor_debug') && __vapor_debug('Caching Laravel configuration'); 62 | 63 | $app->make(ConsoleKernelContract::class)->call('config:cache'); 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Start Octane Worker 68 | |-------------------------------------------------------------------------- 69 | | 70 | | We need to boot the application request Octane worker so it's ready to 71 | | serve incoming requests. This will initialize this worker then wait 72 | | for Lambda invocations to be received for this Vapor application. 73 | | 74 | */ 75 | function_exists('__vapor_debug') && __vapor_debug('Preparing to boot Octane'); 76 | 77 | Octane::boot( 78 | __DIR__, 79 | getenv('OCTANE_DATABASE_SESSION_PERSIST') === 'true', 80 | getenv('OCTANE_DATABASE_SESSION_TTL') ?: 0 81 | ); 82 | 83 | /* 84 | |-------------------------------------------------------------------------- 85 | | Listen For Lambda Invocations 86 | |-------------------------------------------------------------------------- 87 | | 88 | | When using FPM, we will listen for Lambda invocations and proxy them 89 | | through the FPM process. We'll then return formatted FPM response 90 | | back to the user. We'll monitor FPM to make sure it is running. 91 | | 92 | */ 93 | 94 | $invocations = 0; 95 | 96 | $lambdaRuntime = LambdaRuntime::fromEnvironmentVariable(); 97 | 98 | while (true) { 99 | $lambdaRuntime->nextInvocation(function ($invocationId, $event) { 100 | return OctaneHttpHandlerFactory::make($event) 101 | ->handle($event) 102 | ->toApiGatewayFormat(); 103 | }); 104 | 105 | LambdaContainer::terminateIfInvocationLimitHasBeenReached( 106 | ++$invocations, (int) ($_ENV['VAPOR_MAX_REQUESTS'] ?? 250) 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /stubs/fpmRuntime.php: -------------------------------------------------------------------------------- 1 | useStoragePath(StorageDirectories::PATH); 63 | 64 | function_exists('__vapor_debug') && __vapor_debug('Caching Laravel configuration'); 65 | 66 | $app->make(ConsoleKernelContract::class)->call('config:cache'); 67 | 68 | /* 69 | |-------------------------------------------------------------------------- 70 | | Start PHP-FPM 71 | |-------------------------------------------------------------------------- 72 | | 73 | | We need to boot the PHP-FPM process with the appropriate handler so it 74 | | is ready to accept requests. This will initialize this process then 75 | | wait for this socket to become ready before continuing execution. 76 | | 77 | */ 78 | 79 | function_exists('__vapor_debug') && __vapor_debug('Preparing to boot FPM'); 80 | 81 | $fpm = Fpm::boot( 82 | __DIR__.'/httpHandler.php' 83 | ); 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Listen For Lambda Invocations 88 | |-------------------------------------------------------------------------- 89 | | 90 | | When using FPM, we will listen for Lambda invocations and proxy them 91 | | through the FPM process. We'll then return formatted FPM response 92 | | back to the user. We'll monitor FPM to make sure it is running. 93 | | 94 | */ 95 | 96 | $invocations = 0; 97 | 98 | $lambdaRuntime = LambdaRuntime::fromEnvironmentVariable(); 99 | 100 | while (true) { 101 | $lambdaRuntime->nextInvocation(function ($invocationId, $event) { 102 | try { 103 | return FpmHttpHandlerFactory::make($event) 104 | ->handle($event) 105 | ->toApiGatewayFormat(); 106 | } catch (WriteFailedException $e) { 107 | if (Str::contains($e->getMessage(), 'Failed to write request to socket [broken pipe]')) { 108 | function_exists('__vapor_debug') && __vapor_debug($e->getMessage()); 109 | 110 | return (new LambdaResponse(502, [], '')) 111 | ->toApiGatewayFormat(); 112 | } 113 | 114 | throw $e; 115 | } 116 | }); 117 | 118 | $fpm->ensureRunning(); 119 | 120 | LambdaContainer::terminateIfInvocationLimitHasBeenReached( 121 | ++$invocations, (int) ($_ENV['VAPOR_MAX_REQUESTS'] ?? 250) 122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /src/Runtime/Environment.php: -------------------------------------------------------------------------------- 1 | app = $app; 62 | 63 | $this->environment = $_ENV['VAPOR_ENV'] ?? $_ENV['APP_ENV'] ?? 'production'; 64 | $this->environmentFile = '.env.'.$this->environment; 65 | $this->encryptedFile = '.env.'.$this->environment.'.encrypted'; 66 | } 67 | 68 | /** 69 | * Decrypt the environment file and load it into the runtime. 70 | * 71 | * @return void 72 | */ 73 | public static function decrypt($app) 74 | { 75 | (new static($app))->decryptEnvironment(); 76 | } 77 | 78 | /** 79 | * Decrypt the environment file and load it into the runtime. 80 | * 81 | * @return void 82 | */ 83 | public function decryptEnvironment() 84 | { 85 | try { 86 | if (! $this->canBeDecrypted()) { 87 | return; 88 | } 89 | 90 | $this->copyEncryptedFile(); 91 | 92 | $this->decryptFile(); 93 | 94 | $this->loadEnvironment(); 95 | } catch (Throwable $e) { 96 | function_exists('__vapor_debug') && __vapor_debug($e->getMessage()); 97 | } 98 | } 99 | 100 | /** 101 | * Determine if it is possible to decrypt the environment file. 102 | * 103 | * @return bool 104 | */ 105 | public function canBeDecrypted() 106 | { 107 | if (! isset($_ENV['LARAVEL_ENV_ENCRYPTION_KEY'])) { 108 | return false; 109 | } 110 | 111 | if (version_compare($this->app->version(), '9.37.0', '<')) { 112 | function_exists('__vapor_debug') && __vapor_debug('Decrypt command not available.'); 113 | 114 | return false; 115 | } 116 | 117 | if (! file_exists($this->app->basePath($this->encryptedFile))) { 118 | function_exists('__vapor_debug') && __vapor_debug('Encrypted environment file not found.'); 119 | 120 | return false; 121 | } 122 | 123 | return true; 124 | } 125 | 126 | /** 127 | * Copy the encrypted environment file to the writable path. 128 | * 129 | * @return void 130 | */ 131 | public function copyEncryptedFile() 132 | { 133 | copy( 134 | $this->app->basePath($this->encryptedFile), 135 | $this->writePath.DIRECTORY_SEPARATOR.$this->encryptedFile 136 | ); 137 | } 138 | 139 | /** 140 | * Decrypt the environment file. 141 | * 142 | * @return void 143 | */ 144 | public function decryptFile() 145 | { 146 | function_exists('__vapor_debug') && __vapor_debug('Decrypting environment variables.'); 147 | 148 | $this->console()->call('env:decrypt', ['--env' => $this->environment, '--path' => $this->writePath]); 149 | } 150 | 151 | /** 152 | * Load the decrypted environment file. 153 | * 154 | * @return void 155 | */ 156 | public function loadEnvironment() 157 | { 158 | function_exists('__vapor_debug') && __vapor_debug('Loading decrypted environment variables.'); 159 | 160 | Dotenv::createMutable($this->writePath, $this->environmentFile)->load(); 161 | } 162 | 163 | /** 164 | * Get the console kernel implementation. 165 | * 166 | * @return \Illuminate\Contracts\Console\Kernel 167 | */ 168 | public function console() 169 | { 170 | if (! $this->console) { 171 | $this->console = $this->app->make(Kernel::class); 172 | } 173 | 174 | return $this->console; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/Runtime/LambdaResponse.php: -------------------------------------------------------------------------------- 1 | body = $body; 42 | $this->status = $status; 43 | $this->headers = $headers; 44 | } 45 | 46 | /** 47 | * Convert the response to API Gateway's supported format. 48 | * 49 | * @return array 50 | */ 51 | public function toApiGatewayFormat() 52 | { 53 | $requiresEncoding = isset($this->headers['x-vapor-base64-encode'][0]); 54 | 55 | return [ 56 | 'isBase64Encoded' => $requiresEncoding, 57 | 'statusCode' => $this->status, 58 | 'headers' => empty($this->headers) ? new stdClass : $this->prepareHeaders($this->headers), 59 | 'body' => $requiresEncoding ? base64_encode($this->body) : $this->body, 60 | ]; 61 | } 62 | 63 | /** 64 | * Prepare the given response headers for API Gateway. 65 | * 66 | * @param array $responseHeaders 67 | * @return array 68 | */ 69 | protected function prepareHeaders(array $responseHeaders) 70 | { 71 | $headers = []; 72 | 73 | foreach ($responseHeaders as $name => $values) { 74 | $name = $this->normalizeHeaderName($name); 75 | 76 | if ($name == 'Set-Cookie') { 77 | $headers = array_merge($headers, $this->buildCookieHeaders($values)); 78 | 79 | continue; 80 | } 81 | 82 | foreach ($values as $value) { 83 | $headers[$name] = $value; 84 | } 85 | } 86 | 87 | if (! isset($headers['Content-Type'])) { 88 | $headers['Content-Type'] = 'text/html'; 89 | } 90 | 91 | return $headers; 92 | } 93 | 94 | /** 95 | * Build the Set-Cookie header names using binary casing. 96 | * 97 | * @param array $values 98 | * @return array 99 | */ 100 | protected function buildCookieHeaders(array $values) 101 | { 102 | $headers = []; 103 | 104 | foreach ($values as $index => $value) { 105 | $headers[$this->cookiePermutation($index)] = $value; 106 | } 107 | 108 | return $headers; 109 | } 110 | 111 | /** 112 | * Calculate the permutation of Set-Cookie for the current index. 113 | * 114 | * @param int $index 115 | * @return string 116 | */ 117 | protected function cookiePermutation($index) 118 | { 119 | // Hard-coded to support up to 18 cookies for now... 120 | switch ($index) { 121 | case 0: 122 | return 'set-cookie'; 123 | case 1: 124 | return 'Set-cookie'; 125 | case 2: 126 | return 'sEt-cookie'; 127 | case 3: 128 | return 'seT-cookie'; 129 | case 4: 130 | return 'set-Cookie'; 131 | case 5: 132 | return 'set-cOokie'; 133 | case 6: 134 | return 'set-coOkie'; 135 | case 7: 136 | return 'set-cooKie'; 137 | case 8: 138 | return 'set-cookIe'; 139 | case 9: 140 | return 'set-cookiE'; 141 | case 10: 142 | return 'SEt-cookie'; 143 | case 11: 144 | return 'SET-cookie'; 145 | case 12: 146 | return 'SEt-Cookie'; 147 | case 13: 148 | return 'SEt-cOokie'; 149 | case 14: 150 | return 'SEt-coOkie'; 151 | case 15: 152 | return 'SEt-cooKie'; 153 | case 16: 154 | return 'SEt-cookIe'; 155 | case 17: 156 | return 'SEt-cookiE'; 157 | default: 158 | return 'Set-Cookie'; 159 | } 160 | } 161 | 162 | /** 163 | * Normalize the given header name into studly-case. 164 | * 165 | * @param string $name 166 | * @return string 167 | */ 168 | protected function normalizeHeaderName($name) 169 | { 170 | return str_replace(' ', '-', ucwords(str_replace('-', ' ', $name))); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Http/Controllers/SignedStorageUrlController.php: -------------------------------------------------------------------------------- 1 | ensureEnvironmentVariablesAreAvailable($request); 24 | 25 | Gate::authorize('uploadFiles', [ 26 | $request->user(), 27 | $bucket = $request->input('bucket') ?: $_ENV['AWS_BUCKET'], 28 | ]); 29 | 30 | $client = $this->storageClient(); 31 | 32 | $uuid = (string) Str::uuid(); 33 | 34 | $expiresAfter = config('vapor.signed_storage_url_expires_after', 5); 35 | 36 | $signedRequest = $client->createPresignedRequest( 37 | $this->createCommand($request, $client, $bucket, $key = $this->getKey($uuid)), 38 | sprintf('+%s minutes', $expiresAfter) 39 | ); 40 | 41 | $uri = $signedRequest->getUri(); 42 | 43 | return response()->json([ 44 | 'uuid' => $uuid, 45 | 'bucket' => $bucket, 46 | 'key' => $key, 47 | 'url' => $uri->getScheme().'://'.$uri->getAuthority().$uri->getPath().'?'.$uri->getQuery(), 48 | 'headers' => $this->headers($request, $signedRequest), 49 | ], 201); 50 | } 51 | 52 | /** 53 | * Create a command for the PUT operation. 54 | * 55 | * @param \Illuminate\Http\Request $request 56 | * @param \Aws\S3\S3Client $client 57 | * @param string $bucket 58 | * @param string $key 59 | * @return \Aws\Command 60 | */ 61 | protected function createCommand(Request $request, S3Client $client, $bucket, $key) 62 | { 63 | return $client->getCommand('putObject', array_filter([ 64 | 'Bucket' => $bucket, 65 | 'Key' => $key, 66 | 'ACL' => $request->input('visibility') ?: $this->defaultVisibility(), 67 | 'ContentType' => $request->input('content_type') ?: 'application/octet-stream', 68 | 'CacheControl' => $request->input('cache_control') ?: null, 69 | 'Expires' => $request->input('expires') ?: null, 70 | ])); 71 | } 72 | 73 | /** 74 | * Get the headers that should be used when making the signed request. 75 | * 76 | * @param \Illuminate\Http\Request $request 77 | * @param \GuzzleHttp\Psr7\Request 78 | * @return array 79 | */ 80 | protected function headers(Request $request, $signedRequest) 81 | { 82 | return array_merge( 83 | $signedRequest->getHeaders(), 84 | [ 85 | 'Content-Type' => $request->input('content_type') ?: 'application/octet-stream', 86 | ] 87 | ); 88 | } 89 | 90 | /** 91 | * Ensure the required environment variables are available. 92 | * 93 | * @param \Illuminate\Http\Request $request 94 | * @return void 95 | */ 96 | protected function ensureEnvironmentVariablesAreAvailable(Request $request) 97 | { 98 | $missing = array_diff_key(array_flip(array_filter([ 99 | $request->input('bucket') ? null : 'AWS_BUCKET', 100 | 'AWS_DEFAULT_REGION', 101 | 'AWS_ACCESS_KEY_ID', 102 | 'AWS_SECRET_ACCESS_KEY', 103 | ])), $_ENV); 104 | 105 | if (empty($missing)) { 106 | return; 107 | } 108 | 109 | throw new InvalidArgumentException( 110 | 'Unable to issue signed URL. Missing environment variables: '.implode(', ', array_keys($missing)) 111 | ); 112 | } 113 | 114 | /** 115 | * Get the S3 storage client instance. 116 | * 117 | * @return \Aws\S3\S3Client 118 | */ 119 | protected function storageClient() 120 | { 121 | $config = [ 122 | 'region' => config('filesystems.disks.s3.region', $_ENV['AWS_DEFAULT_REGION']), 123 | 'version' => 'latest', 124 | 'signature_version' => 'v4', 125 | 'use_path_style_endpoint' => config('filesystems.disks.s3.use_path_style_endpoint', false), 126 | ]; 127 | 128 | if (! isset($_ENV['AWS_LAMBDA_FUNCTION_VERSION'])) { 129 | $config['credentials'] = array_filter([ 130 | 'key' => $_ENV['AWS_ACCESS_KEY_ID'] ?? null, 131 | 'secret' => $_ENV['AWS_SECRET_ACCESS_KEY'] ?? null, 132 | 'token' => $_ENV['AWS_SESSION_TOKEN'] ?? null, 133 | ]); 134 | 135 | if (array_key_exists('AWS_URL', $_ENV) && ! is_null($_ENV['AWS_URL'])) { 136 | $config['url'] = $_ENV['AWS_URL']; 137 | $config['endpoint'] = $_ENV['AWS_URL']; 138 | } 139 | 140 | if (array_key_exists('AWS_ENDPOINT', $_ENV) && ! is_null($_ENV['AWS_ENDPOINT'])) { 141 | $config['endpoint'] = $_ENV['AWS_ENDPOINT']; 142 | } 143 | } 144 | 145 | return new S3Client($config); 146 | } 147 | 148 | /** 149 | * Get key for the given UUID. 150 | * 151 | * @param string $uuid 152 | * @return string 153 | */ 154 | protected function getKey(string $uuid) 155 | { 156 | return 'tmp/'.$uuid; 157 | } 158 | 159 | /** 160 | * Get the default visibility for uploads. 161 | * 162 | * @return string 163 | */ 164 | protected function defaultVisibility() 165 | { 166 | return 'private'; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Console/Commands/VaporQueueListFailedCommand.php: -------------------------------------------------------------------------------- 1 | options()) 47 | ->filter(function ($value, $option) { 48 | return ! is_null($value) && in_array($option, ['id', 'queue', 'query', 'start']); 49 | }); 50 | 51 | $failer = $this->laravel['queue.failer']; 52 | $start = $this->option('start'); 53 | 54 | if (is_callable([$failer, 'getTable']) && $start) { 55 | $failed = $failer->getTable() 56 | ->where('failed_at', '>=', Carbon::createFromTimestamp($start)->toDateTimeString()) 57 | ->get(); 58 | 59 | $options = $options->reject(function ($value, $name) { 60 | return $name === 'start'; 61 | }); 62 | } else { 63 | $failed = $failer->all(); 64 | } 65 | 66 | $failedJobs = collect($failed)->filter(function ($job) use ($options) { 67 | return $options->every(function ($value, $option) use ($job) { 68 | return $this->filter($job, $option, $value); 69 | }); 70 | }); 71 | 72 | $total = count($failedJobs); 73 | 74 | $page = $this->option('page'); 75 | $limit = $this->option('limit'); 76 | 77 | if ($limit) { 78 | $failedJobs = $failedJobs->forPage($page, $limit); 79 | } 80 | 81 | $failedJobs = $failedJobs->map(function ($failed) { 82 | return array_merge((array) $failed, [ 83 | 'payload' => $failed->payload, 84 | 'exception' => Str::limit($failed->exception, 1000), 85 | 'name' => $this->extractJobName($failed->payload), 86 | 'queue' => Str::afterLast($failed->queue, '/'), 87 | 'message' => $this->extractMessage($failed->exception), 88 | 'connection' => $failed->connection, 89 | ]); 90 | })->values()->toArray(); 91 | 92 | $failedJobs = [ 93 | 'failed_jobs' => $failedJobs, 94 | 'total' => $total, 95 | 'from' => $limit ? ($page - 1) * $limit + 1 : 1, 96 | 'to' => $limit ? min($page * $limit, $total) : $total, 97 | 'has_next_page' => $limit && $total > $limit * $page, 98 | 'has_previous_page' => $limit && $page > 1 && $total > $limit * ($page - 1), 99 | ]; 100 | 101 | $this->output->writeln( 102 | json_encode($failedJobs) 103 | ); 104 | } 105 | 106 | /** 107 | * Extract the failed job name from payload. 108 | * 109 | * @param string $payload 110 | * @return string|null 111 | */ 112 | private function extractJobName($payload) 113 | { 114 | $payload = json_decode($payload, true); 115 | 116 | if ($payload && (! isset($payload['data']['command']))) { 117 | return $payload['job'] ?? null; 118 | } elseif ($payload && isset($payload['data']['command'])) { 119 | return $this->matchJobName($payload); 120 | } 121 | } 122 | 123 | /** 124 | * Extract the failed job message from exception. 125 | * 126 | * @param string $exception 127 | * @return string 128 | */ 129 | private function extractMessage($exception) 130 | { 131 | if (Str::startsWith($exception, ManuallyFailedException::class)) { 132 | $message = 'Manually failed'; 133 | } else { 134 | [$_, $message] = explode(':', $exception); 135 | [$message] = explode(' in /', $message); 136 | [$message] = explode(' in closure', $message); 137 | } 138 | 139 | if (! empty($message)) { 140 | return trim($message); 141 | } 142 | 143 | return ''; 144 | } 145 | 146 | /** 147 | * Match the job name from the payload. 148 | * 149 | * @param array $payload 150 | * @return string|null 151 | */ 152 | protected function matchJobName($payload) 153 | { 154 | preg_match('/"([^"]+)"/', $payload['data']['command'], $matches); 155 | 156 | return $matches[1] ?? $payload['job'] ?? null; 157 | } 158 | 159 | /** 160 | * Determine whether the given job matches the given filter. 161 | * 162 | * @param stdClass $job 163 | * @param string $option 164 | * @param string $value 165 | * @return bool 166 | */ 167 | protected function filter($job, $option, $value) 168 | { 169 | if ($option === 'id') { 170 | return $job->id === $value; 171 | } 172 | 173 | if ($option === 'queue') { 174 | return Str::afterLast($job->queue, '/') === $value; 175 | } 176 | 177 | if ($option === 'query') { 178 | return Str::contains(json_encode($job), $value); 179 | } 180 | 181 | if ($option === 'start') { 182 | return Carbon::parse($job->failed_at)->timestamp >= $value; 183 | } 184 | 185 | return false; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/VaporServiceProvider.php: -------------------------------------------------------------------------------- 1 | ensureRoutesAreDefined(); 33 | $this->registerOctaneCommands(); 34 | 35 | if (($_ENV['VAPOR_SERVERLESS_DB'] ?? null) === 'true') { 36 | Schema::defaultStringLength(191); 37 | } 38 | 39 | if ($this->app->resolved('queue')) { 40 | call_user_func($this->queueExtender()); 41 | } else { 42 | $this->app->afterResolving( 43 | 'queue', $this->queueExtender() 44 | ); 45 | } 46 | } 47 | 48 | /** 49 | * Get the queue extension callback. 50 | * 51 | * @return \Closure 52 | */ 53 | protected function queueExtender() 54 | { 55 | return function () { 56 | Queue::extend('sqs', function () { 57 | return new VaporConnector; 58 | }); 59 | }; 60 | } 61 | 62 | /** 63 | * Register the service provider. 64 | * 65 | * @return void 66 | */ 67 | public function register() 68 | { 69 | $this->app->singleton( 70 | Contracts\SignedStorageUrlController::class, 71 | SignedStorageUrlController::class 72 | ); 73 | 74 | $this->configure(); 75 | $this->offerPublishing(); 76 | $this->ensureAssetPathsAreConfigured(); 77 | $this->ensureRedisIsConfigured(); 78 | $this->ensureDynamoDbIsConfigured(); 79 | $this->ensureQueueIsConfigured(); 80 | $this->ensureSqsIsConfigured(); 81 | $this->ensureMixIsConfigured(); 82 | $this->configureTrustedProxy(); 83 | 84 | $this->registerMiddleware(); 85 | $this->registerCommands(); 86 | } 87 | 88 | /** 89 | * Setup the configuration for Horizon. 90 | * 91 | * @return void 92 | */ 93 | protected function configure() 94 | { 95 | $this->mergeConfigFrom( 96 | __DIR__.'/../config/vapor.php', 'vapor' 97 | ); 98 | } 99 | 100 | /** 101 | * Setup the resource publishing groups for Horizon. 102 | * 103 | * @return void 104 | */ 105 | protected function offerPublishing() 106 | { 107 | if ($this->app->runningInConsole()) { 108 | $this->publishes([ 109 | __DIR__.'/../config/vapor.php' => config_path('vapor.php'), 110 | ], 'vapor-config'); 111 | } 112 | } 113 | 114 | /** 115 | * Ensure Laravel Mix is properly configured. 116 | * 117 | * @return void 118 | */ 119 | protected function ensureMixIsConfigured() 120 | { 121 | if (isset($_ENV['MIX_URL'])) { 122 | Config::set('app.mix_url', $_ENV['MIX_URL']); 123 | } 124 | } 125 | 126 | /** 127 | * Configure trusted proxy. 128 | * 129 | * @return void 130 | */ 131 | private function configureTrustedProxy() 132 | { 133 | Config::set('trustedproxy.proxies', Config::get('trustedproxy.proxies') ?? ['0.0.0.0/0', '2000:0:0:0:0:0:0:0/3']); 134 | } 135 | 136 | /** 137 | * Register the Vapor HTTP middleware. 138 | * 139 | * @return void 140 | */ 141 | protected function registerMiddleware() 142 | { 143 | $this->app[HttpKernel::class]->pushMiddleware(ServeStaticAssets::class); 144 | } 145 | 146 | /** 147 | * Register the Vapor console commands. 148 | * 149 | * @return void 150 | * 151 | * @throws \InvalidArgumentException 152 | */ 153 | protected function registerCommands() 154 | { 155 | if (! $this->app->runningInConsole()) { 156 | return; 157 | } 158 | 159 | $this->app[ConsoleKernel::class]->command('vapor:handle {payload}', function () { 160 | throw new InvalidArgumentException( 161 | 'Unknown event type. Please create a vapor:handle command to handle custom events.' 162 | ); 163 | }); 164 | 165 | $this->app->singleton('command.vapor.work', function ($app) { 166 | return new VaporWorkCommand($app['queue.vaporWorker']); 167 | }); 168 | 169 | $this->app->singleton('command.vapor.queue-failed', function () { 170 | return new VaporQueueListFailedCommand; 171 | }); 172 | 173 | $this->app->singleton('command.vapor.health-check', function () { 174 | return new VaporHealthCheckCommand; 175 | }); 176 | 177 | $this->app->singleton('command.vapor.schedule', function () { 178 | return new VaporScheduleCommand; 179 | }); 180 | 181 | $this->commands(['command.vapor.work', 'command.vapor.queue-failed', 'command.vapor.health-check', 'command.vapor.schedule']); 182 | } 183 | 184 | /** 185 | * Register the Vapor "Octane" console commands. 186 | * 187 | * @return void 188 | * 189 | * @throws \InvalidArgumentException 190 | */ 191 | protected function registerOctaneCommands() 192 | { 193 | // Ensure we are running on Vapor... 194 | if (! isset($_ENV['VAPOR_SSM_PATH'])) { 195 | return; 196 | } 197 | 198 | if ($this->app->runningInConsole()) { 199 | $this->commands(OctaneStatusCommand::class); 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Console/Commands/VaporWorkCommand.php: -------------------------------------------------------------------------------- 1 | worker = $worker; 66 | } 67 | 68 | /** 69 | * Execute the console command. 70 | * 71 | * @param \Laravel\Vapor\Events\LambdaEvent $event 72 | * @return void 73 | */ 74 | public function handle(LambdaEvent $event) 75 | { 76 | if ($this->downForMaintenance()) { 77 | return; 78 | } 79 | 80 | if (! static::$listeningForEvents) { 81 | $this->listenForEvents(); 82 | 83 | static::$listeningForEvents = true; 84 | } 85 | 86 | $this->worker->setCache($this->laravel['cache']->driver()); 87 | 88 | return $this->worker->runVaporJob( 89 | $this->marshalJob($this->message($event)), 90 | 'sqs', 91 | $this->gatherWorkerOptions() 92 | ); 93 | } 94 | 95 | /** 96 | * Marshal the job with the given message ID. 97 | * 98 | * @param array $message 99 | * @return \Laravel\Vapor\Queue\VaporJob 100 | */ 101 | protected function marshalJob(array $message) 102 | { 103 | $normalizedMessage = $this->normalizeMessage($message); 104 | 105 | $queue = $this->worker->getManager()->connection('sqs'); 106 | 107 | return new VaporJob( 108 | $this->laravel, $queue->getSqs(), $normalizedMessage, 109 | 'sqs', $this->queueUrl($message) 110 | ); 111 | } 112 | 113 | /** 114 | * Normalize the casing of the message array. 115 | * 116 | * @param array $message 117 | * @return array 118 | */ 119 | protected function normalizeMessage(array $message) 120 | { 121 | return [ 122 | 'MessageId' => $message['messageId'], 123 | 'ReceiptHandle' => $message['receiptHandle'], 124 | 'Body' => $message['body'], 125 | 'Attributes' => $message['attributes'], 126 | 'MessageAttributes' => $message['messageAttributes'], 127 | ]; 128 | } 129 | 130 | /** 131 | * Get the message payload. 132 | * 133 | * @param \Laravel\Vapor\Events\LambdaEvent $event 134 | * @return array 135 | */ 136 | protected function message($event) 137 | { 138 | return $event['Records'][0]; 139 | } 140 | 141 | /** 142 | * Get the queue URL from the given message. 143 | * 144 | * @param array $message 145 | * @return string 146 | */ 147 | protected function queueUrl(array $message) 148 | { 149 | $eventSourceArn = explode(':', $message['eventSourceARN']); 150 | 151 | return sprintf( 152 | 'https://sqs.%s.amazonaws.com/%s/%s', 153 | $message['awsRegion'], 154 | $accountId = $eventSourceArn[4], 155 | $queue = $eventSourceArn[5] 156 | ); 157 | } 158 | 159 | /** 160 | * Gather all of the queue worker options as a single object. 161 | * 162 | * @return \Illuminate\Queue\WorkerOptions 163 | */ 164 | protected function gatherWorkerOptions() 165 | { 166 | $options = [ 167 | $this->option('delay'), 168 | $memory = 512, 169 | $this->option('timeout'), 170 | $sleep = 0, 171 | $this->option('tries'), 172 | $this->option('force'), 173 | $stopWhenEmpty = false, 174 | ]; 175 | 176 | if (property_exists(WorkerOptions::class, 'name')) { 177 | $options = array_merge(['default'], $options); 178 | } 179 | 180 | return new WorkerOptions(...$options); 181 | } 182 | 183 | /** 184 | * Store a failed job event. 185 | * 186 | * @param \Illuminate\Queue\Events\JobFailed $event 187 | * @return void 188 | */ 189 | protected function logFailedJob(JobFailed $event) 190 | { 191 | $this->laravel['queue.failer']->log( 192 | $event->connectionName, $event->job->getQueue(), 193 | $event->job->getRawBody(), $event->exception 194 | ); 195 | } 196 | 197 | /** 198 | * Determine if the worker should run in maintenance mode. 199 | * 200 | * @return bool 201 | */ 202 | protected function downForMaintenance() 203 | { 204 | if (! $this->option('force')) { 205 | return $this->laravel->isDownForMaintenance(); 206 | } 207 | 208 | return false; 209 | } 210 | 211 | /** 212 | * Reset static variables. 213 | * 214 | * @return void 215 | */ 216 | public static function flushState() 217 | { 218 | static::$listeningForEvents = false; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/Runtime/HttpKernel.php: -------------------------------------------------------------------------------- 1 | app = $app; 39 | } 40 | 41 | /** 42 | * Handle the incoming HTTP request. 43 | * 44 | * @param \Illuminate\Http\Request $request 45 | * @return \Illuminate\Http\Response 46 | */ 47 | public function handle(Request $request) 48 | { 49 | $this->app->useStoragePath(StorageDirectories::PATH); 50 | 51 | if (static::shouldSendMaintenanceModeResponse($request)) { 52 | if ( 53 | isset($_ENV['VAPOR_MAINTENANCE_MODE_SECRET']) && 54 | $_ENV['VAPOR_MAINTENANCE_MODE_SECRET'] == $request->path() 55 | ) { 56 | $response = static::bypassResponse($_ENV['VAPOR_MAINTENANCE_MODE_SECRET']); 57 | 58 | $this->app->terminate(); 59 | } elseif ( 60 | isset($_ENV['VAPOR_MAINTENANCE_MODE_SECRET']) && 61 | static::hasValidBypassCookie($request, $_ENV['VAPOR_MAINTENANCE_MODE_SECRET']) 62 | ) { 63 | $response = $this->sendRequest($request); 64 | } else { 65 | if ($request->wantsJson() && file_exists($_ENV['LAMBDA_TASK_ROOT'].'/503.json')) { 66 | $response = JsonResponse::fromJsonString( 67 | file_get_contents($_ENV['LAMBDA_TASK_ROOT'].'/503.json'), 68 | 503 69 | ); 70 | } else { 71 | $response = new Response( 72 | file_get_contents($_ENV['LAMBDA_TASK_ROOT'].'/503.html'), 73 | 503 74 | ); 75 | } 76 | 77 | $this->app->terminate(); 78 | } 79 | } else { 80 | $response = $this->sendRequest($request); 81 | } 82 | 83 | return $response; 84 | } 85 | 86 | /** 87 | * Determine if a maintenance mode response should be sent. 88 | * 89 | * @param \Illuminate\Http\Request $request 90 | * @return bool 91 | */ 92 | public static function shouldSendMaintenanceModeResponse(Request $request) 93 | { 94 | return isset($_ENV['VAPOR_MAINTENANCE_MODE']) && 95 | $_ENV['VAPOR_MAINTENANCE_MODE'] === 'true' && 96 | 'https://'.$request->getHttpHost() !== $_ENV['APP_VANITY_URL']; 97 | } 98 | 99 | /** 100 | * Determine if the incoming request has a maintenance mode bypass cookie. 101 | * 102 | * @param \Illuminate\Http\Request $request 103 | * @param string $secret 104 | * @return bool 105 | */ 106 | public static function hasValidBypassCookie($request, $secret) 107 | { 108 | return $request->cookie('laravel_maintenance') && 109 | MaintenanceModeBypassCookie::isValid( 110 | $request->cookie('laravel_maintenance'), 111 | $secret 112 | ); 113 | } 114 | 115 | /** 116 | * Redirect the user back to the root of the application with a maintenance mode bypass cookie. 117 | * 118 | * @param string $secret 119 | * @return \Illuminate\Http\RedirectResponse 120 | */ 121 | public static function bypassResponse(string $secret) 122 | { 123 | $response = new RedirectResponse('/'); 124 | 125 | $expiresAt = Carbon::now()->addHours(12); 126 | $path = isset($_ENV['VAPOR_MAINTENANCE_MODE_COOKIE_PATH']) ? $_ENV['VAPOR_MAINTENANCE_MODE_COOKIE_PATH'] : '/'; 127 | $domain = isset($_ENV['VAPOR_MAINTENANCE_MODE_COOKIE_DOMAIN']) ? $_ENV['VAPOR_MAINTENANCE_MODE_COOKIE_DOMAIN'] : null; 128 | 129 | $cookie = new Cookie('laravel_maintenance', base64_encode(json_encode([ 130 | 'expires_at' => $expiresAt->getTimestamp(), 131 | 'mac' => hash_hmac('sha256', $expiresAt->getTimestamp(), $secret), 132 | ])), $expiresAt, $path, $domain); 133 | 134 | $response->headers->setCookie($cookie); 135 | 136 | return $response; 137 | } 138 | 139 | /** 140 | * Resolve the HTTP kernel for the request. 141 | * 142 | * @param \Illuminate\Http\Request $request 143 | * @return \Illuminate\Contracts\Http\Kernel 144 | */ 145 | protected function resolveKernel(Request $request) 146 | { 147 | return tap($this->app->make(HttpKernelContract::class), function ($kernel) use ($request) { 148 | $this->app->instance('request', $request); 149 | 150 | Facade::clearResolvedInstance('request'); 151 | 152 | $kernel->bootstrap(); 153 | }); 154 | } 155 | 156 | /** 157 | * Send the request to the kernel. 158 | * 159 | * @param \Illuminate\Http\Request $request 160 | * @return \Illuminate\Http\Response 161 | */ 162 | protected function sendRequest(Request $request) 163 | { 164 | $kernel = $this->resolveKernel($request); 165 | 166 | $response = (new Pipeline)->send($request) 167 | ->through([ 168 | new EnsureOnNakedDomain, 169 | new RedirectStaticAssets, 170 | new EnsureVanityUrlIsNotIndexed, 171 | new EnsureBinaryEncoding(), 172 | ])->then(function ($request) use ($kernel) { 173 | return $kernel->handle($request); 174 | }); 175 | 176 | $kernel->terminate($request, $response); 177 | 178 | return $response; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Runtime/Octane/OctaneRequestContextFactory.php: -------------------------------------------------------------------------------- 1 | serverVariables['REQUEST_METHOD']; 29 | 30 | $contentType = array_change_key_case($request->headers)['content-type'] ?? null; 31 | 32 | $serverRequest = new ServerRequest( 33 | $request->serverVariables['REQUEST_METHOD'], 34 | static::parseUri($request->serverVariables['REQUEST_URI']), 35 | $request->headers, 36 | $request->body, 37 | $request->serverVariables['SERVER_PROTOCOL'], 38 | $request->serverVariables 39 | ); 40 | 41 | $serverRequest = $serverRequest->withCookieParams(static::cookies($event, $request->headers)); 42 | 43 | $serverRequest = $serverRequest->withUploadedFiles(static::uploadedFiles( 44 | $method, $contentType, $request->body 45 | )); 46 | 47 | $serverRequest = $serverRequest->withParsedBody(static::parsedBody( 48 | $method, $contentType, $request->body 49 | )); 50 | 51 | parse_str($request->serverVariables['QUERY_STRING'], $queryParams); 52 | 53 | $serverRequest = $serverRequest->withQueryParams($queryParams); 54 | 55 | return new RequestContext([ 56 | 'psr7Request' => $serverRequest, 57 | ]); 58 | } 59 | 60 | /** 61 | * Get the cookies from the given headers. 62 | * 63 | * @param array $event 64 | * @param array $headers 65 | * @return array 66 | */ 67 | protected static function cookies($event, $headers) 68 | { 69 | if (isset($event['version']) && $event['version'] === '2.0') { 70 | $cookies = $event['cookies'] ?? []; 71 | } else { 72 | $headers = array_change_key_case($headers); 73 | 74 | $cookies = isset($headers['cookie']) ? explode('; ', $headers['cookie']) : []; 75 | } 76 | 77 | if (empty($cookies)) { 78 | return []; 79 | } 80 | 81 | return Collection::make($cookies)->mapWithKeys(function ($cookie) { 82 | $cookie = explode('=', trim($cookie), 2); 83 | 84 | $key = $cookie[0]; 85 | 86 | if (! isset($cookie[1])) { 87 | return [$key => null]; 88 | } 89 | 90 | return [$key => urldecode($cookie[1])]; 91 | })->filter()->all(); 92 | } 93 | 94 | /** 95 | * Create a new file instance from the given HTTP request document part. 96 | * 97 | * @param \Riverline\MultipartParser\Part $part 98 | * @return \Psr\Http\Message\UploadedFileInterface 99 | */ 100 | protected static function createFile($part) 101 | { 102 | file_put_contents( 103 | $path = tempnam(sys_get_temp_dir(), 'vapor_upload_'), 104 | $part->getBody() 105 | ); 106 | 107 | return new UploadedFile( 108 | $path, 109 | filesize($path), 110 | UPLOAD_ERR_OK, 111 | $part->getFileName(), 112 | $part->getMimeType() 113 | ); 114 | } 115 | 116 | /** 117 | * Parse the files for the given HTTP request body. 118 | * 119 | * @param string $contentType 120 | * @param string $body 121 | * @return array 122 | */ 123 | protected static function parseFiles($contentType, $body) 124 | { 125 | $document = new Part("Content-Type: $contentType\r\n\r\n".$body); 126 | 127 | if (! $document->isMultiPart()) { 128 | return []; 129 | } 130 | 131 | return Collection::make($document->getParts()) 132 | ->filter 133 | ->isFile() 134 | ->reduce(function ($files, $part) { 135 | return Str::contains($name = $part->getName(), '[') 136 | ? Arr::setMultiPartArrayValue($files, $name, static::createFile($part)) 137 | : SupportArr::set($files, $name, static::createFile($part)); 138 | }, []); 139 | } 140 | 141 | /** 142 | * Get the uploaded files for the incoming event. 143 | * 144 | * @param string $method 145 | * @param string $contentType 146 | * @param string $body 147 | * @return array 148 | */ 149 | protected static function uploadedFiles($method, $contentType, $body) 150 | { 151 | if (! in_array($method, ['POST', 'PUT']) || 152 | is_null($contentType) || 153 | static::isUrlEncodedForm($contentType)) { 154 | return []; 155 | } 156 | 157 | return static::parseFiles($contentType, $body); 158 | } 159 | 160 | /** 161 | * Get the parsed body for the event. 162 | * 163 | * @param string $method 164 | * @param string $contentType 165 | * @param string $body 166 | * @return array|null 167 | */ 168 | protected static function parsedBody($method, $contentType, $body) 169 | { 170 | if (! in_array($method, ['POST', 'PUT']) || is_null($contentType)) { 171 | return; 172 | } 173 | 174 | if (static::isUrlEncodedForm($contentType)) { 175 | parse_str($body, $parsedBody); 176 | 177 | return $parsedBody; 178 | } 179 | 180 | return static::parseBody($contentType, $body); 181 | } 182 | 183 | /** 184 | * Parse the incoming event's request body. 185 | * 186 | * @param string $contentType 187 | * @param string $body 188 | * @return array 189 | */ 190 | protected static function parseBody($contentType, $body) 191 | { 192 | $document = new Part("Content-Type: $contentType\r\n\r\n".$body); 193 | 194 | if (! $document->isMultiPart()) { 195 | return; 196 | } 197 | 198 | return Collection::make($document->getParts()) 199 | ->reject 200 | ->isFile() 201 | ->reduce(function ($parsedBody, $part) { 202 | return Str::contains($name = $part->getName(), '[') 203 | ? Arr::setMultiPartArrayValue($parsedBody, $name, $part->getBody()) 204 | : SupportArr::set($parsedBody, $name, $part->getBody()); 205 | }, []); 206 | } 207 | 208 | /** 209 | * Parse the incoming event's request uri. 210 | * 211 | * @param string $uri 212 | * @return string 213 | */ 214 | protected static function parseUri($uri) 215 | { 216 | if (parse_url($uri) === false) { 217 | return '/'; 218 | } 219 | 220 | return $uri; 221 | } 222 | 223 | /** 224 | * Determine if the given content type represents a URL encoded form. 225 | * 226 | * @param string $contentType 227 | * @return bool 228 | */ 229 | protected static function isUrlEncodedForm($contentType) 230 | { 231 | return Str::contains(strtolower($contentType), 'application/x-www-form-urlencoded'); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/Runtime/Fpm/Fpm.php: -------------------------------------------------------------------------------- 1 | client = $client; 71 | $this->handler = $handler; 72 | $this->serverVariables = $serverVariables; 73 | $this->socketConnection = $socketConnection; 74 | } 75 | 76 | /** 77 | * Boot FPM with the given handler. 78 | * 79 | * @param string $handler 80 | * @return static 81 | */ 82 | public static function boot($handler, array $serverVariables = []) 83 | { 84 | if (file_exists(static::SOCKET)) { 85 | @unlink(static::SOCKET); 86 | } 87 | 88 | $socketConnection = new UnixDomainSocket(self::SOCKET, 1000, 900000); 89 | 90 | return static::$instance = tap(new static(new Client, $socketConnection, $handler, $serverVariables), function ($fpm) { 91 | $fpm->start(); 92 | }); 93 | } 94 | 95 | /** 96 | * Resolve the static FPM instance. 97 | * 98 | * @return static 99 | */ 100 | public static function resolve() 101 | { 102 | return static::$instance; 103 | } 104 | 105 | /** 106 | * Start the PHP-FPM process. 107 | */ 108 | public function start() 109 | { 110 | if ($this->isReady()) { 111 | $this->killExistingFpm(); 112 | } 113 | 114 | function_exists('__vapor_debug') && __vapor_debug('Ensuring ready to start FPM'); 115 | 116 | $this->ensureReadyToStart(); 117 | 118 | $this->fpm = new Process([ 119 | 'php-fpm', 120 | '--nodaemonize', 121 | '--force-stderr', 122 | '--fpm-config', 123 | self::CONFIG, 124 | ]); 125 | 126 | function_exists('__vapor_debug') && __vapor_debug('Starting FPM Process...'); 127 | 128 | $this->fpm->disableOutput() 129 | ->setTimeout(null) 130 | ->start(function ($type, $output) { 131 | fwrite(STDERR, $output.PHP_EOL); 132 | }); 133 | 134 | $this->ensureFpmHasStarted(); 135 | } 136 | 137 | /** 138 | * Ensure that the proper configuration is in place to start FPM. 139 | * 140 | * @return void 141 | */ 142 | protected function ensureReadyToStart() 143 | { 144 | if (! is_dir(dirname(self::SOCKET))) { 145 | mkdir(dirname(self::SOCKET)); 146 | } 147 | 148 | if (! file_exists(self::CONFIG)) { 149 | file_put_contents( 150 | self::CONFIG, 151 | file_get_contents(__DIR__.'/../../../stubs/php-fpm.conf') 152 | ); 153 | } 154 | } 155 | 156 | /** 157 | * Proxy the request to PHP-FPM and return its response. 158 | * 159 | * @param \Laravel\Vapor\Runtime\Fpm\FpmRequest $request 160 | * @return \Laravel\Vapor\Runtime\Fpm\FpmResponse 161 | */ 162 | public function handle($request) 163 | { 164 | return (new FpmApplication($this->client, $this->socketConnection)) 165 | ->handle($request); 166 | } 167 | 168 | /** 169 | * Wait until the FPM process is ready to receive requests. 170 | * 171 | * @return void 172 | */ 173 | protected function ensureFpmHasStarted() 174 | { 175 | $elapsed = 0; 176 | 177 | while (! $this->isReady()) { 178 | usleep(5000); 179 | 180 | $elapsed += 5000; 181 | 182 | if ($elapsed > ($fiveSeconds = 5000000)) { 183 | throw new Exception('Timed out waiting for FPM to start: '.self::SOCKET); 184 | } 185 | 186 | if (! $this->fpm->isRunning()) { 187 | throw new Exception('PHP-FPM was unable to start.'); 188 | } 189 | } 190 | } 191 | 192 | /** 193 | * Determine is the FPM process is ready to receive requests. 194 | * 195 | * @return bool 196 | */ 197 | protected function isReady() 198 | { 199 | clearstatcache(false, self::SOCKET); 200 | 201 | return file_exists(self::SOCKET); 202 | } 203 | 204 | /** 205 | * Ensure that the FPM process is still running. 206 | * 207 | * @return void 208 | * 209 | * @throws \Exception 210 | */ 211 | public function ensureRunning() 212 | { 213 | try { 214 | if (! $this->fpm || ! $this->fpm->isRunning()) { 215 | throw new Exception('PHP-FPM has stopped unexpectedly.'); 216 | } 217 | } catch (Throwable $e) { 218 | function_exists('__vapor_debug') && __vapor_debug($e->getMessage()); 219 | 220 | exit(1); 221 | } 222 | } 223 | 224 | /** 225 | * Stop the FPM process. 226 | * 227 | * @return void 228 | */ 229 | public function stop() 230 | { 231 | if ($this->fpm && $this->fpm->isRunning()) { 232 | $this->fpm->stop(); 233 | } 234 | } 235 | 236 | /** 237 | * Kill any existing FPM processes on the system. 238 | * 239 | * @return void 240 | */ 241 | protected function killExistingFpm() 242 | { 243 | function_exists('__vapor_debug') && __vapor_debug('Killing existing FPM'); 244 | 245 | if (! file_exists(static::PID_FILE)) { 246 | return unlink(static::SOCKET); 247 | } 248 | 249 | $pid = (int) file_get_contents(static::PID_FILE); 250 | 251 | if (posix_getpgid($pid) === false) { 252 | return $this->removeFpmProcessFiles(); 253 | } 254 | 255 | $result = posix_kill($pid, SIGTERM); 256 | 257 | if ($result === false) { 258 | return $this->removeFpmProcessFiles(); 259 | } 260 | 261 | $this->waitUntilStopped($pid); 262 | 263 | $this->removeFpmProcessFiles(); 264 | } 265 | 266 | /** 267 | * Remove FPM's process related files. 268 | * 269 | * @return void 270 | */ 271 | protected function removeFpmProcessFiles() 272 | { 273 | unlink(static::SOCKET); 274 | unlink(static::PID_FILE); 275 | } 276 | 277 | /** 278 | * Wait until the given process is stopped. 279 | * 280 | * @param int $pid 281 | * @return void 282 | */ 283 | protected function waitUntilStopped($pid) 284 | { 285 | $elapsed = 0; 286 | 287 | while (posix_getpgid($pid) !== false) { 288 | usleep(5000); 289 | 290 | $elapsed += 5000; 291 | 292 | if ($elapsed > 1000000) { 293 | throw new Exception('Process did not stop within the given threshold.'); 294 | } 295 | } 296 | } 297 | 298 | /** 299 | * Get the underlying process. 300 | * 301 | * @return \Symfony\Component\Process\Process 302 | */ 303 | public function process() 304 | { 305 | return $this->fpm; 306 | } 307 | 308 | /** 309 | * Get the handler. 310 | * 311 | * @return string 312 | */ 313 | public function handler() 314 | { 315 | return $this->handler; 316 | } 317 | 318 | /** 319 | * Get the server variables. 320 | * 321 | * @return array 322 | */ 323 | public function serverVariables() 324 | { 325 | return $this->serverVariables; 326 | } 327 | 328 | /** 329 | * Handle the destruction of the class. 330 | * 331 | * @return void 332 | */ 333 | public function __destruct() 334 | { 335 | $this->stop(); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/Runtime/Request.php: -------------------------------------------------------------------------------- 1 | body = $body; 41 | $this->serverVariables = $serverVariables; 42 | $this->headers = $headers; 43 | } 44 | 45 | /** 46 | * Create a new request from the given Lambda event. 47 | * 48 | * @param array $event 49 | * @param array $serverVariables 50 | * @param string|null $handler 51 | * @return static 52 | */ 53 | public static function fromLambdaEvent(array $event, array $serverVariables = [], $handler = null) 54 | { 55 | [$uri, $queryString] = static::getUriAndQueryString($event); 56 | 57 | $headers = static::getHeaders($event); 58 | 59 | $requestBody = static::getRequestBody($event); 60 | 61 | $serverVariables = array_merge($serverVariables, [ 62 | 'GATEWAY_INTERFACE' => 'FastCGI/1.0', 63 | 'PATH_INFO' => $event['path'] ?? $event['requestContext']['http']['path'] ?? '/', 64 | 'QUERY_STRING' => $queryString, 65 | 'REMOTE_ADDR' => '127.0.0.1', 66 | 'REMOTE_PORT' => $headers['x-forwarded-port'] ?? 80, 67 | 'REQUEST_METHOD' => $event['httpMethod'] ?? $event['requestContext']['http']['method'], 68 | 'REQUEST_URI' => $uri, 69 | 'REQUEST_TIME' => time(), 70 | 'REQUEST_TIME_FLOAT' => microtime(true), 71 | 'SERVER_ADDR' => '127.0.0.1', 72 | 'SERVER_NAME' => $headers['host'] ?? 'localhost', 73 | 'SERVER_PORT' => $headers['x-forwarded-port'] ?? 80, 74 | 'SERVER_PROTOCOL' => $event['requestContext']['protocol'] ?? $event['requestContext']['http']['protocol'] ?? 'HTTP/1.1', 75 | 'SERVER_SOFTWARE' => 'vapor', 76 | ]); 77 | 78 | if ($handler) { 79 | $serverVariables['SCRIPT_FILENAME'] = $handler; 80 | } 81 | 82 | if ($timestamp = self::extractRequestTimestamp($event)) { 83 | $serverVariables['AWS_API_GATEWAY_REQUEST_TIME'] = $timestamp; 84 | } 85 | 86 | [$headers, $serverVariables] = static::ensureContentTypeIsSet( 87 | $event, $headers, $serverVariables 88 | ); 89 | 90 | [$headers, $serverVariables] = static::ensureContentLengthIsSet( 91 | $event, $headers, $serverVariables, $requestBody 92 | ); 93 | 94 | $headers = static::ensureSourceIpAddressIsSet( 95 | $event, $headers 96 | ); 97 | 98 | foreach ($headers as $header => $value) { 99 | $serverVariables['HTTP_'.strtoupper(str_replace('-', '_', $header))] = $value; 100 | } 101 | 102 | return new static($serverVariables, $requestBody, $headers); 103 | } 104 | 105 | /** 106 | * Get the URI and query string for the given event. 107 | * 108 | * @param array $event 109 | * @return array 110 | */ 111 | protected static function getUriAndQueryString(array $event) 112 | { 113 | $uri = $event['requestContext']['http']['path'] ?? $event['path'] ?? '/'; 114 | 115 | $queryString = self::getQueryString($event); 116 | 117 | parse_str($queryString, $queryParameters); 118 | 119 | return [ 120 | empty($queryString) ? $uri : $uri.'?'.$queryString, 121 | http_build_query($queryParameters), 122 | ]; 123 | } 124 | 125 | /** 126 | * Get the query string from the event. 127 | * 128 | * @param array $event 129 | * @return string 130 | */ 131 | protected static function getQueryString(array $event) 132 | { 133 | if (isset($event['version']) && $event['version'] === '2.0') { 134 | return http_build_query( 135 | collect($event['queryStringParameters'] ?? []) 136 | ->mapWithKeys(function ($value, $key) { 137 | $values = explode(',', $value); 138 | 139 | return count($values) === 1 140 | ? [$key => $values[0]] 141 | : [(substr($key, -2) == '[]' ? substr($key, 0, -2) : $key) => $values]; 142 | })->all() 143 | ); 144 | } 145 | 146 | if (! isset($event['multiValueQueryStringParameters'])) { 147 | return http_build_query( 148 | $event['queryStringParameters'] ?? [] 149 | ); 150 | } 151 | 152 | return http_build_query( 153 | collect($event['multiValueQueryStringParameters'] ?? []) 154 | ->mapWithKeys(function ($values, $key) use ($event) { 155 | $key = ! isset($event['requestContext']['elb']) ? $key : urldecode($key); 156 | 157 | return count($values) === 1 158 | ? [$key => $values[0]] 159 | : [(substr($key, -2) == '[]' ? substr($key, 0, -2) : $key) => $values]; 160 | })->map(function ($values) use ($event) { 161 | if (! isset($event['requestContext']['elb'])) { 162 | return $values; 163 | } 164 | 165 | return ! is_array($values) ? urldecode($values) : array_map(function ($value) { 166 | return urldecode($value); 167 | }, $values); 168 | })->all() 169 | ); 170 | } 171 | 172 | /** 173 | * Get the request headers from the event. 174 | * 175 | * @param array $event 176 | * @return array 177 | */ 178 | protected static function getHeaders(array $event) 179 | { 180 | if (! isset($event['multiValueHeaders'])) { 181 | return array_change_key_case( 182 | $event['headers'] ?? [], CASE_LOWER 183 | ); 184 | } 185 | 186 | return array_change_key_case( 187 | collect($event['multiValueHeaders'] ?? []) 188 | ->mapWithKeys(function ($headers, $name) { 189 | return [$name => Arr::last($headers)]; 190 | })->all(), CASE_LOWER 191 | ); 192 | } 193 | 194 | /** 195 | * Get the request body from the event. 196 | * 197 | * @param array $event 198 | * @return string 199 | */ 200 | protected static function getRequestBody(array $event) 201 | { 202 | $body = $event['body'] ?? ''; 203 | 204 | return isset($event['isBase64Encoded']) && $event['isBase64Encoded'] 205 | ? base64_decode($body) 206 | : $body; 207 | } 208 | 209 | /** 210 | * Ensure the request headers / server variables contain a content type. 211 | * 212 | * @param array $event 213 | * @param array $headers 214 | * @param array $serverVariables 215 | * @return array 216 | */ 217 | protected static function ensureContentTypeIsSet(array $event, array $headers, array $serverVariables) 218 | { 219 | if ((! isset($headers['content-type']) && isset($event['httpMethod']) && (strtoupper($event['httpMethod']) === 'POST')) || 220 | (! isset($headers['content-type']) && isset($event['requestContext']['http']['method']) && (strtoupper($event['requestContext']['http']['method']) === 'POST'))) { 221 | $headers['content-type'] = 'application/x-www-form-urlencoded'; 222 | } 223 | 224 | if (isset($headers['content-type'])) { 225 | $serverVariables['CONTENT_TYPE'] = $headers['content-type']; 226 | } 227 | 228 | return [$headers, $serverVariables]; 229 | } 230 | 231 | /** 232 | * Ensure the request headers / server variables contain a content length. 233 | * 234 | * @param array $event 235 | * @param array $headers 236 | * @param array $serverVariables 237 | * @param string $requestBody 238 | * @return array 239 | */ 240 | protected static function ensureContentLengthIsSet(array $event, array $headers, array $serverVariables, $requestBody) 241 | { 242 | if ((! isset($headers['content-length']) && isset($event['httpMethod']) && ! in_array(strtoupper($event['httpMethod']), ['TRACE'])) || 243 | (! isset($headers['content-length']) && isset($event['requestContext']['http']['method']) && ! in_array(strtoupper($event['requestContext']['http']['method']), ['TRACE']))) { 244 | $headers['content-length'] = strlen($requestBody); 245 | } 246 | 247 | if (isset($headers['content-length'])) { 248 | $serverVariables['CONTENT_LENGTH'] = $headers['content-length']; 249 | } 250 | 251 | return [$headers, $serverVariables]; 252 | } 253 | 254 | /** 255 | * Ensure the request headers contain a source IP address. 256 | * 257 | * @param array $event 258 | * @param array $headers 259 | * @return array 260 | */ 261 | protected static function ensureSourceIpAddressIsSet(array $event, array $headers) 262 | { 263 | if (isset($event['requestContext']['identity']['sourceIp'])) { 264 | $headers['x-vapor-source-ip'] = $event['requestContext']['identity']['sourceIp']; 265 | } 266 | 267 | if (isset($event['requestContext']['http']['sourceIp'])) { 268 | $headers['x-vapor-source-ip'] = $event['requestContext']['http']['sourceIp']; 269 | } 270 | 271 | return $headers; 272 | } 273 | 274 | /** 275 | * Extracts the time (millisecond epoch) when the request was received by the API Gateway. 276 | * 277 | * @param array $event 278 | * @return int|null 279 | */ 280 | protected static function extractRequestTimestamp(array $event) 281 | { 282 | if (! isset($event['requestContext'])) { 283 | return null; 284 | } 285 | 286 | return $event['requestContext']['requestTimeEpoch'] // REST API (V1) 287 | ?? $event['requestContext']['timeEpoch'] // HTTP API (V2) 288 | ?? null; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/Runtime/Octane/Octane.php: -------------------------------------------------------------------------------- 1 | boot()->onRequestHandled(static::manageDatabaseSessions($databaseSessionPersist, $databaseSessionTtl)); 78 | 79 | if ($databaseSessionPersist && $databaseSessionTtl > 0) { 80 | static::worker()->application()->make('db')->beforeExecuting(function ($query, $bindings, $connection) { 81 | if ($connection instanceof MySqlConnection && ! in_array($connection->getName(), static::$databaseSessions)) { 82 | static::$databaseSessions[] = $connection->getName(); 83 | 84 | $connection->unprepared(sprintf( 85 | 'SET SESSION wait_timeout=%s', static::DB_SESSION_DEFAULT_TTL 86 | )); 87 | } 88 | }); 89 | } 90 | } 91 | 92 | /** 93 | * Manage the database sessions. 94 | * 95 | * @param bool $databaseSessionPersist 96 | * @param int $databaseSessionTtl 97 | * @return callable 98 | */ 99 | protected static function manageDatabaseSessions($databaseSessionPersist, $databaseSessionTtl) 100 | { 101 | return function ($request, $response, $sandbox) use ($databaseSessionPersist, $databaseSessionTtl) { 102 | if (! $sandbox->resolved('db') || ($databaseSessionPersist && $databaseSessionTtl == 0)) { 103 | return; 104 | } 105 | 106 | $connections = collect($sandbox->make('db')->getConnections()); 107 | 108 | if (! $databaseSessionPersist) { 109 | return $connections->each->disconnect(); 110 | } 111 | 112 | $connections->filter(function ($connection) { 113 | $hasSession = in_array($connection->getName(), static::$databaseSessions); 114 | 115 | if (! $hasSession) { 116 | try { 117 | $connection->disconnect(); 118 | } catch (Throwable $e) { 119 | // Likely already disconnected... 120 | } 121 | } 122 | 123 | return $hasSession; 124 | })->map->getRawPdo()->filter(function ($pdo) { 125 | return $pdo instanceof PDO; 126 | })->each->exec(sprintf( 127 | 'SET SESSION wait_timeout=%s', $databaseSessionTtl 128 | )); 129 | }; 130 | } 131 | 132 | /** 133 | * Handle the given Octane request. 134 | * 135 | * @param \Laravel\Octane\RequestContext $request 136 | * @return \Laravel\Vapor\Runtime\Response 137 | */ 138 | public static function handle($request) 139 | { 140 | [$request, $context] = (new self)->marshalRequest($request); 141 | 142 | static::$databaseSessions = []; 143 | 144 | self::worker()->application()->useStoragePath(StorageDirectories::PATH); 145 | 146 | if (HttpKernel::shouldSendMaintenanceModeResponse($request)) { 147 | if (isset($_ENV['VAPOR_MAINTENANCE_MODE_SECRET']) && 148 | $_ENV['VAPOR_MAINTENANCE_MODE_SECRET'] == $request->path()) { 149 | $response = HttpKernel::bypassResponse($_ENV['VAPOR_MAINTENANCE_MODE_SECRET']); 150 | } elseif (isset($_ENV['VAPOR_MAINTENANCE_MODE_SECRET']) && 151 | static::hasValidBypassCookie($request, $_ENV['VAPOR_MAINTENANCE_MODE_SECRET'])) { 152 | $response = static::sendRequest($request, $context); 153 | } elseif ($request->wantsJson() && file_exists($_ENV['LAMBDA_TASK_ROOT'].'/503.json')) { 154 | $response = JsonResponse::fromJsonString( 155 | file_get_contents($_ENV['LAMBDA_TASK_ROOT'].'/503.json'), 503 156 | ); 157 | } else { 158 | $response = new \Illuminate\Http\Response( 159 | file_get_contents($_ENV['LAMBDA_TASK_ROOT'].'/503.html'), 503 160 | ); 161 | } 162 | } else { 163 | $response = static::sendRequest($request, $context); 164 | } 165 | 166 | $content = $response instanceof BinaryFileResponse 167 | ? $response->getFile()->getContent() 168 | : $response->getContent(); 169 | 170 | if ($response instanceof StreamedResponse) { 171 | $content = static::captureContent($response); 172 | } 173 | 174 | return tap(new Response( 175 | $content, 176 | $response->headers->all(), 177 | $response->getStatusCode() 178 | ), static function () { 179 | static::$response = null; 180 | }); 181 | } 182 | 183 | /** 184 | * Send the request to the worker. 185 | * 186 | * @param \Illuminate\Http\Request $request 187 | * @param \Laravel\Octane\RequestContext $context 188 | * @return \Laravel\Octane\OctaneResponse 189 | */ 190 | protected static function sendRequest($request, $context) 191 | { 192 | return (new Pipeline)->send($request) 193 | ->through([ 194 | new EnsureOnNakedDomain, 195 | new RedirectStaticAssets, 196 | new EnsureVanityUrlIsNotIndexed, 197 | new EnsureBinaryEncoding(), 198 | ])->then(function ($request) use ($context) { 199 | static::$worker->handle($request, $context); 200 | 201 | return static::$response->response; 202 | }); 203 | } 204 | 205 | /** 206 | * Determine if the incoming request has a maintenance mode bypass cookie. 207 | * 208 | * @param \Illuminate\Http\Request $request 209 | * @param string $secret 210 | * @return bool 211 | */ 212 | public static function hasValidBypassCookie($request, $secret) 213 | { 214 | return HttpKernel::hasValidBypassCookie($request, $secret); 215 | } 216 | 217 | /** 218 | * Terminates an Octane worker instance, if any. 219 | * 220 | * @return void 221 | */ 222 | public static function terminate() 223 | { 224 | if (static::$worker) { 225 | static::$worker->terminate(); 226 | 227 | static::$worker = null; 228 | 229 | self::ensureServerSoftware(null); 230 | } 231 | } 232 | 233 | /** 234 | * Marshal the given Octane request context into an Laravel foundation request. 235 | */ 236 | public function marshalRequest(RequestContext $context): array 237 | { 238 | return [ 239 | static::toHttpFoundationRequest($context->psr7Request), 240 | $context, 241 | ]; 242 | } 243 | 244 | /** 245 | * Stores the response in the instance. 246 | */ 247 | public function respond(RequestContext $context, OctaneResponse $response): void 248 | { 249 | static::$response = $response; 250 | } 251 | 252 | /** 253 | * Send an error message to the server. 254 | */ 255 | public function error(Throwable $e, Application $app, Request $request, RequestContext $context): void 256 | { 257 | try { 258 | static::$response = new OctaneResponse( 259 | $app[ExceptionHandler::class]->render($request, $e) 260 | ); 261 | } catch (Throwable $throwable) { 262 | function_exists('__vapor_debug') && __vapor_debug($throwable->getMessage()); 263 | function_exists('__vapor_debug') && __vapor_debug($e->getMessage()); 264 | 265 | static::$response = new OctaneResponse( 266 | new \Illuminate\Http\Response('', 500) 267 | ); 268 | } 269 | } 270 | 271 | /** 272 | * Ensures the given software name is set globally. 273 | * 274 | * @param string|null $software 275 | * @return void 276 | */ 277 | protected static function ensureServerSoftware($software) 278 | { 279 | $_ENV['SERVER_SOFTWARE'] = $software; 280 | $_SERVER['SERVER_SOFTWARE'] = $software; 281 | } 282 | 283 | /** 284 | * Get the Octane worker, if any. 285 | * 286 | * @return \Laravel\Octane\Worker|null 287 | */ 288 | public static function worker() 289 | { 290 | return static::$worker; 291 | } 292 | 293 | /** 294 | * Capture the content from a streamed response. 295 | */ 296 | protected static function captureContent(StreamedResponse $response): string 297 | { 298 | ob_start(); 299 | $response->sendContent(); 300 | $content = ob_get_contents(); 301 | ob_end_clean(); 302 | 303 | return $content; 304 | } 305 | } 306 | --------------------------------------------------------------------------------