Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HTTPS to REST API #1770

Merged
merged 59 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
23f4873
Add HTTPS to REST API
charlesbvll Apr 4, 2023
bccfed8
Merge branch 'main' into add-https-rest
charlesbvll Apr 4, 2023
ef58919
Replace underscore with dash
charlesbvll Apr 11, 2023
9a73a35
Replace underscore with dash
charlesbvll Apr 11, 2023
9240cba
Merge branch 'main' into add-https-rest
charlesbvll Apr 11, 2023
3f943bd
Add client-side TLS
charlesbvll Apr 12, 2023
b42ffce
Add type hints
charlesbvll Apr 12, 2023
5897d4d
Make gRPC return server message optional
charlesbvll Apr 12, 2023
3d8096c
Reformat
charlesbvll Apr 12, 2023
f79dd59
Reorder parameters
charlesbvll Apr 12, 2023
eb8efb1
Disable too many local variables
charlesbvll Apr 12, 2023
21bd3be
Merge branch 'main' into add-https-rest
danieljanes Apr 13, 2023
cf14eb4
Merge branch 'main' of https://github.com/adap/flower into add-https-…
charlesbvll Apr 14, 2023
dad788e
Set verify to True by default
charlesbvll Apr 14, 2023
baae0d5
Reformat files
charlesbvll Apr 14, 2023
f904eff
Remove unecesary pylint disable
charlesbvll Apr 14, 2023
a1722f9
Use isinstance instead of type
charlesbvll Apr 14, 2023
96ea8fe
Reduce line lenghts
charlesbvll Apr 14, 2023
d4e8137
Reduce line lenghts
charlesbvll Apr 14, 2023
169f790
Reformat file
charlesbvll Apr 14, 2023
e5502c2
Merge branch 'main' into add-https-rest
charlesbvll Apr 17, 2023
16c21bb
Improve missing certificate handling
tanertopal Apr 17, 2023
bb0bc90
Merge branch 'main' into add-https-rest
tanertopal Apr 17, 2023
507ca88
Remove todo statement
tanertopal Apr 17, 2023
e6fffaf
Merge branch 'main' into add-https-rest
tanertopal Apr 18, 2023
f787b28
Revert change
tanertopal Apr 18, 2023
b2cbb8e
Fix missing files Exception
tanertopal Apr 18, 2023
430a66b
Merge branch 'add-https-rest' of github.com:adap/flower into add-http…
tanertopal Apr 18, 2023
c9143b0
Update src/py/flwr/client/app.py
tanertopal Apr 18, 2023
3ffc445
Merge branch 'main' into add-https-rest
tanertopal Apr 18, 2023
6556f4d
Apply autoformat
tanertopal Apr 18, 2023
fc68e0e
Merge branch 'main' into add-https-rest
tanertopal Apr 18, 2023
1e77218
Improve missing ssl file handling.
tanertopal Apr 18, 2023
5c52695
Merge branch 'add-https-rest' of github.com:adap/flower into add-http…
tanertopal Apr 18, 2023
daf6822
Update app.py
tanertopal Apr 18, 2023
9401815
Simplify
tanertopal Apr 18, 2023
a85121b
Update src/py/flwr/client/app.py
danieljanes Apr 18, 2023
22f7bef
Update src/py/flwr/client/app.py
danieljanes Apr 18, 2023
0cb81d1
Update src/py/flwr/client/app.py
danieljanes Apr 18, 2023
4a87ef1
Update src/py/flwr/client/grpc_client/connection.py
danieljanes Apr 18, 2023
2e2a131
Update src/py/flwr/client/app.py
danieljanes Apr 18, 2023
d06cedb
Update src/py/flwr/client/app.py
danieljanes Apr 18, 2023
642a52e
Update src/py/flwr/client/rest_client/connection.py
danieljanes Apr 18, 2023
0213c07
Merge branch 'main' into add-https-rest
danieljanes Apr 18, 2023
c5cec46
Exception handling
tanertopal Apr 18, 2023
7111249
Fix
tanertopal Apr 18, 2023
bd56a05
Merge branch 'add-https-rest' of github.com:adap/flower into add-http…
tanertopal Apr 18, 2023
ce82e19
Update src/py/flwr/server/app.py
tanertopal Apr 18, 2023
cb06fc2
Update src/py/flwr/server/app.py
tanertopal Apr 18, 2023
da1527c
Update src/py/flwr/server/app.py
tanertopal Apr 18, 2023
0a87939
Autoformat
tanertopal Apr 18, 2023
b5b038f
Merge branch 'main' into add-https-rest
tanertopal Apr 18, 2023
c26f261
Merge branch 'main' into add-https-rest
tanertopal Apr 19, 2023
5ab7f0b
Fix linter issue
tanertopal Apr 19, 2023
0d18130
Fixes a linter error
tanertopal Apr 19, 2023
5e1c70e
Fix linter error
tanertopal Apr 19, 2023
8551fd8
Merge branch 'main' into add-https-rest
charlesbvll Apr 19, 2023
8133499
Merge branch 'main' into add-https-rest
charlesbvll Apr 19, 2023
9356f8e
Ignore too-many-arguments error
charlesbvll Apr 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ share/python-wheels/
.installed.cfg
*.egg
MANIFEST
.srl

