Skip to content

Commit

Permalink
Merge pull request #439 from Renumics/feature/start-with-tls
Browse files Browse the repository at this point in the history
Feature/start with tls
  • Loading branch information
druzsan authored Feb 27, 2024
2 parents 294e576 + 2cbcaae commit 4a78a95
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 30 deletions.
21 changes: 21 additions & 0 deletions renumics/spotlight/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ def cli_dtype_callback(
multiple=True,
help="Columns to embed (if no --embed-all).",
)
@click.option(
"--ssl-keyfile",
type=click.Path(exists=True, dir_okay=False),
default=None,
help="SSL key file",
)
@click.option(
"--ssl-certfile",
type=click.Path(exists=True, dir_okay=False),
default=None,
help="SSL certificate file",
)
@click.option(
"--ssl-keyfile-password", type=str, default=None, help="SSL keyfile password"
)
@click.option("-v", "--verbose", is_flag=True)
@click.version_option(spotlight.__version__)
def main(
Expand All @@ -124,6 +139,9 @@ def main(
analyze_all: bool,
embed: Tuple[str],
embed_all: bool,
ssl_keyfile: Optional[str],
ssl_certfile: Optional[str],
ssl_keyfile_password: Optional[str],
verbose: bool,
) -> None:
"""
Expand All @@ -150,4 +168,7 @@ def main(
wait="forever",
analyze=True if analyze_all else list(analyze),
embed=True if embed_all else list(embed),
ssl_keyfile=ssl_keyfile,
ssl_certfile=ssl_certfile,
ssl_keyfile_password=ssl_keyfile_password,
)
48 changes: 45 additions & 3 deletions renumics/spotlight/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
from renumics.spotlight.settings import settings


class MissingTLSCertificate(Exception):
pass


class MissingTLSKey(Exception):
pass


class Server:
"""
Local proxy object for the spotlight server process
Expand All @@ -32,6 +40,9 @@ class Server:
_host: str
_port: int
_requested_port: int
_ssl_keyfile: Optional[str]
_ssl_certfile: Optional[str]
_ssl_keyfile_password: Optional[str]

_vite: Optional[Vite]

Expand All @@ -54,15 +65,41 @@ class Server:
_all_frontends_disconnected: threading.Event
_any_frontend_connected: threading.Event

def __init__(self, host: str = "127.0.0.1", port: int = 8000) -> None:
def __init__(
self,
host: str = "127.0.0.1",
port: int = 8000,
ssl_keyfile: Optional[str] = None,
ssl_certfile: Optional[str] = None,
ssl_keyfile_password: Optional[str] = None,
) -> None:
self.process = None

self._vite = None

self._app_config = AppConfig()

self._host = host
if self._host not in ("127.0.0.1", "localhost"):
if ssl_certfile is None:
raise MissingTLSCertificate(
"Starting Spotlight on non-localhost without TLS certificate is insecure. Please provide TLS certificate and key."
)
if ssl_keyfile is None:
raise MissingTLSKey(
"Starting Spotlight on non-localhost without TLS certificate is insecure. Please provide TLS certificate and key."
)
elif ssl_certfile is None and ssl_keyfile is not None:
raise MissingTLSCertificate(
"TLS key provided, but TLS certificate is missing."
)
elif ssl_certfile is not None and ssl_keyfile is None:
raise MissingTLSKey("TLS certificate provided, but TLS key is missing.")
self._ssl_keyfile = ssl_keyfile
self._ssl_certfile = ssl_certfile
self._ssl_keyfile_password = ssl_keyfile_password
self._requested_port = port
self._port = self._requested_port
self.process = None

self.connected_frontends = 0
self._any_frontend_connected = threading.Event()
Expand Down Expand Up @@ -145,7 +182,12 @@ def start(self, config: AppConfig) -> None:
sock.close()
else:
command += ["--fd", str(sock.fileno())]

if self._ssl_keyfile is not None:
command += ["--ssl-keyfile", self._ssl_keyfile]
if self._ssl_certfile is not None:
command += ["--ssl-certfile", self._ssl_certfile]
if self._ssl_keyfile_password is not None:
command += ["--ssl-keyfile-password", self._ssl_keyfile_password]
if settings.dev:
command.append("--reload")

Expand Down
37 changes: 33 additions & 4 deletions renumics/spotlight/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,25 @@ class Viewer:

_host: str
_requested_port: Union[int, Literal["auto"]]
_ssl_keyfile: Optional[str]
_ssl_certfile: Optional[str]
_ssl_keyfile_password: Optional[str]
_server: Optional[Server]
_df: Optional[pd.DataFrame]

def __init__(
self,
host: str = "127.0.0.1",
port: Union[int, Literal["auto"]] = "auto",
ssl_keyfile: Optional[str] = None,
ssl_certfile: Optional[str] = None,
ssl_keyfile_password: Optional[str] = None,
) -> None:
self._host = host
self._requested_port = port
self._ssl_keyfile = ssl_keyfile
self._ssl_certfile = ssl_certfile
self._ssl_keyfile_password = ssl_keyfile_password
self._server = None
self._df = None

Expand Down Expand Up @@ -218,7 +227,13 @@ def show(

if not self._server:
port = 0 if self._requested_port == "auto" else self._requested_port
self._server = Server(host=self._host, port=port)
self._server = Server(
self._host,
port,
self._ssl_keyfile,
self._ssl_certfile,
self._ssl_keyfile_password,
)
self._server.start(config)

if self not in _VIEWERS:
Expand Down Expand Up @@ -273,7 +288,11 @@ def open_browser(self) -> None:
"""
if not self.port:
return
launch_browser_in_thread("localhost", self.port)
if self._ssl_certfile is not None:
protocol = "https"
else:
protocol = "http"
launch_browser_in_thread(f"{protocol}://{self.host}:{self.port}/")

def refresh(self) -> None:
"""
Expand Down Expand Up @@ -319,7 +338,11 @@ def url(self) -> str:
"""
The viewer's url.
"""
return f"http://{self.host}:{self.port}/"
if self._ssl_certfile is not None:
protocol = "https"
else:
protocol = "http"
return f"{protocol}://{self.host}:{self.port}/"

def __repr__(self) -> str:
return self.url
Expand Down Expand Up @@ -380,6 +403,9 @@ def show(
analyze: Optional[Union[bool, List[str]]] = None,
issues: Optional[Collection[DataIssue]] = None,
embed: Optional[Union[List[str], bool]] = None,
ssl_keyfile: Optional[str] = None,
ssl_certfile: Optional[str] = None,
ssl_keyfile_password: Optional[str] = None,
) -> Viewer:
"""
Start a new Spotlight viewer.
Expand All @@ -406,6 +432,9 @@ def show(
issues: Custom dataset issues displayed in the viewer.
embed: Automatically embed all or given columns with default
embedders (disabled by default).
ssl_keyfile: Optional SSL key file.
ssl_certfile: Optional SSL certificate file.
ssl_certfile: Optional SSL keyfile password.
"""

viewer = None
Expand All @@ -416,7 +445,7 @@ def show(
viewer = _VIEWERS[index]
break
if not viewer:
viewer = Viewer(host, port)
viewer = Viewer(host, port, ssl_keyfile, ssl_certfile, ssl_keyfile_password)

viewer.show(
dataset,
Expand Down
30 changes: 7 additions & 23 deletions renumics/spotlight/webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,37 @@
Launch browser.
"""

import os
import sys
import threading
import time
import webbrowser

import requests
from loguru import logger

from renumics.spotlight.environ import set_temp_environ


def wait_for(url: str) -> None:
"""Wait until the service at url is reachable."""
while True:
try:
requests.head(url, timeout=10)
requests.head(url, timeout=10, verify=False)
break
except requests.exceptions.ConnectionError:
time.sleep(0.5)


def launch_browser_in_thread(host: str, port: int) -> threading.Thread:
def launch_browser_in_thread(url: str) -> threading.Thread:
"""Open the app in a browser in background once it runs."""
thread = threading.Thread(target=launch_browser, args=(host, port))
thread = threading.Thread(target=launch_browser, args=(url,))
thread.start()
return thread


def launch_browser(host: str, port: int) -> None:
def launch_browser(url: str) -> None:
"""Open the app in a browser once it runs."""
app_url = f"http://{host}:{port}/"
wait_for(app_url) # wait also for socket?

# If we want to launch firefox with the webbrowser module,
# we need to restore LD_LIBRARY_PATH if running through pyinstaller.
# The original LD_LIBRARY_PATH is stored as LD_LIBRARY_PATH_ORIG.
# See https://github.com/pyinstaller/pyinstaller/issues/6334
wait_for(url) # wait also for socket?
try:
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
with set_temp_environ(
LD_LIBRARY_PATH=os.environ.get("LD_LIBRARY_PATH_ORIG")
):
webbrowser.open(app_url)
else:
webbrowser.open(app_url)
webbrowser.open(url)
except Exception:
logger.warning(
f"Couldn't launch browser automatically, you can reach Spotlight at {app_url}."
f"Couldn't launch browser automatically, you can reach Spotlight at {url}."
)

0 comments on commit 4a78a95

Please sign in to comment.