def _start_bundled_app(cls, labbook: LabBook, router: ProxyRouter, username: str, bundled_app: dict, container_override_id: str = None): tool_port = bundled_app['port'] labbook_ip = ContainerOperations.get_labbook_ip(labbook, username) endpoint = f'http://{labbook_ip}:{tool_port}' route_prefix = quote_plus(bundled_app['name']) matched_routes = router.get_matching_routes(endpoint, route_prefix) run_command = True suffix = None if len(matched_routes) == 1: logger.info(f"Found existing {bundled_app['name']} in route table for {str(labbook)}.") suffix = matched_routes[0] run_command = False elif len(matched_routes) > 1: raise ValueError(f"Multiple {bundled_app['name']} instances found in route table " f"for {str(labbook)}! Restart container.") if run_command: logger.info(f"Adding {bundled_app['name']} to route table for {str(labbook)}.") suffix, _ = router.add(endpoint, route_prefix) suffix = "/" + suffix # Start app logger.info(f"Starting {bundled_app['name']} in {str(labbook)}.") start_bundled_app(labbook, username, bundled_app['command'], tag=container_override_id) return suffix
def _start_rstudio(cls, lb: LabBook, pr: ProxyRouter, username: str, container_override_id: str = None): lb_ip = ContainerOperations.get_labbook_ip(lb, username) lb_endpoint = f'http://{lb_ip}:8787' mitm_endpoint = MITMProxyOperations.get_mitmendpoint(lb_endpoint) # start mitm proxy if it doesn't exist if mitm_endpoint is None: # get a proxy prefix unique_key = unique_id() # start proxy mitm_endpoint = MITMProxyOperations.start_mitm_proxy( lb_endpoint, unique_key) # Ensure we start monitor when starting MITM proxy start_labbook_monitor( lb, username, "rstudio", # This is the endpoint for the proxy and not the rserver? url=f'{lb_endpoint}/{unique_key}', author=get_logged_in_author()) # All messages will come through MITM, so we don't need to monitor rserver directly start_rserver(lb, username, tag=container_override_id) # add route rt_prefix, _ = pr.add(mitm_endpoint, f'rserver/{unique_key}/') # Warning: RStudio will break if there is a trailing slash! suffix = f'/{rt_prefix}' else: # existing route to MITM or not? matched_routes = pr.get_matching_routes(mitm_endpoint, 'rserver') if len(matched_routes) == 1: suffix = matched_routes[0] elif len(matched_routes) == 0: logger.warning( 'Creating missing route for existing RStudio mitmproxy_proxy' ) # TODO DC: This feels redundant with already getting the mitm_endpoint above # Can we refactor this into a more coherent single operation? Maybe an MITMProxy instance? unique_key = MITMProxyOperations.get_mitmkey(lb_endpoint) # add route rt_prefix, _ = pr.add(mitm_endpoint, f'rserver/{unique_key}/') # Warning: RStudio will break if there is a trailing slash! suffix = f'/{rt_prefix}' else: raise ValueError( f"Multiple RStudio proxy instances for {str(lb)}. Please restart the Project " "or manually delete stale containers.") return suffix
def _start_jupyter_tool(cls, lb: LabBook, pr: ProxyRouter, username: str, container_override_id: str = None): tool_port = 8888 lb_ip = ContainerOperations.get_labbook_ip(lb, username) lb_endpoint = f'http://{lb_ip}:{tool_port}' matched_routes = pr.get_matching_routes(lb_endpoint, 'jupyter') run_start_jupyter = True suffix = None if len(matched_routes) == 1: logger.info( f'Found existing Jupyter instance in route table for {str(lb)}.' ) suffix = matched_routes[0] # wait for jupyter to be up try: check_jupyter_reachable(lb_ip, tool_port, suffix) run_start_jupyter = False except GigantumException: logger.warning( f'Detected stale route. Attempting to restart Jupyter and clean up route table.' ) pr.remove(suffix[1:]) elif len(matched_routes) > 1: raise ValueError( f"Multiple Jupyter instances found in route table for {str(lb)}! Restart container." ) if run_start_jupyter: rt_prefix = unique_id() rt_prefix, _ = pr.add(lb_endpoint, f'jupyter/{rt_prefix}') # Start jupyterlab suffix = start_jupyter(lb, username, tag=container_override_id, proxy_prefix=rt_prefix) # Ensure we start monitor IFF jupyter isn't already running. start_labbook_monitor(lb, username, 'jupyterlab', url=f'{lb_endpoint}/{rt_prefix}', author=get_logged_in_author()) return suffix
def _start_dev_tool(cls, lb: LabBook, username: str, dev_tool: str, container_override_id: str = None): pr = ProxyRouter.get_proxy(lb.client_config.config['proxy']) if dev_tool == "rstudio": suffix = cls._start_rstudio(lb, pr, username) elif dev_tool in ["jupyterlab", "notebook"]: # Note that starting the dev tool is identical whether we're targeting jupyterlab or notebook suffix = cls._start_jupyter_tool(lb, pr, username, container_override_id) else: raise GigantumException( f"'{dev_tool}' not currently supported as a Dev Tool") # Don't include the port in the path if running on 80 apparent_proxy_port = lb.client_config.config['proxy'][ "apparent_proxy_port"] if apparent_proxy_port == 80: path = suffix else: path = f':{apparent_proxy_port}{suffix}' return path
def test_connect_to_internal_process_via_proxy_1(config_fixture, start_proxy): """ Create a route to proxy but specifify the route prefix. """ pr = ProxyRouter.get_proxy(config_fixture) pfx, host = pr.add("http://localhost:5555", 'test/server/1') assert host == 'http://localhost:5555' assert pfx in [p[1:] for p in pr.routes.keys()] assert 'spool' in requests.get(f'http://localhost/{pfx}').text
def _start_dev_tool(cls, labbook: LabBook, username: str, dev_tool: str, container_override_id: str = None): router = ProxyRouter.get_proxy(labbook.client_config.config['proxy']) bam = BundledAppManager(labbook) bundled_apps = bam.get_bundled_apps() bundled_app_names = [x for x in bundled_apps] if dev_tool == "rstudio": suffix = cls._start_rstudio(labbook, router, username) elif dev_tool in ["jupyterlab", "notebook"]: # Note that starting the dev tool is identical whether we're targeting jupyterlab or notebook suffix = cls._start_jupyter_tool(labbook, router, username, container_override_id) elif dev_tool in bundled_app_names: app_data = bundled_apps[dev_tool] app_data['name'] = dev_tool suffix = cls._start_bundled_app(labbook, router, username, app_data, container_override_id) else: raise GigantumException(f"'{dev_tool}' not currently supported as a Dev Tool") # Don't include the port in the path if running on 80 apparent_proxy_port = labbook.client_config.config['proxy']["apparent_proxy_port"] if apparent_proxy_port == 80: path = suffix else: path = f':{apparent_proxy_port}{suffix}' return path
def test_make_and_delete_routes(config_fixture, start_proxy): pr = ProxyRouter.get_proxy(config_fixture) pfx1, host1 = pr.add("http://localhost:5555") pfx2, host2 = pr.add("http://localhost:6666") assert pr.routes pr.remove(pfx1) pr.remove(pfx2) assert len(pr.routes.keys()) == 0
def test_search(config_fixture, start_proxy): rando = str(uuid.uuid4()).replace('-', '')[:6] pr = ProxyRouter.get_proxy(config_fixture) pf1, host1 = pr.add("http://localhost:6666") pf2, host2 = pr.add("http://localhost:5555", f'constant/{rando}') assert pr.search('cat') is None assert pr.search('http://localhost:6666')[1:].isalnum() assert pr.search('http://localhost:5555') == f'/constant/{rando}' assert pr.search(pf1) is None assert pr.search(pf2) is None
def test_check(config_fixture, start_proxy): pr = ProxyRouter.get_proxy(config_fixture) pf1, host1 = pr.add("http://localhost:6666") pf2, host2 = pr.add("http://localhost:5555", f'constant/x') pf3, host3 = pr.add("http://localhost:9911", f'not/real') pf4, host4 = pr.add("http://12.24.111.21:22", f'should/timeout') r1 = pr.search("http://localhost:6666") r2 = pr.search("http://localhost:5555") t0 = time.time() assert pr.check(pf1) is True assert pr.check(pf2) is True assert pr.check(r1) is True assert pr.check(r2) is True assert pr.check('constant/x') is True assert pr.check(pf3) is False assert pr.check(pf4) is False t1 = time.time() # Check that timeouts timed-out super quickly assert t1 - t0 < 2.0
# Allow CORS CORS(app, max_age=7200) # Set Debug mode app.config['DEBUG'] = config.config["flask"]["DEBUG"] # Register LabBook service app.register_blueprint(blueprint.complete_labbook_service) # Configure CHP try: api_prefix = app.config["LABMGR_CONFIG"].config['proxy']["labmanager_api_prefix"] apparent_proxy_port = app.config["LABMGR_CONFIG"].config['proxy']["apparent_proxy_port"] api_port = app.config["LABMGR_CONFIG"].config['proxy']['api_port'] proxy_router = ProxyRouter.get_proxy(app.config["LABMGR_CONFIG"].config['proxy']) proxy_router.add("http://localhost:10001", "api") logger.info(f"Proxy routes ({type(proxy_router)}): {proxy_router.routes}") except Exception as e: logger.exception(e) # Set auth error handler @app.errorhandler(AuthenticationError) def handle_auth_error(ex): response = jsonify(ex.error) response.status_code = ex.status_code return response # Set Unauth'd route for API health-check
def test_connect_to_internal_process_via_proxy_2(config_fixture, start_proxy): """ Create route to proxy but have it auto-generate a route prefix. """ pr = ProxyRouter.get_proxy(config_fixture) pfx, host = pr.add("http://localhost:6666") assert pfx in [p[1:] for p in pr.routes.keys()] assert 'ldconfig' in requests.get(f'http://localhost/{pfx}').text
def test_no_routes(config_fixture, start_proxy): pr = ProxyRouter.get_proxy(config_fixture) assert type(pr) == ProxyRouter assert pr.routes == {}
def test_null1(): pr = ProxyRouter.get_proxy() assert type(pr) == NullRouter assert pr.is_null_proxy is True
def configure_mitmroute(cls, labbook: LabBook, router: ProxyRouter, username: str, _retry: Optional[bool] = False) -> Tuple[str, str]: """Ensure mitm is configured and proxied for labbook Args: labbook: the specific target running a dev tool router: The link to the configurable-proxy-router wrapper username: username of the logged in user _retry: (internal use only) is this a recursive call after clean-up? Returns: str that contains the mitm proxy endpoint as http://{ip}:{port} """ labbook_container_name = ContainerOperations.labbook_image_name( labbook, username) # Note that the use of rserver is not intrinsically meaningful - we could make this more generic # if mitmproxy supports multiple dev tools proxy_target = f'rserver/{labbook_container_name}/' mitm_key = cls.get_mitm_redis_key(labbook_container_name) redis_conn = redis.Redis(db=1) devtool_ip = ContainerOperations.get_labbook_ip(labbook, username) devtool_endpoint = f"http://{devtool_ip}:8787" for monitored_labbook_name in cls.get_running_proxies(): if (monitored_labbook_name != labbook_container_name) and \ (cls.get_devtool_endpoint(monitored_labbook_name) == devtool_endpoint): # Some other MITM proxy is pointing at our dev tool cls.stop_mitm_proxy(monitored_labbook_name) if not redis_conn.exists(mitm_key): # start mitm proxy if it's not configured yet mitm_endpoint = cls.start_mitm_proxy(devtool_endpoint, labbook_container_name) # add route rt_prefix, _ = router.add(mitm_endpoint, proxy_target) # Warning: RStudio will break if there is a trailing slash! suffix = f'/{rt_prefix}' elif _retry: # This should never happen - but it's better than the chance of infinite recursion raise ValueError( f'Unable to cleanly stop mitmproxy_proxy for {labbook_container_name}. Please try manually.' ) else: # Are we pointing to the correct dev tool endpoint? configured_devtool_endpoint = cls.get_devtool_endpoint( labbook_container_name) if devtool_endpoint != configured_devtool_endpoint: logger.warning( f'Existing RStudio mitmproxy_proxy for stale endpoint {configured_devtool_endpoint}, removing' ) cls.stop_mitm_proxy(labbook_container_name) return cls.configure_mitmroute(labbook, router, username, _retry=True) # Is the MITM endpoint configured in Redis? retval = cls.get_mitmendpoint(labbook_container_name) if retval is None: logger.warning( 'Existing RStudio mitmproxy_proxy is partially configured, removing' ) cls.stop_mitm_proxy(labbook_container_name) return cls.configure_mitmroute(labbook, router, username, _retry=True) else: mitm_endpoint = retval # existing route to MITM or not? matched_routes = router.get_matching_routes( mitm_endpoint, proxy_target) if len(matched_routes) == 1: suffix = matched_routes[0] elif len(matched_routes) == 0: logger.warning( 'Creating missing route for existing RStudio mitmproxy_proxy' ) rt_prefix, _ = router.add(mitm_endpoint, proxy_target) # Warning: RStudio will break if there is a trailing slash! suffix = f'/{rt_prefix}' else: raise ValueError( f"Multiple RStudio proxy instances for {str(labbook)}. Please restart the Project " "or manually delete stale containers.") # The endpoint plus proxy target constitute the full URL for an MITM-reverse-proxied RStudio return f"{mitm_endpoint}/{proxy_target}", suffix