Merge pull request #2807 from derekpierre/porter-nginx

Porter: Nginx, CORS support
pull/2811/head
Derek Pierre 2021-10-14 11:14:43 -04:00 committed by GitHub
commit a161480277
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 428 additions and 73 deletions

View File

@ -462,7 +462,7 @@ commands:
- run: sudo chown -R circleci:circleci /usr/local/bin
- run: sudo chown -R circleci:circleci /usr/local/lib/python3.7/site-packages
- save_cache:
key: pip-v8-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
key: pip-v9-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
paths:
- "~/.local/bin"
- "~/.local/lib/python3.7/site-packages"
@ -477,7 +477,7 @@ commands:
- run: sudo chown -R circleci:circleci /usr/local/bin
- run: sudo chown -R circleci:circleci /usr/local/lib/python3.7/site-packages
- restore_cache: # ensure this step occurs *before* installing dependencies
key: pip-v8-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
key: pip-v9-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
- restore_cache:
key: solc-v2-{{ checksum "nucypher/blockchain/eth/sol/__conf__.py" }}
@ -869,7 +869,7 @@ jobs:
- setup_remote_docker
- restore_cache:
keys:
- v2-{{ .Branch }}-{{ arch }}
- v3-{{ .Branch }}-{{ arch }}
paths:
- ~/docker/nucypher-porter.tar
- run:
@ -887,7 +887,7 @@ jobs:
mkdir -p ~/docker
docker save -o ~/docker/nucypher-porter.tar nucypher/porter:circle
- save_cache:
key: v2-{{ .Branch }}-{{ arch }}
key: v3-{{ .Branch }}-{{ arch }}
paths:
- ~/docker/nucypher-porter.tar
@ -977,7 +977,7 @@ jobs:
- setup_remote_docker
- restore_cache:
keys:
- v2-{{ .Branch }}-{{ arch }}
- v3-{{ .Branch }}-{{ arch }}
paths:
- ~/docker/nucypher-porter.tar
- run:

View File

@ -16,7 +16,8 @@ services:
- ~/.local/share/nucypher:/nucypher
command: ["nucypher", "porter", "run",
"--provider", "${WEB3_PROVIDER_URI}",
"--network", "${NUCYPHER_NETWORK}"]
"--network", "${NUCYPHER_NETWORK}",
"--allow-origins", "${PORTER_CORS_ALLOW_ORIGINS}"] # empty string if env var not defined which translates to CORS not enabled by default
porter-https:
restart: on-failure
@ -33,7 +34,8 @@ services:
"--provider", "${WEB3_PROVIDER_URI}",
"--network", "${NUCYPHER_NETWORK}",
"--tls-key-filepath", "/etc/porter/tls/key.pem",
"--tls-certificate-filepath", "/etc/porter/tls/cert.pem"]
"--tls-certificate-filepath", "/etc/porter/tls/cert.pem",
"--allow-origins", "${PORTER_CORS_ALLOW_ORIGINS}"] # empty string if env var not defined which translates to CORS not enabled by default
porter-https-auth:
restart: on-failure
@ -52,4 +54,5 @@ services:
"--network", "${NUCYPHER_NETWORK}",
"--tls-key-filepath", "/etc/porter/tls/key.pem",
"--tls-certificate-filepath", "/etc/porter/tls/cert.pem",
"--basic-auth-filepath", "/etc/porter/auth/htpasswd"]
"--basic-auth-filepath", "/etc/porter/auth/htpasswd",
"--allow-origins", "${PORTER_CORS_ALLOW_ORIGINS}"] # empty string if env var not defined which translates to CORS not enabled by default

View File

@ -0,0 +1,4 @@
FROM nginxproxy/nginx-proxy:alpine
# Copy porter.local virtual host location configuration file
COPY ./deploy/docker/porter/nginx/porter.local_location /etc/nginx/vhost.d/

View File

@ -0,0 +1,37 @@
version: '3'
services:
nginx-proxy:
restart: always
image: nginxproxy/nginx-proxy:alpine
build:
context: ../../../..
dockerfile: deploy/docker/porter/nginx/Dockerfile
ports:
- "443:443"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
# because of the vhost name used below, the cert and key should be named "porter.local.crt" and "porter.local.key" respectively
- "${TLS_DIR}:/etc/nginx/certs/"
nginx-porter:
restart: on-failure
image: porter:latest
build:
context: ../../../..
dockerfile: deploy/docker/porter/Dockerfile
expose:
# Default Porter port
- "9155"
volumes:
- .:/code
- ~/.local/share/nucypher:/nucypher
command: [ "nucypher", "porter", "run",
"--provider", "${WEB3_PROVIDER_URI}",
"--network", "${NUCYPHER_NETWORK}" ]
environment:
- VIRTUAL_HOST=porter.local
- VIRTUAL_PORT=9155
depends_on:
- nginx-proxy

