diff --git a/.gitignore b/.gitignore index 6160051..8fbac6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ php71.zip php73.zip +php71g.zip +php73g.zip diff --git a/Makefile b/Makefile index eee5fce..2b91afc 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) -all: php71.zip php73.zip +all: php71.zip php73.zip php71g.zip php73g.zip php71.zip: docker run --rm -e http_proxy=${http_proxy} -v $(ROOT_DIR):/opt/layer lambci/lambda:build-provided /opt/layer/build.sh @@ -8,18 +8,36 @@ php71.zip: php73.zip: docker run --rm -e http_proxy=${http_proxy} -v $(ROOT_DIR):/opt/layer lambci/lambda:build-provided /opt/layer/build-php-remi.sh 3 +php71g.zip: + docker run --rm -e GENERAL_EVENT=true -e http_proxy=${http_proxy} -v $(ROOT_DIR):/opt/layer lambci/lambda:build-provided /opt/layer/build.sh + +php73g.zip: + docker run --rm -e GENERAL_EVENT=true -e http_proxy=${http_proxy} -v $(ROOT_DIR):/opt/layer lambci/lambda:build-provided /opt/layer/build-php-remi.sh 3 + upload71: php71.zip ./upload.sh 7.1 upload73: php73.zip ./upload.sh 7.3 +upload71g: php71g.zip + ./upload.sh 7.1g + +upload73g: php73g.zip + ./upload.sh 7.3g + publish71: php71.zip ./publish.sh 7.1 publish73: php73.zip ./publish.sh 7.3 +publish71g: php71g.zip + ./publish.sh 7.1g + +publish73g: php73g.zip + ./publish.sh 7.3g + clean: - rm -f php71.zip php73.zip + rm -f php71.zip php73.zip php71g.zip php73g.zip diff --git a/bootstrap.generalenv b/bootstrap.generalenv new file mode 100755 index 0000000..2239385 --- /dev/null +++ b/bootstrap.generalenv @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +PHP_INI_SCAN_DIR=/opt/etc/php.d/:/var/task/etc/php.d/ /opt/bin/php -c /opt/php.ini -d extension_dir=/opt/lib/php/modules /opt/lib/runtime.php diff --git a/build-php-remi.sh b/build-php-remi.sh index 8d8fbef..8332d4a 100755 --- a/build-php-remi.sh +++ b/build-php-remi.sh @@ -41,5 +41,21 @@ cp /usr/lib64/libonig.so.5 lib/ mkdir -p lib/php/7.${PHP_MINOR_VERSION} cp -a /usr/lib64/php/modules lib/php/7.${PHP_MINOR_VERSION}/ -zip -r /opt/layer/php7${PHP_MINOR_VERSION}.zip . - +TARGET_NAME=php7${PHP_MINOR_VERSION} +if [ "${GENERAL_EVENT}" = "true" ]; then + TARGET_NAME=${TARGET_NAME}g + + php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" + php composer-setup.php + php -r "unlink('composer-setup.php');" + ./composer.phar global require aws/aws-sdk-php + ./composer.phar global clear-cache + cp -a /root/.composer lib/composer + cp /opt/layer/php.ini.generalenv php.ini + mv lib/php/7.${PHP_MINOR_VERSION}/* lib/php/ + rmdir lib/php/7.${PHP_MINOR_VERSION} + cp /opt/layer/bootstrap.generalenv bootstrap + cp /opt/layer/lib/*.php lib/ +fi + +zip -r /opt/layer/${TARGET_NAME}.zip . diff --git a/build.sh b/build.sh index 7233fd8..ab6be94 100755 --- a/build.sh +++ b/build.sh @@ -20,4 +20,21 @@ cp /usr/lib64/libpq.so.5 lib/ cp -a /usr/lib64/php lib/ -zip -r /opt/layer/php71.zip . \ No newline at end of file +TARGET_NAME=php71 +if [ "${GENERAL_EVENT}" = "true" ]; then + TARGET_NAME=${TARGET_NAME}g + + php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" + php composer-setup.php + php -r "unlink('composer-setup.php');" + ./composer.phar global require aws/aws-sdk-php + ./composer.phar global clear-cache + cp -a /root/.composer lib/composer + cp /opt/layer/php.ini.generalenv php.ini + mv lib/php/7.1/* lib/php/ + rmdir lib/php/7.1 + cp /opt/layer/bootstrap.generalenv bootstrap + cp /opt/layer/lib/*.php lib/ +fi + +zip -r /opt/layer/${TARGET_NAME}.zip . diff --git a/lib/LambdaContext.php b/lib/LambdaContext.php new file mode 100644 index 0000000..2421d5f --- /dev/null +++ b/lib/LambdaContext.php @@ -0,0 +1,38 @@ +$name; + } + + public function __construct($request) + { + $this->deadlineMs = (int)$request['Lambda-Runtime-Deadline-Ms']; + $this->awsRequestId = $request['Lambda-Runtime-Aws-Request-Id']; + $this->invokedFunctionArn = $request['Lambda-Runtime-Invoked-Function-Arn']; + $this->logGroupName = getenv('AWS_LAMBDA_LOG_GROUP_NAME'); + $this->logStreamName = getenv('AWS_LAMBDA_LOG_STREAM_NAME'); + $this->functionName = getenv("AWS_LAMBDA_FUNCTION_NAME"); + $this->memoryLimitInMb = getenv('AWS_LAMBDA_FUNCTION_MEMORY_SIZE'); + $this->functionVersion = getenv('AWS_LAMBDA_FUNCTION_VERSION'); + if (isset($request['Lambda-Runtime-Cognito-Identity'])) { + $this->identity = json_decode($request['Lambda-Runtime-Cognito-Identity']); + } + if (isset($request['Lambda-Runtime-Client-Context'])) { + $this->clientContext = json_decode($request['Lambda-Runtime-Client-Context']); + } + } +} diff --git a/lib/LambdaErrors.php b/lib/LambdaErrors.php new file mode 100644 index 0000000..4173b18 --- /dev/null +++ b/lib/LambdaErrors.php @@ -0,0 +1,96 @@ +errorClass = get_class($originalError); + $this->errorType = "{$classification}<{$this->errorClass}>"; + $file = $originalError->getFile(); + $line = $originalError->getLine(); + $message = $originalError->getMessage(); + $this->errorMessage = "{$file}({$line}): {$message}"; + $this->stackTrace = $this->_sanitize_stacktrace($originalError->getTraceAsString()); + parent::__construct($this->errorMessage); + } + + public function toLambdaResponse() + { + return [ + 'errorMessage' => $this->errorMessage, + 'errorType' => $this->errorType, + 'stackTrace' => $this->stackTrace, + ]; + } + + public function runtimeErrorType() + { + $classification = 'Function'; + if ($this->_allowedError()) { + $classification = $this->errorType; + } + return $classification; + } + + private function _sanitize_stacktrace($stacktrace) + { + $ret = []; + $safeTrace = true; + foreach (array_slice(explode(PHP_EOL, $stacktrace), 0, 100) as $trace) { + if ($safeTrace) { + [$no, $file] = explode(' ', $trace); + if (preg_match('@^/opt/lib/@', $file) === 1) { + $safeTrace = false; + } else { + $ret[] = $trace; + } + } + } + return $ret; + } + + private function _allowedError() + { + return $this->_standardError(); + } + + private function _standardError() + { + return true; // @Todo: To determine standard exception classes. + } +} + +class LambdaHandlerError extends LambdaError +{ +} + +class LambdaHandlerCriticalException extends LambdaError +{ +} + +class LambdaRuntimeError extends LambdaError +{ + public function __construct($originalError) + { + parent::__construct($originalError, 'Runtime'); + } +} + +class LambdaRuntimeInitError extends LambdaError +{ + public function __construct($originalError) + { + parent::__construct($originalError, 'Init'); + } +} diff --git a/lib/LambdaHandler.php b/lib/LambdaHandler.php new file mode 100644 index 0000000..8936981 --- /dev/null +++ b/lib/LambdaHandler.php @@ -0,0 +1,46 @@ +$name; + } else { + throw new Exception("Cannot access private property {$name}"); + } + } + + public function __construct($envHandler) + { + $handlerSplit = explode('.', $envHandler); + if (count($handlerSplit) == 2) { + [$this->handlerFileName, $this->handlerMethodName] = $handlerSplit; + } elseif (count($handlerSplit) == 3) { + [$this->handlerFileName, $this->handlerClass, $this->handlerMethodName] = $handlerSplit; + } else { + throw new Exception("Invalid handler {$handlerSplit}, must be of form FILENAME.METHOD or FILENAME.CLASS.METHOD where FILENAME corresponds with an existing PHP source file FILENAME.php, CLASS is an optional module/class namespace and METHOD is a callable method. If using CLASS, METHOD must be a static method."); + } + } + + public function callHandler($request, $context) + { + try { + if ($this->handlerClass) { + $fun = "{$this->handlerClass}::{$this->handlerMethodName}"; + } else { + $fun = $this->handlerMethodName; + } + $response = call_user_func($fun, $request, $context); + return LambdaMarshaller::marshallResponse($response); + } catch (Error $e) { + throw new LambdaErrors\LambdaHandlerCriticalException($e); + } catch (Exception $e) { + throw new LambdaErrors\LambdaHandlerError($e); + } + } +} diff --git a/lib/LambdaLogger.php b/lib/LambdaLogger.php new file mode 100644 index 0000000..cb9dfbd --- /dev/null +++ b/lib/LambdaLogger.php @@ -0,0 +1,14 @@ +toLambdaResponse(), JSON_PRETTY_PRINT)); + } +} diff --git a/lib/LambdaMarshaller.php b/lib/LambdaMarshaller.php new file mode 100644 index 0000000..37bf047 --- /dev/null +++ b/lib/LambdaMarshaller.php @@ -0,0 +1,31 @@ +getHeader('Content-Type')[0]; + if ($contentType == 'application/json') { + return json_decode($rawRequest->getBody()->getContents()); + } else { + return $rawRequest->getBody()->getContents(); # return it unaltered + } + } + + # By default, just runs #to_json on the method's response value. + # This can be overwritten by users who know what they are doing. + # The response is an array of response, content-type. + # If returned without a content-type, it is assumed to be application/json + # Finally, StringIO/IO is used to signal a response that shouldn't be + # formatted as JSON, and should get a different content-type header. + public static function marshallResponse($methodResponse) + { + if (is_resource($methodResponse) && get_resource_type($methodResponse) == 'stream') { + return [$methodResponse, 'application/unknown']; + } else { + return [json_encode($methodResponse, true), 'application/json']; + } + } +} diff --git a/lib/LambdaServer.php b/lib/LambdaServer.php new file mode 100644 index 0000000..5c6353d --- /dev/null +++ b/lib/LambdaServer.php @@ -0,0 +1,83 @@ +http = new GuzzleHttp\Client([ + 'base_uri' => $serverAddress, + ]); + } + + public function nextInvocation() + { + $path = "/2018-06-01/runtime/invocation/next"; + try { + $response = $this->http->request('GET', $path, [ + 'timeout' => self::LONG_TIMEOUT, + ]); + $status = $response->getStatusCode(); + if ($status == 200) { + return $response; + } else { + throw new Exception("Received {$status} when waiting for next invocation."); + } + } catch (Exception $e) { + throw new LambdaErrors\InvocationError($e); + } + } + + public function sendResponse($requestId, $responseObject, $contentType = 'application/json') + { + $path = "/2018-06-01/runtime/invocation/{$requestId}/response"; + try { + if ($contentType == 'application/unkown') { + $responseObject = stream_get_contents($responseObject); + } + $this->http->request('POST', $path, [ + 'body' => $responseObject, + 'headers' => [ + 'Content-Type' => $contentType, + ], + ]); + } catch (Exception $e) { + throw new LambdaErrors\LambdaRuntimeError($e); + } + } + + public function sendErrorResponse($requestId, $error) + { + $path = "/2018-06-01/runtime/invocation/{$requestId}/error"; + try { + $this->http->request('POST', $path, [ + 'body' => json_encode($error->toLambdaResponse(), true), + 'headers' => [ + 'Lambda-Runtime-Function-Error-Type' => $error->runtimeErrorType(), + ], + ]); + } catch (Exception $e) { + throw new LambdaErrors\LambdaRuntimeError($e); + } + } + + public function sendInitError($error) + { + $path = '/2018-06-01/runtime/init/error'; + try { + $this->http->request('POST', $path, [ + 'body' => json_encode($error->toLambdaResponse(), true), + 'headers' => [ + 'Lambda-Runtime-Function-Error-Type' => $error->runtimeErrorType(), + ], + ]); + } catch (Exception $e) { + throw new LambdaErrors\LambdaRuntimeInitError($e); + } + } +} diff --git a/lib/runtime.php b/lib/runtime.php new file mode 100644 index 0000000..15da0ff --- /dev/null +++ b/lib/runtime.php @@ -0,0 +1,63 @@ +handlerFileName}.php"; +} catch (Throwable $t) { + $runtimeLoopActive = false; + $exitCode = -4; + $e = new LambdaErrors\LambdaRuntimeInitError($t); + LambdaLogger::logError($e, "Init error when loading handler {$envHandler}"); + $lambdaServer->sendInitError($e); +} + +while ($runtimeLoopActive) { + try { + $rawRequest = $lambdaServer->nextInvocation(); + $headers = array_map(function($val){return $val[0];}, $rawRequest->getHeaders()); + if (isset($headers['Lambda-Runtime-Trace-Id'])) { + putenv('_X_AMZN_TRACE_ID=' . $headers['Lambda-Runtime-Trace-Id']); + } + $request = LambdaMarshaller::marshallRequest($rawRequest); + } catch (LambdaErrors\InvocationError $e) { + $runtimeLoopActive = false; + throw $e; + } + + try { + $requestId = $headers['Lambda-Runtime-Aws-Request-Id']; + $context = new LambdaContext($headers); + [$handlerResponse, $contentType] = $lambdaHandler->callHandler($request, $context); + $lambdaServer->sendResponse($requestId, $handlerResponse, $contentType); + } catch (LambdaErrors\LambdaHandlerError $e) { + LambdaLogger::logError($e, "Error raised from handler method"); + $lambdaServer->sendErrorResponse($requestId, $e); + } catch (LambdaErrors\LambdaHandlerCriticalException $e) { + LambdaLogger::logError($e, "Critical exception from handler"); + $lambdaServer->sendErrorResponse($requestId, $e); + $runtimeLoopActive = false; + $exitCode = -1; + } catch (LambdaErrors\LambdaRuntimeError $e) { + $lambdaServer->sendErrorResponse($requestId, $e); + $runtimeLoopActive = false; + $exitCode = -2; + } +} + +exit($exitCode); diff --git a/php.ini.generalenv b/php.ini.generalenv new file mode 100644 index 0000000..a4b722d --- /dev/null +++ b/php.ini.generalenv @@ -0,0 +1,7 @@ +extension_dir=/opt/lib/php/modules +display_errors=On + +extension=curl.so +extension=json.so +extension=mbstring.so +extension=zip.so