Skip to content

Commit

Permalink
Add SSL support to local start-api and start-lambda (#5902)
Browse files Browse the repository at this point in the history
* Add SSL support to local start-api and start-lambda

* Validate SSL options earlier; simplify SSLError handler

* Use click.Path(exists=True) for SSL file validation

* update tests & formatting

* Remove SSL options from start-lambda

---------

Co-authored-by: Mehmet Nuri Deveci <[email protected]>
Co-authored-by: Lucas <[email protected]>
  • Loading branch information
3 people authored Dec 14, 2023
1 parent 6fd7c4b commit 49e4d70
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 28 deletions.
17 changes: 13 additions & 4 deletions samcli/commands/local/lib/local_api_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class LocalApiService:
Lambda function.
"""

def __init__(self, lambda_invoke_context, port, host, static_dir, disable_authorizer):
def __init__(self, lambda_invoke_context, port, host, static_dir, disable_authorizer, ssl_context):
"""
Initialize the local API service.
Expand All @@ -28,11 +28,14 @@ def __init__(self, lambda_invoke_context, port, host, static_dir, disable_author
:param string host: Local hostname or IP address to bind to
:param string static_dir: Optional, directory from which static files will be mounted
:param bool disable_authorizer: Optional, flag for disabling the parsing of lambda authorizers
:param tuple(string, string) ssl_context: Optional, path to ssl certificate and key files to start service
in https
"""

self.port = port
self.host = host
self.static_dir = static_dir
self.ssl_context = ssl_context

self.cwd = lambda_invoke_context.get_cwd()
self.disable_authorizer = disable_authorizer
Expand Down Expand Up @@ -66,13 +69,14 @@ def start(self):
static_dir=static_dir_path,
port=self.port,
host=self.host,
ssl_context=self.ssl_context,
stderr=self.stderr_stream,
)

service.create()

# Print out the list of routes that will be mounted
self._print_routes(self.api_provider.api.routes, self.host, self.port)
self._print_routes(self.api_provider.api.routes, self.host, self.port, bool(self.ssl_context))
LOG.info(
"You can now browse to the above endpoints to invoke your functions. "
"You do not need to restart/reload SAM CLI while working on your functions, "
Expand All @@ -84,7 +88,7 @@ def start(self):
service.run()

@staticmethod
def _print_routes(routes, host, port):
def _print_routes(routes, host, port, ssl_enabled=False):
"""
Helper method to print the APIs that will be mounted. This method is purely for printing purposes.
This method takes in a list of Route Configurations and prints out the Routes grouped by path.
Expand All @@ -100,14 +104,19 @@ def _print_routes(routes, host, port):
Host name where the service is running
:param int port:
Port number where the service is running
:param bool ssl_enabled:
Boolean parameter to set whether SSL configuration is enabled
:returns list(string):
List of lines that were printed to the console. Helps with testing
"""

print_lines = []
protocol = "https" if ssl_enabled else "http"
for route in routes:
methods_str = "[{}]".format(", ".join(route.methods))
output = "Mounting {} at http://{}:{}{} {}".format(route.function_name, host, port, route.path, methods_str)
output = "Mounting {} at {}://{}:{}{} {}".format(
route.function_name, protocol, host, port, route.path, methods_str
)
print_lines.append(output)

LOG.info(output)
Expand Down
11 changes: 9 additions & 2 deletions samcli/commands/local/lib/local_lambda_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,21 @@ class LocalLambdaService:
that are defined in a SAM file.
"""

def __init__(self, lambda_invoke_context, port, host):
def __init__(self, lambda_invoke_context, port, host, ssl_context):
"""
Initialize the Local Lambda Invoke service.
:param samcli.commands.local.cli_common.invoke_context.InvokeContext lambda_invoke_context: Context object
that can help with Lambda invocation
:param int port: Port to listen on
:param string host: Local hostname or IP address to bind to
:param tuple(string, string) ssl_context: Optional, path to ssl certificate and key files to start service
in https
"""

self.port = port
self.host = host
self.ssl_context = ssl_context
self.lambda_runner = lambda_invoke_context.local_lambda_runner
self.stderr_stream = lambda_invoke_context.stderr

Expand All @@ -43,7 +46,11 @@ def start(self):
# to the console or a log file. stderr from Docker container contains runtime logs and output of print
# statements from the Lambda function
service = LocalLambdaInvokeService(
lambda_runner=self.lambda_runner, port=self.port, host=self.host, stderr=self.stderr_stream
lambda_runner=self.lambda_runner,
port=self.port,
host=self.host,
ssl_context=self.ssl_context,
stderr=self.stderr_stream,
)

service.create()
Expand Down
29 changes: 28 additions & 1 deletion samcli/commands/local/start_api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
"""

import logging
from ssl import SSLError

import click

from samcli.cli.cli_config_file import ConfigProvider, configuration_option, save_params_option
from samcli.cli.main import aws_creds_options, pass_context, print_cmdline_args
from samcli.cli.main import common_options as cli_framework_options
from samcli.commands._utils.click_mutex import ClickMutex
from samcli.commands._utils.option_value_processor import process_image_options
from samcli.commands._utils.options import (
generate_next_command_recommendation,
Expand Down Expand Up @@ -77,6 +79,22 @@
default=False,
help="Disable custom Lambda Authorizers from being parsed and invoked.",
)
@click.option(
"--ssl-cert-file",
default=None,
type=click.Path(exists=True),
cls=ClickMutex,
required_param_lists=[["ssl_key_file"]],
help="Path to SSL certificate file (default: None)",
)
@click.option(
"--ssl-key-file",
default=None,
type=click.Path(exists=True),
cls=ClickMutex,
required_param_lists=[["ssl_cert_file"]],
help="Path to SSL key file (default: None)",
)
@invoke_common_options
@warm_containers_common_options
@local_common_options
Expand Down Expand Up @@ -120,6 +138,8 @@ def cli(
hook_name,
skip_prepare_infra,
terraform_plan_file,
ssl_cert_file,
ssl_key_file,
):
"""
`sam local start-api` command entry point
Expand Down Expand Up @@ -152,6 +172,8 @@ def cli(
container_host_interface,
invoke_image,
hook_name,
ssl_cert_file,
ssl_key_file,
) # pragma: no cover


Expand Down Expand Up @@ -181,6 +203,8 @@ def do_cli( # pylint: disable=R0914
container_host_interface,
invoke_image,
hook_name,
ssl_cert_file,
ssl_key_file,
):
"""
Implementation of the ``cli`` method, just separated out for unit testing purposes
Expand Down Expand Up @@ -226,12 +250,14 @@ def do_cli( # pylint: disable=R0914
container_host_interface=container_host_interface,
invoke_images=processed_invoke_images,
) as invoke_context:
ssl_context = (ssl_cert_file, ssl_key_file) if ssl_cert_file else None
service = LocalApiService(
lambda_invoke_context=invoke_context,
port=port,
host=host,
static_dir=static_dir,
disable_authorizer=disable_authorizer,
ssl_context=ssl_context,
)
service.start()
if not hook_name:
Expand All @@ -243,7 +269,8 @@ def do_cli( # pylint: disable=R0914
]
)
click.secho(command_suggestions, fg="yellow")

except SSLError as ex:
raise UserException(f"SSL Error: {ex.strerror}", wrapped_from=ex.__class__.__name__) from ex
except NoApisDefined as ex:
raise UserException(
"Template does not have any APIs connected to Lambda functions", wrapped_from=ex.__class__.__name__
Expand Down
2 changes: 2 additions & 0 deletions samcli/commands/local/start_api/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
CONTAINER_OPTION_NAMES: List[str] = [
"host",
"port",
"ssl_cert_file",
"ssl_key_file",
"env_vars",
"container_env_vars",
"debug_port",
Expand Down
8 changes: 6 additions & 2 deletions samcli/local/apigw/local_apigw_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from datetime import datetime
from io import StringIO
from time import time
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple

from flask import Flask, Request, request
from werkzeug.datastructures import Headers
Expand Down Expand Up @@ -66,6 +66,7 @@ def __init__(
port: Optional[int] = None,
host: Optional[str] = None,
stderr: Optional[StreamWriter] = None,
ssl_context: Optional[Tuple[str, str]] = None,
):
"""
Creates an ApiGatewayService
Expand All @@ -84,10 +85,13 @@ def __init__(
host : str
Optional. host to start the service on
Defaults to '127.0.0.1
ssl_context : (str, str)
Optional. tuple(str, str) indicating the cert and key files to use to start in https mode
Defaults to None
stderr : samcli.lib.utils.stream_writer.StreamWriter
Optional stream writer where the stderr from Docker container should be written to
"""
super().__init__(lambda_runner.is_debugging(), port=port, host=host)
super().__init__(lambda_runner.is_debugging(), port=port, host=host, ssl_context=ssl_context)
self.api = api
self.lambda_runner = lambda_runner
self.static_dir = static_dir
Expand Down
7 changes: 5 additions & 2 deletions samcli/local/lambda_service/local_lambda_invoke_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def to_url(self, value):


class LocalLambdaInvokeService(BaseLocalService):
def __init__(self, lambda_runner, port, host, stderr=None):
def __init__(self, lambda_runner, port, host, stderr=None, ssl_context=None):
"""
Creates a Local Lambda Service that will only response to invoking a function
Expand All @@ -42,10 +42,13 @@ def __init__(self, lambda_runner, port, host, stderr=None):
Optional. port for the service to start listening on
host str
Optional. host to start the service on
ssl_context : (str, str)
Optional. tuple(str, str) indicating the cert and key files to use to start in https mode
Defaults to None
stderr io.BaseIO
Optional stream where the stderr from Docker container should be written to
"""
super().__init__(lambda_runner.is_debugging(), port=port, host=host)
super().__init__(lambda_runner.is_debugging(), port=port, host=host, ssl_context=ssl_context)
self.lambda_runner = lambda_runner
self.stderr = stderr

Expand Down
7 changes: 5 additions & 2 deletions samcli/local/services/base_local_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@


class BaseLocalService:
def __init__(self, is_debugging, port, host):
def __init__(self, is_debugging, port, host, ssl_context):
"""
Creates a BaseLocalService class
Expand All @@ -22,10 +22,13 @@ def __init__(self, is_debugging, port, host):
Optional. port for the service to start listening on Defaults to 3000
host str
Optional. host to start the service on Defaults to '127.0.0.1
ssl_context tuple(str, str)
Optional. path to ssl certificate and key files to start service in https
"""
self.is_debugging = is_debugging
self.port = port
self.host = host
self.ssl_context = ssl_context
self._app = None

def create(self):
Expand Down Expand Up @@ -62,7 +65,7 @@ def run(self):

flask.cli.show_server_banner = lambda *args: None

self._app.run(threaded=multi_threaded, host=self.host, port=self.port)
self._app.run(threaded=multi_threaded, host=self.host, port=self.port, ssl_context=self.ssl_context)

@staticmethod
def service_response(body, headers, status_code):
Expand Down
Loading

0 comments on commit 49e4d70

Please sign in to comment.