View File

@ -0,0 +1,27 @@
set $allow_origin "";
#
# Allow CORS for any domain by default - comment out if not desired
#
if ($http_origin ~* (.*)) {
set $allow_origin "true";
}
#
# Allow CORS for specific domain. For specifying conditions, see https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#if.
# Uncomment and edit if desired. There can be one or more of these 'if' directives for various origin checks.
#
#if ($http_origin ~* (.*\.yourdomain\.com$)) {
# set $allow_origin "true";
#}
#
# For multiple top-level domains:
#
#if ($http_origin ~* (.*\.yourdomain\.(com|org)$)) {
# set $allow_origin "true";
#}
if ($allow_origin = "true") {
add_header 'Access-Control-Allow-Origin' '$http_origin';
}

View File

@ -21,6 +21,16 @@ web and mobile experience is accessible to application developers.
Running Porter
--------------
There are a variety of possible infrastructure setups for running the Porter service, and two scenarios for running
the Porter service are provided here:
#. Run the Porter service directly via docker, docker-compose, or the CLI (see `Run Porter Directly`_)
#. Run the Porter service with a reverse proxy via docker-compose (see `Run Porter with Reverse Proxy`_)
Run Porter Directly
*******************
.. note::
If running the Porter service using Docker or Docker Compose, it will run on port 80 (HTTP) or 443 (HTTPS). If
@ -29,32 +39,33 @@ Running Porter
Security
^^^^^^^^
HTTPS
+++++
To run the Porter service over HTTPS, it will require a TLS key (``--tls-key-filepath`` option) and a TLS certificate.
* **HTTPS:** To run the Porter service over HTTPS, it will require a TLS key and a TLS certificate. These can be
specified via the `` --tls-key-filepath`` and ``--tls-certificate-filepath`` CLI options or via the ``TLS_DIR``
environment variable for docker-compose.
* **CORS:** Allowed origins for `Cross-Origin Resource Sharing (CORS) <https://en.wikipedia.org/wiki/Cross-origin_resource_sharing>`_
is not enabled by default and can be enabled either via the ``--allow-origins`` option for the CLI,
or the ``PORTER_CORS_ALLOW_ORIGINS`` environment variable for docker-compose.
If desired, keys and self-signed certificates can be created for the localhost using the ``openssl`` command:
The value is expected to be a comma-delimited list of strings/regular expressions for origins to allow requests from. To allow all origins,
simply use "*".
.. code:: bash
.. note::
$ openssl req -x509 -out cert.pem -keyout key.pem \
-newkey rsa:2048 -nodes -sha256 \
-subj '/CN=localhost' -extensions EXT -config <( \
printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
Origin values can be a string (for exact matches) or regular expressions (for more complex matches).
.. important::
As part of CORS, the scheme (``https`` or ``http``) is also checked, so using only ``example.com`` is incorrect
to allow an origin from that specific domain. For exact matches, you can use ``https://example.com`` for HTTPS or
``http://example.com`` for HTTP. For non-default ports (i.e. not 443 or 80), the ports should be specified
e.g. ``https://example.com:8000`` or ``http://example.com:8001``.
Self-signed certificates are not recommended, other than for testing.
For regular expressions, to allow all sub-domains of ``example.com``, you could use ``.*\.example\.com$`` which
incorporates wildcards for scheme and sub-domain. To allow multiple top-level domains you could use
``.*\.example\.(com|org)$`` which allows any origins from both ``example.com`` and ``example.org`` domains.
Authentication
++++++++++++++
Porter will allow the configuration of Basic Authentication out of the box via
an `htpasswd <https://httpd.apache.org/docs/2.4/programs/htpasswd.html>`_ file. The use of Basic Authentication
necessitates HTTPS since user credentials will be passed over the network as cleartext.
Alternative authentication mechanisms can be implemented outside of Porter via an intermediary proxy service, for
example an Nginx HTTPS reverse proxy.
* **Authentication:** Porter will allow the configuration of Basic Authentication out of the box via
an `htpasswd <https://httpd.apache.org/docs/2.4/programs/htpasswd.html>`_ file. This file can be provided via the
``--basic-auth-filepath`` CLI option or ``HTPASSWD_FILE`` environment variable for docker-compose. The use
of Basic Authentication necessitates HTTPS since user credentials will be passed over the network as cleartext.
via Docker
@ -101,6 +112,23 @@ Run Porter within Docker without acquiring or installing the ``nucypher`` codeba
--tls-key-filepath /etc/porter/tls/<KEY FILENAME> \
--tls-certificate-filepath /etc/porter/tls/<CERT FILENAME>
* Without Basic Authentication, but with CORS enabled to allow all origins:
.. code:: bash
$ docker run -d --rm \
--name porter-https-cors \
-v ~/.local/share/nucypher/:/root/.local/share/nucypher \
-v <TLS DIRECTORY>:/etc/porter/tls \
-p 443:9155 \
nucypher/porter:latest \
nucypher porter run \
--provider <YOUR WEB3 PROVIDER URI> \
--network <NETWORK NAME> \
--tls-key-filepath /etc/porter/tls/<KEY FILENAME> \
--tls-certificate-filepath /etc/porter/tls/<CERT FILENAME> \
--allow-origins "*"
* With Basic Authentication:
.. code:: bash
@ -119,17 +147,23 @@ Run Porter within Docker without acquiring or installing the ``nucypher`` codeba
--tls-certificate-filepath /etc/porter/tls/<CERT FILENAME> \
--basic-auth-filepath /etc/porter/auth/htpasswd
The ``<TLS DIRECTORY>`` is expected to contain the TLS key file (``<KEY FILENAME>``) and the
certificate (``<CERT FILENAME>``) to run Porter over HTTPS.
.. note::
The commands above are for illustrative purposes and can be modified as necessary.
#. Porter will be available on default ports 80 (HTTP) or 443 (HTTPS). The porter service running will be one of
the following depending on the mode chosen:
* ``porter-http``
* ``porter-https``
* ``porter-https-cors``
* ``porter-https-auth``
#. View Porter logs
.. code:: bash
@ -148,7 +182,7 @@ via Docker Compose
Docker Compose will start the Porter service within a Docker container.
#. Acquire the ``nucypher`` codebase - see :ref:`acquire_codebase`. There is no need
#. :ref:`acquire_codebase`. There is no need
to install ``nucypher`` after acquiring the codebase since Docker will be used.
#. Set the required environment variables:
@ -170,7 +204,8 @@ Docker Compose will start the Porter service within a Docker container.
$ export NUCYPHER_NETWORK=<NETWORK NAME>
* *(Optional)* TLS directory variable containing the TLS key and the certificate to run Porter over HTTPS. The directory is expected to contain two files:
* *(Optional)* TLS directory containing the TLS key and certificate to run Porter over HTTPS.
The directory is expected to contain two files:
* ``key.pem`` - the TLS key
* ``cert.pem`` - the TLS certificate
@ -181,6 +216,12 @@ Docker Compose will start the Porter service within a Docker container.
$ export TLS_DIR=<ABSOLUTE PATH TO TLS DIRECTORY>
* *(Optional)* Enable CORS. For example, to only allow access from your sub-domains for ``example.com``:
.. code:: bash
$ export PORTER_CORS_ALLOW_ORIGINS=".*\.example\.com$"
* *(Optional)* Filepath to the htpasswd file for Basic Authentication
Set the htpasswd filepath environment variable
@ -211,13 +252,15 @@ Docker Compose will start the Porter service within a Docker container.
$ docker-compose -f deploy/docker/porter/docker-compose.yml up -d porter-https-auth
#. Porter will be available on default ports 80 (HTTP) or 443 (HTTPS). The porter service running will be one of
Porter will be available on default ports 80 (HTTP) or 443 (HTTPS). The porter service running will be one of
the following depending on the mode chosen:
* ``porter-http``
* ``porter-https``
* ``porter-https-auth``
#. View Porter logs
.. code:: bash
@ -261,7 +304,6 @@ For a full list of CLI options, run:
the Pipe for nucypher network operations
Reading Latest Chaindata...
Network: <NETWORK NAME>
Provider: ...
Running Porter Web Controller at http://127.0.0.1:9155
@ -284,16 +326,15 @@ For a full list of CLI options, run:
the Pipe for nucypher network operations
Reading Latest Chaindata...
Network: <NETWORK NAME>
Provider: ...
Running Porter Web Controller at https://127.0.0.1:9155
For HTTPS with Basic Authentication, add the ``--basic-auth-filepath`` option:
To enable CORS, use the ``--allow-origins`` option:
.. code:: console
$ nucypher porter run --provider <YOUR WEB3 PROVIDER URI> --network <NETWORK NAME> --tls-key-filepath <TLS KEY FILEPATH> --tls-certificate-filepath <CERT FILEPATH> --basic-auth-filepath <HTPASSWD FILE>
$ nucypher porter run --provider <YOUR WEB3 PROVIDER URI> --network <NETWORK NAME> --tls-key-filepath <TLS KEY FILEPATH> --tls-certificate-filepath <CERT FILEPATH> --allow-origins ".*\.example\.com$"
______
@ -305,18 +346,139 @@ For a full list of CLI options, run:
the Pipe for nucypher network operations
Reading Latest Chaindata...
Network: <NETWORK NAME>
Provider: ...
CORS Allow Origins: ['.*\\.example\\.com$']
Running Porter Web Controller at https://127.0.0.1:9155
To enable Basic Authentication, add the ``--basic-auth-filepath`` option:
.. code:: console
$ nucypher porter run --provider <YOUR WEB3 PROVIDER URI> --network <NETWORK NAME> --tls-key-filepath <TLS KEY FILEPATH> --tls-certificate-filepath <CERT FILEPATH> --allow-origins ".*\.example\.com$" --basic-auth-filepath <HTPASSWD FILE>
______
(_____ \ _
_____) )__ ____| |_ ____ ____
| ____/ _ \ / ___) _)/ _ )/ ___)
| | | |_| | | | |_( (/ /| |
|_| \___/|_| \___)____)_|
the Pipe for nucypher network operations
Network: <NETWORK NAME>
Provider: ...
CORS Allow Origins: ['.*\\.example\\.com$']
Basic Authentication enabled
Running Porter Web Controller at https://127.0.0.1:9155
Run Porter with Reverse Proxy
*****************************
This type of Porter execution illustrates the use of a reverse proxy that is a go between or intermediate server that
handles requests from clients to an internal Porter service. An NGINX reverse proxy instance is
used in this case. It will handle functionality such as TLS, CORS, and authentication so that the Porter service
itself does not have to, and allows for more complex configurations than provided by Porter itself. More information
about the NGINX reverse proxy docker image used and additional configuration options
is available `here <https://hub.docker.com/r/nginxproxy/nginx-proxy>`_.
via Docker Compose
^^^^^^^^^^^^^^^^^^
Docker Compose will be used to start the NGINX reverse proxy and the Porter service containers.
#. :ref:`acquire_codebase`. There is no need
to install ``nucypher`` after acquiring the codebase since Docker will be used.
#. Set the required environment variables:
* Web3 Provider URI environment variable
.. code:: bash
$ export WEB3_PROVIDER_URI=<YOUR WEB3 PROVIDER URI>
.. note::
Local ipc is not supported when running via Docker.
* Network Name environment variable
.. code:: bash
$ export NUCYPHER_NETWORK=<NETWORK NAME>
* The reverse proxy is set up to run over HTTPS by default, and therefore requires a TLS directory containing
the TLS key and certificate for the reverse proxy. The directory is expected to contain two files:
* ``porter.local.key`` - the TLS key
* ``porter.local.crt`` - the TLS certificate
Set the TLS directory environment variable
.. code:: bash
$ export TLS_DIR=<ABSOLUTE PATH TO TLS DIRECTORY>
* *(Optional)* The CORS configuration is set in the ``nucypher/deploy/docker/porter/nginx/porter.local_location`` file.
.. important::
By default, CORS for the reverse proxy is configured to allow all origins
If you would like to modify the CORS allowed origin setting to be more specific, you can modify the file to
check for specific domains. There are some examples in the file - see `NGINX if-directive <https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#if>`_
for adding ore complex conditional checks.
For example, to only allow requests from all sub-domains of ``example.com``, the file should be edited to include:
.. code::
if ($http_origin ~* (.*\.example\.com$)) {
set $allow_origin "true";
}
.. note::
If you modify the file you should rebuild the docker images using docker-compose.
#. *(Optional)* Build the docker images:
.. code:: bash
$ docker-compose -f deploy/docker/porter/nginx/docker-compose.yml build
#. Run the NGINX reverse proxy and Porter service
.. code:: bash
$ docker-compose -f deploy/docker/porter/nginx/docker-compose.yml up -d
#. The NGINX reverse proxy will be publicly accessible via the default HTTPS port 443, and will route requests to the
internal Porter service.
#. View Porter service logs
.. code:: bash
$ docker-compose -f deploy/docker/porter/nginx/docker-compose.yml logs -f nginx-porter
#. Stop Porter service and NGINX reverse proxy
.. code:: bash
$ docker-compose -f deploy/docker/porter/nginx/docker-compose.yml down
API
---
Status Codes
^^^^^^^^^^^^
************
All documented API endpoints use JSON and are REST-like.
Some common returned status codes you may encounter are:
@ -346,7 +508,7 @@ GitHub issue. For any questions, message us in our `Discord <https://discord.gg/
URL Query Parameters
^^^^^^^^^^^^^^^^^^^^
********************
All parameters can be passed as either JSON data within the request or as query parameter strings in the URL.
Query parameters used within the URL will need to be URL encoded e.g. ``/`` in a base64 string becomes ``%2F`` etc.
@ -374,12 +536,12 @@ More examples shown below.
GET /get_ursulas
^^^^^^^^^^^^^^^^
****************
Sample available Ursulas for a policy as part of Alice's ``grant`` workflow. Returns a list of Ursulas
and their associated information that is used for the policy.
Parameters
++++++++++
^^^^^^^^^^
+----------------------------------+---------------+-----------------------------------------------+
| **Parameter** | **Type** | **Description** |
+==================================+===============+===============================================+
@ -398,7 +560,7 @@ Parameters
Returns
+++++++
^^^^^^^
List of Ursulas with associated information:
* ``encrypting_key`` - Ursula's encrypting key encoded as hex
@ -406,7 +568,7 @@ List of Ursulas with associated information:
* ``uri`` - Ursula's URI
Example Request
+++++++++++++++
^^^^^^^^^^^^^^^
.. code:: bash
curl -X GET <PORTER URI>/get_ursulas \
@ -424,7 +586,7 @@ OR
Example Response
++++++++++++++++
^^^^^^^^^^^^^^^^
.. code::
Status: 200 OK
@ -467,11 +629,11 @@ Example Response
POST /retrieve_cfrags
^^^^^^^^^^^^^^^^^^^^^
*********************
Get data re-encrypted by the network as part of Bob's ``retrieve`` workflow.
Parameters
++++++++++
^^^^^^^^^^
+-------------------------------------------+---------------+----------------------------------------+
| **Parameter** | **Type** | **Description** |
+===========================================+===============+========================================+
@ -515,7 +677,7 @@ Parameters
retry functionality that skips previously successful reencryption operations.
Returns
+++++++
^^^^^^^
The result of the re-encryption operations performed:
* ``retrieval_results`` - The list of results from the re-encryption operations performed; contains a mapping of
@ -524,7 +686,7 @@ The result of the re-encryption operations performed:
*retrieval kit*, the corresponding list of cfrags could be empty or less than the expected threshold.
Example Request
+++++++++++++++
^^^^^^^^^^^^^^^
.. code:: bash
curl -X POST <PORTER URI>/retrieve_cfrags \
@ -544,7 +706,7 @@ OR
Example Response
++++++++++++++++
^^^^^^^^^^^^^^^^
.. code::
Status: 200 OK

View File

@ -0,0 +1 @@
Fixed WebController bug caused by Path object for TLS/certificate path provided to Hendrix instead of a string.

View File

@ -0,0 +1,3 @@
CORS, NGINX support for Porter:
- Added opt-in CORS origins support to Porter; no origins allowed by default when running Porter directly.
- Provided docker-compose execution for Porter to run behind an NGINX reverse proxy server - all origins allowed by default for CORS, but can be customized. NGINX allows for the potential for more complex infrastructure configurations.

View File

@ -21,8 +21,13 @@ import click
from nucypher.blockchain.eth.networks import NetworksInventory
from nucypher.characters.lawful import Ursula
from nucypher.cli.config import group_general_config
from nucypher.cli.literature import BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED, PORTER_RUN_MESSAGE, \
BASIC_AUTH_REQUIRES_HTTPS
from nucypher.cli.literature import (
PORTER_BASIC_AUTH_ENABLED,
PORTER_BASIC_AUTH_REQUIRES_HTTPS,
PORTER_BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED,
PORTER_CORS_ALLOWED_ORIGINS,
PORTER_RUN_MESSAGE,
)
from nucypher.cli.options import (
option_network,
option_provider_uri,
@ -34,7 +39,6 @@ from nucypher.cli.options import (
from nucypher.cli.types import NETWORK_PORT
from nucypher.cli.utils import setup_emitter, get_registry
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.utilities.porter.control.interfaces import PorterInterface
from nucypher.utilities.porter.porter import Porter
@ -58,6 +62,7 @@ def porter():
@click.option('--tls-certificate-filepath', help="Pre-signed TLS certificate filepath", type=click.Path(dir_okay=False, exists=True, path_type=Path))
@click.option('--tls-key-filepath', help="TLS private key filepath", type=click.Path(dir_okay=False, exists=True, path_type=Path))
@click.option('--basic-auth-filepath', help="htpasswd filepath for basic authentication", type=click.Path(dir_okay=False, exists=True, resolve_path=True, path_type=Path))
@click.option('--allow-origins', help="The CORS origin(s) comma-delimited list of strings/regexes for origins to allow - no origins allowed by default", type=click.STRING)
@click.option('--dry-run', '-x', help="Execute normally without actually starting Porter", is_flag=True)
@click.option('--eager', help="Start learning and scraping the network before starting up other services", is_flag=True, default=True)
def run(general_config,
@ -71,6 +76,7 @@ def run(general_config,
tls_certificate_filepath,
tls_key_filepath,
basic_auth_filepath,
allow_origins,
dry_run,
eager):
"""Start Porter's Web controller."""
@ -79,14 +85,14 @@ def run(general_config,
# HTTP/HTTPS
if bool(tls_key_filepath) ^ bool(tls_certificate_filepath):
raise click.BadOptionUsage(option_name='--tls-key-filepath, --tls-certificate-filepath',
message=BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED)
message=PORTER_BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED)
is_https = (tls_key_filepath and tls_certificate_filepath)
# check authentication
if basic_auth_filepath and not is_https:
raise click.BadOptionUsage(option_name='--basic-auth-filepath',
message=BASIC_AUTH_REQUIRES_HTTPS)
message=PORTER_BASIC_AUTH_REQUIRES_HTTPS)
if federated_only:
if not teacher_uri:
@ -136,10 +142,20 @@ def run(general_config,
if not federated_only:
emitter.message(f"Provider: {provider_uri}", color='green')
if basic_auth_filepath:
emitter.message("Basic Authentication enabled", color='green')
# firm up falsy status (i.e. change specified empty string to None)
allow_origins = allow_origins if allow_origins else None
# covert to list of strings/regexes
allow_origins_list = None
if allow_origins:
allow_origins_list = allow_origins.split(",") # split into list of origins to allow
emitter.message(PORTER_CORS_ALLOWED_ORIGINS.format(allow_origins=allow_origins_list), color='green')
controller = PORTER.make_web_controller(htpasswd_filepath=basic_auth_filepath, crash_on_error=False)
if basic_auth_filepath:
emitter.message(PORTER_BASIC_AUTH_ENABLED, color='green')
controller = PORTER.make_web_controller(crash_on_error=False,
htpasswd_filepath=basic_auth_filepath,
cors_allow_origins_list=allow_origins_list)
http_scheme = "https" if is_https else "http"
message = PORTER_RUN_MESSAGE.format(http_scheme=http_scheme, http_port=http_port)
emitter.message(message, color='green', bold=True)

View File

@ -722,6 +722,10 @@ SUCCESSFUL_MANUALLY_SAVE_METADATA = "Successfully saved node metadata to {metada
PORTER_RUN_MESSAGE = "Running Porter Web Controller at {http_scheme}://127.0.0.1:{http_port}"
BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED = "Both --tls-key-filepath and --tls-certificate-filepath must be provided to launch porter with TLS; only one specified"
PORTER_BASIC_AUTH_ENABLED = "Basic Authentication enabled"
BASIC_AUTH_REQUIRES_HTTPS = "Basic authentication can only be used with HTTPS. --tls-key-filepath and --tls-certificate-filepath must also be provided"
PORTER_CORS_ALLOWED_ORIGINS = "CORS Allow Origins: {allow_origins}"
PORTER_BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED = "Both --tls-key-filepath and --tls-certificate-filepath must be provided to launch porter with TLS; only one specified"
PORTER_BASIC_AUTH_REQUIRES_HTTPS = "Basic authentication can only be used with HTTPS. --tls-key-filepath and --tls-certificate-filepath must also be provided"

View File

@ -20,6 +20,7 @@ import inspect
import json
from abc import ABC, abstractmethod
from json import JSONDecodeError
from pathlib import Path
from typing import Optional
import maya
@ -269,8 +270,8 @@ class WebController(InterfaceControlServer):
def start(self,
port: int,
tls_key_filepath: str = None,
tls_certificate_filepath: str = None,
tls_key_filepath: Path = None,
tls_certificate_filepath: Path = None,
dry_run: bool = False):
if dry_run:
return
@ -279,8 +280,8 @@ class WebController(InterfaceControlServer):
self.log.info("Starting HTTPS Control...")
# HTTPS endpoint
hx_deployer = HendrixDeployTLS(action="start",
key=tls_key_filepath,
cert=tls_certificate_filepath,
key=str(tls_key_filepath.absolute()),
cert=str(tls_certificate_filepath.absolute()),
options={
"wsgi": self._transport,
"https_port": port,

View File

@ -14,6 +14,7 @@
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from pathlib import Path
from typing import List, NamedTuple, Optional, Sequence
from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION, NO_CONTROL_PROTOCOL
@ -199,7 +200,10 @@ the Pipe for nucypher network operations
self.controller = controller
return controller
def make_web_controller(self, crash_on_error: bool = False, htpasswd_filepath: str = None):
def make_web_controller(self,
crash_on_error: bool = False,
htpasswd_filepath: Path = None,
cors_allow_origins_list: List[str] = None):
controller = WebController(app_name=self.APP_NAME,
crash_on_error=crash_on_error,
interface=self._interface_class(porter=self))
@ -207,6 +211,17 @@ the Pipe for nucypher network operations
# Register Flask Decorator
porter_flask_control = controller.make_control_transport()
# CORS origins
if cors_allow_origins_list:
try:
from flask_cors import CORS
except ImportError:
raise ImportError('Porter installation is required for to specify CORS origins '
'- run "pip install nucypher[porter]" and try again.')
_ = CORS(app=porter_flask_control, origins=cors_allow_origins_list)
# Basic Auth
if htpasswd_filepath:
try:
from flask_htpasswd import HtPasswdAuth
@ -214,7 +229,7 @@ the Pipe for nucypher network operations
raise ImportError('Porter installation is required for basic authentication '
'- run "pip install nucypher[porter]" and try again.')
porter_flask_control.config['FLASK_HTPASSWD_PATH'] = htpasswd_filepath
porter_flask_control.config['FLASK_HTPASSWD_PATH'] = str(htpasswd_filepath.absolute())
# ensure basic auth required for all endpoints
porter_flask_control.config['FLASK_AUTH_ALL'] = True
_ = HtPasswdAuth(app=porter_flask_control)

View File

@ -136,7 +136,7 @@ DEPLOY_REQUIRES = [
URSULA_REQUIRES = ['prometheus_client', 'sentry-sdk'] # TODO: Consider renaming to 'monitor', etc.
ALICE_REQUIRES = ['qrcode']
BOB_REQUIRES = ['qrcode']
PORTER_REQUIRES = ['flask-htpasswd'] # needed for basic authentication
PORTER_REQUIRES = ['flask-htpasswd', 'flask-cors'] # needed for basic authentication, cors
EXTRAS = {

View File

@ -22,9 +22,10 @@ import pytest
from nucypher.characters.lawful import Ursula
from nucypher.cli.literature import (
PORTER_RUN_MESSAGE,
BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED,
BASIC_AUTH_REQUIRES_HTTPS
PORTER_BASIC_AUTH_ENABLED,
PORTER_BASIC_AUTH_REQUIRES_HTTPS,
PORTER_BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED,
PORTER_RUN_MESSAGE, PORTER_CORS_ALLOWED_ORIGINS
)
from nucypher.cli.main import nucypher_cli
from nucypher.config.constants import TEMPORARY_DOMAIN
@ -96,7 +97,7 @@ def test_federated_porter_cli_run_tls_filepath_and_certificate(click_runner,
'--tls-key-filepath', tempfile_path) # only tls-key provided
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code != 0 # both --tls-key-filepath and --tls-certificate-filepath must be provided for TLS
assert BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED in result.output
assert PORTER_BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED in result.output
porter_run_command = ('porter', 'run',
'--dry-run',
@ -105,7 +106,7 @@ def test_federated_porter_cli_run_tls_filepath_and_certificate(click_runner,
'--tls-certificate-filepath', tempfile_path) # only certificate provided
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code != 0 # both --tls-key-filepath and --tls-certificate-filepath must be provided for TLS
assert BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED in result.output
assert PORTER_BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED in result.output
#
# tls-key and certificate filepaths must exist
@ -156,6 +157,56 @@ def test_federated_cli_run_https(click_runner, federated_ursulas, temp_dir_path,
assert PORTER_RUN_MESSAGE.format(http_scheme="https", http_port=Porter.DEFAULT_PORT) in result.output
def test_federated_cli_run_https_with_cors_origin(click_runner,
federated_ursulas,
temp_dir_path,
federated_teacher_uri):
tls_key_path = Path(temp_dir_path) / 'key.pem'
_write_random_data(tls_key_path)
certificate_file_path = Path(temp_dir_path) / 'fullchain.pem'
_write_random_data(certificate_file_path)
allow_origins = ".*\.example\.com,.*\.otherexample\.org"
porter_run_command = ('porter', 'run',
'--dry-run',
'--federated-only',
'--teacher', federated_teacher_uri,
'--tls-key-filepath', tls_key_path,
'--tls-certificate-filepath', certificate_file_path,
'--allow-origins', allow_origins)
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code == 0
assert PORTER_RUN_MESSAGE.format(http_scheme="https", http_port=Porter.DEFAULT_PORT) in result.output
assert PORTER_CORS_ALLOWED_ORIGINS.format(allow_origins=allow_origins.split(",")) in result.output
def test_federated_cli_run_https_with_empty_string_cors_origin(click_runner,
federated_ursulas,
temp_dir_path,
federated_teacher_uri):
tls_key_path = Path(temp_dir_path) / 'key.pem'
_write_random_data(tls_key_path)
certificate_file_path = Path(temp_dir_path) / 'fullchain.pem'
_write_random_data(certificate_file_path)
empty_string_allow_origins = ""
porter_run_command = ('porter', 'run',
'--dry-run',
'--federated-only',
'--teacher', federated_teacher_uri,
'--tls-key-filepath', tls_key_path,
'--tls-certificate-filepath', certificate_file_path,
'--allow-origins', empty_string_allow_origins)
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code == 0
assert PORTER_RUN_MESSAGE.format(http_scheme="https", http_port=Porter.DEFAULT_PORT) in result.output
# empty string translates to CORS not being enabled - empty origin string provides wild card comparison
# with just header
assert PORTER_CORS_ALLOWED_ORIGINS.format(allow_origins='') not in result.output
def test_federated_cli_run_https_basic_auth(click_runner,
federated_ursulas,
federated_teacher_uri,
@ -175,7 +226,7 @@ def test_federated_cli_run_https_basic_auth(click_runner,
'--basic-auth-filepath', basic_auth_file)
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code == 0
assert "Basic Authentication enabled" in result.output
assert PORTER_BASIC_AUTH_ENABLED in result.output
def test_blockchain_porter_cli_run_simple(click_runner,
@ -271,6 +322,37 @@ def test_blockchain_porter_cli_run_https(click_runner,
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code == 0
assert PORTER_RUN_MESSAGE.format(http_scheme="https", http_port=Porter.DEFAULT_PORT) in result.output
# no CORS configured by default; empty origin string provides wild card comparison with just header
assert PORTER_CORS_ALLOWED_ORIGINS.format(allow_origins='') not in result.output
def test_blockchain_porter_cli_run_https_with_cors_origin(click_runner,
blockchain_ursulas,
testerchain,
agency_local_registry,
temp_dir_path,
blockchain_teacher_uri):
tls_key_path = Path(temp_dir_path) / 'key.pem'
_write_random_data(tls_key_path)
certificate_file_path = Path(temp_dir_path) / 'fullchain.pem'
_write_random_data(certificate_file_path)
allow_origins = "*"
porter_run_command = ('porter', 'run',
'--dry-run',
'--network', TEMPORARY_DOMAIN,
'--provider', TEST_PROVIDER_URI,
'--registry-filepath', agency_local_registry.filepath,
'--teacher', blockchain_teacher_uri,
'--tls-key-filepath', tls_key_path,
'--tls-certificate-filepath', certificate_file_path,
'--allow-origins', allow_origins)
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code == 0
assert PORTER_RUN_MESSAGE.format(http_scheme="https", http_port=Porter.DEFAULT_PORT) in result.output
assert PORTER_CORS_ALLOWED_ORIGINS.format(allow_origins=[allow_origins]) in result.output
def test_blockchain_porter_cli_run_https_basic_auth(click_runner,
@ -297,7 +379,7 @@ def test_blockchain_porter_cli_run_https_basic_auth(click_runner,
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code != 0
assert BASIC_AUTH_REQUIRES_HTTPS in result.output
assert PORTER_BASIC_AUTH_REQUIRES_HTTPS in result.output
# Basic Auth
porter_run_command = ('porter', 'run',
@ -312,7 +394,7 @@ def test_blockchain_porter_cli_run_https_basic_auth(click_runner,
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
assert result.exit_code == 0
assert "Basic Authentication enabled" in result.output
assert PORTER_BASIC_AUTH_ENABLED in result.output
def _write_random_data(filepath: Path):