# PyInstaller
# Usually these files are written by a python script from a template
Expand Down
27 changes: 16 additions & 11 deletions src/py/flwr/client/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ def start_client(
server_address: str,
client: Client,
grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
root_certificates: Optional[bytes] = None,
root_certificates: Optional[Union[bytes, str]] = None,
rest: bool = False,
) -> None:
"""Start a Flower Client which connects to a gRPC server.
"""Start a Flower client node which connects to a Flower server.

Parameters
----------
Expand All @@ -109,25 +109,25 @@ class `flwr.client.Client`.
value. Note that the Flower server needs to be started with the
same value (see `flwr.server.start_server`), otherwise it will not
know about the increased limit and block larger messages.
root_certificates : bytes (default: None)
The PEM-encoded root certificates as a byte string. If provided, a secure
connection using the certificates will be established to a
SSL-enabled Flower server.
root_certificates : Optional[Union[bytes, str]] (default: None)
The PEM-encoded root certificates as a byte string or a path string.
If provided, a secure connection using the certificates will be
established to an SSL-enabled Flower server.
rest : bool (default: False)
Defines whether or not the client is interacting with the server using the
experimental REST API. This feature is experimental, it might change
considerably in future versions of Flower.

Examples
--------
Starting a client with insecure server connection:
Starting a gRPC client with an insecure server connection:

>>> start_client(
>>> server_address=localhost:8080,
>>> client=FlowerClient(),
>>> )

Starting a SSL-enabled client:
Starting an SSL-enabled gRPC client:

>>> from pathlib import Path
>>> start_client(
Expand Down Expand Up @@ -155,6 +155,11 @@ class `flwr.client.Client`.
"To use the REST API you must install the "
"extra dependencies by running `pip install flwr['rest']`."
) from missing_dep
if server_address[:4] != "http":
sys.exit(
"When using the REST API, please provide `https://` or "
"`http://` before the server address (e.g. `http://127.0.0.1:8080`)"
)
connection = http_request_response
else:
connection = grpc_connection
Expand Down Expand Up @@ -218,9 +223,9 @@ def start_numpy_client(
same value (see `flwr.server.start_server`), otherwise it will not
know about the increased limit and block larger messages.
root_certificates : bytes (default: None)
The PEM-encoded root certificates a byte string. If provided, a secure
connection using the certificates will be established to a
SSL-enabled Flower server.
The PEM-encoded root certificates as a byte string or a path string.
If provided, a secure connection using the certificates will be
established to an SSL-enabled Flower server.
rest : bool (default: False)
Defines whether or not the client is interacting with the server using the
experimental REST API. This feature is experimental, it might be change
Expand Down
14 changes: 9 additions & 5 deletions src/py/flwr/client/grpc_client/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@

from contextlib import contextmanager
from logging import DEBUG
from pathlib import Path
from queue import Queue
from typing import Callable, Iterator, Optional, Tuple
from typing import Callable, Iterator, Optional, Tuple, Union

from flwr.common import GRPC_MAX_MESSAGE_LENGTH
from flwr.common.grpc import create_channel
Expand All @@ -42,7 +43,7 @@ def on_channel_state_change(channel_connectivity: str) -> None:
def grpc_connection(
server_address: str,
max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
root_certificates: Optional[bytes] = None,
root_certificates: Optional[Union[bytes, str]] = None,
) -> Iterator[Tuple[Callable[[], ServerMessage], Callable[[ClientMessage], None]]]:
"""Establish a gRPC connection to a gRPC server.

Expand All @@ -61,9 +62,9 @@ def grpc_connection(
increased limit and block larger messages.
(default: 536_870_912, this equals 512MB)
root_certificates : Optional[bytes] (default: None)
The PEM-encoded root certificates as a byte string. If provided, a secure
connection using the certificates will be established to a SSL-enabled
Flower server.
The PEM-encoded root certificates as a byte string or a path string.
If provided, a secure connection using the certificates will be
established to an SSL-enabled Flower server.

Returns
-------
Expand All @@ -84,6 +85,9 @@ def grpc_connection(
>>> # do something here
>>> send(client_message)
"""
if isinstance(root_certificates, str):
root_certificates = Path(root_certificates).read_bytes()

channel = create_channel(
server_address=server_address,
root_certificates=root_certificates,
Expand Down
35 changes: 28 additions & 7 deletions src/py/flwr/client/rest_client/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from contextlib import contextmanager
from logging import ERROR, INFO, WARN
from typing import Callable, Dict, Iterator, Optional, Tuple
from typing import Callable, Dict, Iterator, Optional, Tuple, Union

try:
import requests
Expand Down Expand Up @@ -48,7 +48,9 @@
def http_request_response(
server_address: str,
max_message_length: int = GRPC_MAX_MESSAGE_LENGTH, # pylint: disable=W0613
root_certificates: Optional[bytes] = None, # pylint: disable=unused-argument
root_certificates: Optional[
Union[bytes, str]
] = None, # pylint: disable=unused-argument
) -> Iterator[
Tuple[Callable[[], Optional[ServerMessage]], Callable[[ClientMessage], None]]
]:
Expand All @@ -60,12 +62,15 @@ def http_request_response(
Parameters
----------
server_address : str
The IPv6 address of the server. If the Flower server runs on the same machine
on port 8080, then `server_address` would be `"[::]:8080"`.
The IPv6 address of the server with `http://` or `https://`.
If the Flower server runs on the same machine
on port 8080, then `server_address` would be `"http://[::]:8080"`.
max_message_length : int
Ignored, only present to preserve API-compatibility.
root_certificates : Optional[bytes] (default: None)
Ignored, for now.
root_certificates : Optional[Union[bytes, str]] (default: None)
danieljanes marked this conversation as resolved.
Show resolved Hide resolved
Path of the root certificate. If provided, a secure
connection using the certificates will be established to an SSL-enabled
Flower server. Bytes won't work for the REST API.
danieljanes marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
Expand All @@ -80,7 +85,21 @@ def http_request_response(
""",
)

base_url = f"http://{server_address}"
base_url = server_address

# NEVER SET VERIFY TO FALSE
# Otherwise any server can fake its identity
# Please refer to:
# https://requests.readthedocs.io/en/latest/user/advanced/#ssl-cert-verification
verify: Union[bool, str] = True
if isinstance(root_certificates, str):
verify = root_certificates
elif isinstance(root_certificates, bytes):
log(
ERROR,
"For the REST API, the root certificates "
"must be provided as a string path to the client.",
)
danieljanes marked this conversation as resolved.
Show resolved Hide resolved

# Necessary state to link TaskRes to TaskIns
state: Dict[str, Optional[TaskIns]] = {"current_task_ins": None}
Expand All @@ -106,6 +125,7 @@ def receive() -> Optional[ServerMessage]:
"Content-Type": "application/protobuf",
},
data=pull_task_ins_req_bytes,
verify=verify,
)

# Check status code and headers
Expand Down Expand Up @@ -177,6 +197,7 @@ def send(client_message_proto: ClientMessage) -> None:
"Content-Type": "application/protobuf",
},
data=push_task_res_request_bytes,
verify=verify,
)

state["current_task_ins"] = None
Expand Down
64 changes: 60 additions & 4 deletions src/py/flwr/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
import sys
import threading
from dataclasses import dataclass
from logging import INFO, WARN
from logging import ERROR, INFO, WARN
from os.path import isfile
from signal import SIGINT, SIGTERM, signal
from types import FrameType
from typing import List, Optional, Tuple
Expand Down Expand Up @@ -348,7 +349,12 @@ def run_server() -> None:
host, port, _ = parsed_address
fleet_thread = threading.Thread(
target=_run_fleet_api_rest,
args=(host, port, state_factory),
args=(
args.rest_fleet_api_address,
args.ssl_keyfile,
args.ssl_certfile,
state_factory,
),
)
fleet_thread.start()
bckg_threads.append(fleet_thread)
Expand Down Expand Up @@ -485,8 +491,9 @@ def _run_fleet_api_grpc_bidi(

# pylint: disable=import-outside-toplevel
def _run_fleet_api_rest(
host: str,
port: int,
address: str,
ssl_keyfile: Optional[str],
ssl_certfile: Optional[str],
state_factory: StateFactory,
) -> None:
"""Run Driver API (REST-based)."""
Expand All @@ -505,6 +512,17 @@ def _run_fleet_api_rest(
# See: https://www.starlette.io/applications/#accessing-the-app-instance
fast_api_app.state.STATE_FACTORY = state_factory

host, port_str = address.split(":")
port = int(port_str)

validation_exceptions = _validate_ssl_files(
ssl_certfile=ssl_certfile, ssl_keyfile=ssl_keyfile
)
if any(validation_exceptions):
# Starting with 3.11 we can use ExceptionGroup but for now
# this seems to be the reasonable approach.
raise ValueError(validation_exceptions)

uvicorn.run(
# "flwr.server.rest_server.rest_api:app",
app=fast_api_app,
Expand All @@ -513,9 +531,37 @@ def _run_fleet_api_rest(
reload=False,
access_log=True,
workers=1,
ssl_keyfile=ssl_keyfile,
ssl_certfile=ssl_certfile,
)


def _validate_ssl_files(
ssl_keyfile: Optional[str], ssl_certfile: Optional[str]
) -> List[ValueError]:
validation_exceptions = []

if ssl_keyfile is not None and not isfile(ssl_keyfile):
msg = "Path argument `--ssl-keyfile` does not point to a file."
log(ERROR, msg)
validation_exceptions.append(ValueError(msg))

if ssl_certfile is not None and not isfile(ssl_certfile):
msg = "Path argument `--ssl-certfile` does not point to a file."
log(ERROR, msg)
validation_exceptions.append(ValueError(msg))

if not bool(ssl_keyfile) == bool(ssl_certfile):
msg = (
"When setting one of `--ssl-keyfile` and "
+ "`--ssl-certfile`, both have to be used."
)
log(ERROR, msg)
validation_exceptions.append(ValueError(msg))

return validation_exceptions


def _parse_args_driver() -> argparse.ArgumentParser:
"""Parse command line arguments for Driver API."""
parser = argparse.ArgumentParser(
Expand Down Expand Up @@ -603,3 +649,13 @@ def _add_args_fleet_api(parser: argparse.ArgumentParser) -> None:
help=f"Fleet API REST server address. Default:'{ADDRESS_FLEET_API_REST}'",
default=ADDRESS_FLEET_API_REST,
)
rest_group.add_argument(
"--ssl-certfile",
help="Fleet API REST SSL certificate file (as a path str). Default:None",
default=None,
)
rest_group.add_argument(
"--ssl-keyfile",
help="Fleet API REST SSL private key file (as a path str). Default:None",
default=None,
)
tanertopal marked this conversation as resolved.
Show resolved Hide resolved