def test_backend_addr_bad_value(url, exc_msg): with pytest.raises(ValueError) as exc: BackendAddr.from_url(url) if isinstance(exc_msg, str): assert str(exc.value) == exc_msg else: assert str(exc.value) in exc_msg
def validate(self, string, pos): try: if len(string) == 0: return QValidator.Intermediate, string, pos BackendAddr.from_url(string) return QValidator.Acceptable, string, pos except ValueError: return QValidator.Invalid, string, pos
def __init__(self, jobs_ctx, config, start_addr): super().__init__() self.setupUi(self) self.jobs_ctx = jobs_ctx self.config = config self.create_job = None self.dialog = None self.status = None self.device_widget = CreateOrgDeviceInfoWidget() self.device_widget.valid_info_entered.connect(self._on_info_valid) self.device_widget.invalid_info_entered.connect(self._on_info_invalid) self.user_widget = CreateOrgUserInfoWidget() self.user_widget.valid_info_entered.connect(self._on_info_valid) self.user_widget.invalid_info_entered.connect(self._on_info_invalid) self.main_layout.addWidget(self.user_widget) self.main_layout.addWidget(self.device_widget) self._on_previous_clicked() self.button_validate.setEnabled(False) self.button_previous.clicked.connect(self._on_previous_clicked) self.button_previous.hide() self.req_success.connect(self._on_req_success) self.req_error.connect(self._on_req_error) self.start_addr = start_addr if self.start_addr: self.user_widget.line_edit_org_name.setText( self.start_addr.organization_id) self.user_widget.line_edit_org_name.setReadOnly(True) self.label_instructions.setText( _("TEXT_BOOTSTRAP_ORGANIZATION_INSTRUCTIONS_organization"). format(organization=self.start_addr.organization_id)) # Not creating on the default server if (self.start_addr.hostname != config.preferred_org_creation_backend_addr.hostname or self.start_addr.port != config.preferred_org_creation_backend_addr.port): self.user_widget.radio_use_custom.setChecked(True) self.user_widget.radio_use_commercial.setDisabled(True) # Will not be used, it just makes the display prettier backend_addr = BackendAddr(self.start_addr.hostname, self.start_addr.port, self.start_addr.use_ssl) self.user_widget.line_edit_backend_addr.setText( backend_addr.to_url()) self.user_widget.line_edit_backend_addr.setDisabled(True) self.user_widget.line_edit_backend_addr.setCursorPosition(0) else: self.user_widget.radio_use_commercial.setChecked(True) self.user_widget.radio_use_custom.setDisabled(True)
def backend_addr(tcp_stream_spy, fixtures_customization, monkeypatch): # Depending on tcp_stream_spy fixture prevent from doing real connection # attempt (which can be long to resolve) when backend is not running use_ssl = fixtures_customization.get("backend_over_ssl", False) addr = BackendAddr(hostname="example.com", port=9999, use_ssl=use_ssl) if use_ssl: # TODO: Trustme & Windows doesn't seem to play well # (that and Python < 3.7 & Windows bug https://bugs.python.org/issue35941) if sys.platform == "win32": pytest.skip("Windows and Trustme are not friends :'(") # Create a ssl certificate and overload default ssl context generation ca = trustme.CA() cert = ca.issue_cert("*.example.com", "example.com") vanilla_create_default_context = ssl.create_default_context def patched_create_default_context(*args, **kwargs): ctx = vanilla_create_default_context(*args, **kwargs) ca.configure_trust(ctx) cert.configure_cert( ctx) # TODO: only server should load this part ? return ctx monkeypatch.setattr("ssl.create_default_context", patched_create_default_context) return addr
async def finalize(self) -> "PkiEnrollmentFinalizedCtx": """ Raises: PkiEnrollmentCertificateNotFoundError PkiEnrollmentCertificateCryptoError PkiEnrollmentCertificateError PkiEnrollmentCertificatePinCodeUnavailableError PkiEnrollmentLocalPendingCryptoError """ signing_key, private_key = await pki_enrollment_load_local_pending_secret_part( config_dir=self.config_dir, enrollment_id=self.enrollment_id) # Create the local device organization_addr = BackendOrganizationAddr.build( backend_addr=BackendAddr(self.addr.hostname, self.addr.port, self.addr.use_ssl), organization_id=self.addr.organization_id, root_verify_key=self.accept_payload.root_verify_key, ) new_device = generate_new_device( organization_addr=organization_addr, device_id=self.accept_payload.device_id, profile=self.accept_payload.profile, human_handle=self.accept_payload.human_handle, device_label=self.accept_payload.device_label, signing_key=signing_key, private_key=private_key, ) return PkiEnrollmentFinalizedCtx( config_dir=self.config_dir, enrollment_id=self.enrollment_id, new_device=new_device, x509_certificate=self.x509_certificate, )
async def _server_factory(entry_point, addr=None): nonlocal count count += 1 if not addr: addr = BackendAddr(hostname=f"server-{count}.localhost", port=9999, use_ssl=False) async with trio.open_service_nursery() as nursery: def connection_factory(*args, **kwargs): client_stream, server_stream = trio.testing.memory_stream_pair( ) nursery.start_soon(entry_point, server_stream) return client_stream tcp_stream_spy.push_hook(addr, connection_factory) try: yield AppServer(entry_point, addr, connection_factory) nursery.cancel_scope.cancel() finally: # It's important to remove the hook just after having cancelled # the nursery. Otherwise another coroutine trying to connect would # end up with a `RuntimeError('Nursery is closed to new arrivals',)` # given `connection_factory` make use of the now-closed nursery. tcp_stream_spy.pop_hook(addr)
def core_config(tmpdir, backend_addr, unused_tcp_port, fixtures_customization): if fixtures_customization.get("fake_preferred_org_creation_backend_addr", False): backend_addr = BackendAddr.from_url(f"parsec://127.0.0.1:{unused_tcp_port}") tmpdir = Path(tmpdir) return CoreConfig( config_dir=tmpdir / "config", data_base_dir=tmpdir / "data", mountpoint_base_dir=tmpdir / "mnt", preferred_org_creation_backend_addr=backend_addr, gui_language=fixtures_customization.get("gui_language"), )
async def _status_organization(organization_id: OrganizationID, backend_addr: BackendAddr, administration_token: str) -> None: url = backend_addr.to_http_domain_url( f"/administration/organizations/{organization_id}") rep_data = await http_request( url=url, method="GET", headers={"authorization": f"Bearer {administration_token}"}) cooked_rep_data = organization_config_rep_serializer.loads(rep_data) for key, value in cooked_rep_data.items(): click.echo(f"{key}: {value}")
def test_backend_organization_addr_good(base_url, expected, verify_key): org = OrganizationID("org") backend_addr = BackendAddr.from_url(base_url) addr = BackendOrganizationAddr.build(backend_addr, organization_id=org, root_verify_key=verify_key) assert addr.hostname == "foo" assert addr.port == expected["port"] assert addr.use_ssl == expected["ssl"] assert addr.organization_id == org assert addr.root_verify_key == verify_key addr2 = BackendOrganizationAddr.from_url(addr.to_url()) assert addr == addr2
def test_build_addrs(): backend_addr = BackendAddr.from_url(BackendAddrTestbed.url) assert backend_addr.hostname == "parsec.cloud.com" assert backend_addr.port == 443 assert backend_addr.use_ssl is True organization_id = OrganizationID("MyOrg") root_verify_key = SigningKey.generate().verify_key organization_addr = BackendOrganizationAddr.build( backend_addr=backend_addr, organization_id=organization_id, root_verify_key=root_verify_key ) assert organization_addr.organization_id == organization_id assert organization_addr.root_verify_key == root_verify_key organization_bootstrap_addr = BackendOrganizationBootstrapAddr.build( backend_addr=backend_addr, organization_id=organization_id, token="a0000000000000000000000000000001", ) assert organization_bootstrap_addr.token == "a0000000000000000000000000000001" assert organization_bootstrap_addr.organization_id == organization_id organization_bootstrap_addr2 = BackendOrganizationBootstrapAddr.build( backend_addr=backend_addr, organization_id=organization_id, token=None ) assert organization_bootstrap_addr2.organization_id == organization_id assert organization_bootstrap_addr2.token == "" organization_file_link_addr = BackendOrganizationFileLinkAddr.build( organization_addr=organization_addr, workspace_id=EntryID.from_hex("2d4ded12-7406-4608-833b-7f57f01156e2"), encrypted_path=b"<encrypted_payload>", ) assert organization_file_link_addr.workspace_id == EntryID.from_hex( "2d4ded12-7406-4608-833b-7f57f01156e2" ) assert organization_file_link_addr.encrypted_path == b"<encrypted_payload>" invitation_addr = BackendInvitationAddr.build( backend_addr=backend_addr, organization_id=organization_id, invitation_type=InvitationType.USER, token=InvitationToken.from_hex("a0000000000000000000000000000001"), ) assert invitation_addr.organization_id == organization_id assert invitation_addr.token == InvitationToken.from_hex("a0000000000000000000000000000001") assert invitation_addr.invitation_type == InvitationType.USER
async def create_organization_req(organization_id: OrganizationID, backend_addr: BackendAddr, administration_token: str) -> str: url = backend_addr.to_http_domain_url("/administration/organizations") data = organization_create_req_serializer.dumps( {"organization_id": organization_id}) rep_data = await http_request( url=url, method="POST", headers={"authorization": f"Bearer {administration_token}"}, data=data, ) cooked_rep_data = organization_create_rep_serializer.loads(rep_data) return cooked_rep_data["bootstrap_token"]
def backend_addr(async_fixture_backend_addr, unused_tcp_port): # Given server port is not known until `running_backend_unconfigured_server` # is ready, `backend_addr` should be an asynchronous fixture. # However `backend_addr` is a really common fixture and we want to be able # to use it event in the non-trio tests (for instance in hypothesis tests). # So we cheat by pretending `backend_addr` is a sync fixture and fallback to # a default addr value if we are in a non-trio test. if iscoroutine(async_fixture_backend_addr): # We are in a non-trio test, just close the coroutine and provide # an addr which is guaranteed to cause connection error async_fixture_backend_addr.close() # `use_ssl=False` is useful if this address is later modified by `correct_addr` return BackendAddr(hostname="127.0.0.1", port=unused_tcp_port, use_ssl=False) else: return async_fixture_backend_addr
async def restart_local_backend(administration_token, backend_port, email_host, db, blockstore): pattern = f"parsec.* backend.* run.* -P {backend_port}" command = ( f"{sys.executable} -Wignore -m parsec.cli backend run --log-level=WARNING " f"-b {blockstore} --db {db} " f"--email-host={email_host} -P {backend_port} " f"--spontaneous-organization-bootstrap " f"--administration-token {administration_token} --backend-addr parsec://localhost:{backend_port}?no_ssl=true" ) # Trio does not support subprocess in windows yet def _windows_target(): for proc in psutil.process_iter(): if "python" in proc.name(): arguments = " ".join(proc.cmdline()) if re.search(pattern, arguments): proc.kill() backend_process = subprocess.Popen(command.split(), stdout=subprocess.PIPE) for data in backend_process.stdout: print(data.decode(), end="") break backend_process.stdout.close() # Windows restart if sys.platform == "win32": await trio.to_thread.run_sync(_windows_target) # Linux restart else: await trio.run_process(["pkill", "-f", pattern], check=False) backend_process = await trio.open_process(command.split(), stdout=subprocess.PIPE) async with backend_process.stdout: async for data in backend_process.stdout: print(data.decode(), end="") break # Make sure the backend is actually started await trio.sleep(0.2) url = f"parsec://localhost:{backend_port}?no_ssl=true" return BackendAddr.from_url(url)
async def _anonymous_cmd(serializer, addr: BackendAddr, organization_id: OrganizationID, **req) -> dict: """ Raises: BackendNotAvailable BackendProtocolError """ logger.info("Request", cmd=req["cmd"]) try: raw_req = serializer.req_dumps(req) except ProtocolError as exc: logger.exception("Invalid request data", cmd=req["cmd"], error=exc) raise BackendProtocolError("Invalid request data") from exc url = addr.to_http_domain_url(f"/anonymous/{organization_id}") try: raw_rep = await http_request(url=url, method="POST", data=raw_req) except OSError as exc: logger.debug("Request failed (backend not available)", cmd=req["cmd"], exc_info=exc) raise BackendNotAvailable(exc) from exc try: rep = serializer.rep_loads(raw_rep) except ProtocolError as exc: logger.exception("Invalid response data", cmd=req["cmd"], error=exc) raise BackendProtocolError("Invalid response data") from exc if rep["status"] == "invalid_msg_format": logger.error("Invalid request data according to backend", cmd=req["cmd"], rep=rep) raise BackendProtocolError("Invalid request data according to backend") if rep["status"] == "bad_timestamp": raise BackendOutOfBallparkError(rep) return rep
def test_backend_organization_bootstrap_addr_good(base_url, expected, verify_key): org = OrganizationID("org") backend_addr = BackendAddr.from_url(base_url) addr = BackendOrganizationBootstrapAddr.build(backend_addr, org, "token-123") assert addr.hostname == "foo" assert addr.port == expected["port"] assert addr.use_ssl == expected["ssl"] assert addr.organization_id == org assert addr.token == "token-123" addr2 = BackendOrganizationBootstrapAddr.from_url(str(addr)) assert addr == addr2 org_addr = addr.generate_organization_addr(verify_key) assert isinstance(org_addr, BackendOrganizationAddr) assert org_addr.root_verify_key == verify_key assert org_addr.hostname == addr.hostname assert org_addr.port == addr.port assert org_addr.use_ssl == addr.use_ssl assert org_addr.organization_id == addr.organization_id
async def restart_local_backend(administration_token, backend_port): pattern = f"parsec.* backend.* run.* -P {backend_port}" command = ( f"python -Wignore -m parsec.cli backend run -b MOCKED --db MOCKED " f"-P {backend_port} --administration-token {administration_token}") # Trio does not support subprocess in windows yet def _windows_target(): for proc in psutil.process_iter(): if "python" in proc.name(): arguments = " ".join(proc.cmdline()) if re.search(pattern, arguments): proc.kill() backend_process = subprocess.Popen(command.split(), stdout=subprocess.PIPE) for data in backend_process.stdout: print(data.decode(), end="") break backend_process.stdout.close() # Windows restart if os.name == "nt" or True: await trio.to_thread.run_sync(_windows_target) # Linux restart else: await trio.run_process(["pkill", "-f", pattern], check=False) backend_process = await trio.open_process(command.split(), stdout=subprocess.PIPE) async with backend_process.stdout: async for data in backend_process.stdout: print(data.decode(), end="") break # Make sure the backend is actually started await trio.sleep(0.2) url = f"parsec://localhost:{backend_port}?no_ssl=true" return BackendAddr.from_url(url)
async def cli_with_running_backend_testbed(backend, *devices): # Must use real TCP sockets instead of the tcp_stream_spy here, this is required # given the cli commands are going to run in a separate thread with they own # trio loop. Hence sharing memory channel between trio loops is going to create # unexpected errors ! async with trio.open_service_nursery() as nursery: listeners = await nursery.start( partial(trio.serve_tcp, backend.handle_client, port=0, host="127.0.0.1")) _, port, *_ = listeners[0].socket.getsockname() backend_addr = BackendAddr("127.0.0.1", port, use_ssl=False) # Now the local device point to an invalid backend address, must fix this def _correct_local_device_backend_addr(device): organization_addr = BackendOrganizationAddr.build( backend_addr, organization_id=device.organization_addr.organization_id, root_verify_key=device.organization_addr.root_verify_key, ) return LocalDevice( organization_addr=organization_addr, device_id=device.device_id, device_label=device.device_label, human_handle=device.human_handle, signing_key=device.signing_key, private_key=device.private_key, profile=device.profile, user_manifest_id=device.user_manifest_id, user_manifest_key=device.user_manifest_key, local_symkey=device.local_symkey, ) yield (backend_addr, ) + tuple( _correct_local_device_backend_addr(d) for d in devices) nursery.cancel_scope.cancel()
async def async_fixture_backend_addr(running_backend_port_known, fixtures_customization): port = await running_backend_port_known() use_ssl = fixtures_customization.get("backend_over_ssl", False) return BackendAddr(hostname="127.0.0.1", port=port, use_ssl=use_ssl)
def backend_addr(tcp_stream_spy): # Depending on tcp_stream_spy fixture prevent from doing real connection # attempt (which can be long to resolve) when backend is not running return BackendAddr(hostname="example.com", port=9999, use_ssl=False)
def backend_addr(self): return (BackendAddr.from_url(self.line_edit_backend_addr.text(), allow_http_redirection=True) if self.radio_use_custom.isChecked() else None)
def test_backend_addr_bad_value(url, exc_msg): with pytest.raises(ValueError) as exc: BackendAddr.from_url(url) assert str(exc.value) == exc_msg
def backend_addr(self): return (BackendAddr.from_url(self.line_edit_backend_addr.text()) if self.check_use_custom_backend.isChecked() else None)
def config_factory( config_dir: Path = None, data_base_dir: Path = None, cache_base_dir: Path = None, mountpoint_base_dir: Path = None, prevent_sync_pattern_path: Optional[Path] = None, mountpoint_enabled: bool = False, disabled_workspaces: FrozenSet[EntryID] = frozenset(), backend_max_cooldown: int = 30, backend_connection_keepalive: Optional[int] = 29, backend_max_connections: int = 4, telemetry_enabled: bool = True, debug: bool = False, gui_last_device: str = None, gui_tray_enabled: bool = True, gui_language: str = None, gui_first_launch: bool = True, gui_last_version: str = None, gui_check_version_at_startup: bool = True, gui_check_version_allow_pre_release: bool = False, gui_workspace_color: bool = False, gui_allow_multiple_instances: bool = False, preferred_org_creation_backend_addr: Optional[BackendAddr] = None, gui_show_confined: bool = False, gui_geometry: bytes = None, environ: dict = {}, **_, ) -> CoreConfig: # The environment variable we always be used first, and if it is not present, # we'll use the value from the configuration file. backend_addr_env = environ.get("PREFERRED_ORG_CREATION_BACKEND_ADDR") if backend_addr_env: preferred_org_creation_backend_addr = BackendAddr.from_url( backend_addr_env) if not preferred_org_creation_backend_addr: preferred_org_creation_backend_addr = BackendAddr.from_url( "parsec://localhost:6777?no_ssl=true") data_base_dir = data_base_dir or get_default_data_base_dir(environ) core_config = CoreConfig( config_dir=config_dir or get_default_config_dir(environ), data_base_dir=data_base_dir, cache_base_dir=cache_base_dir or get_default_cache_base_dir(environ), mountpoint_base_dir=get_default_mountpoint_base_dir(environ), prevent_sync_pattern_path=prevent_sync_pattern_path, mountpoint_enabled=mountpoint_enabled, disabled_workspaces=disabled_workspaces, backend_max_cooldown=backend_max_cooldown, backend_connection_keepalive=backend_connection_keepalive, backend_max_connections=backend_max_connections, telemetry_enabled=telemetry_enabled, debug=debug, sentry_url=environ.get("SENTRY_URL") or None, gui_last_device=gui_last_device, gui_tray_enabled=gui_tray_enabled, gui_language=gui_language, gui_first_launch=gui_first_launch, gui_last_version=gui_last_version, gui_check_version_at_startup=gui_check_version_at_startup, gui_check_version_allow_pre_release=gui_check_version_allow_pre_release, gui_workspace_color=gui_workspace_color, gui_allow_multiple_instances=gui_allow_multiple_instances, preferred_org_creation_backend_addr=preferred_org_creation_backend_addr, gui_show_confined=gui_show_confined, gui_geometry=gui_geometry, ipc_socket_file=data_base_dir / "parsec-cloud.lock", ipc_win32_mutex_name="parsec-cloud", ) # Make sure the directories exist on the system core_config.config_dir.mkdir(mode=0o700, parents=True, exist_ok=True) core_config.data_base_dir.mkdir(mode=0o700, parents=True, exist_ok=True) core_config.cache_base_dir.mkdir(mode=0o700, parents=True, exist_ok=True) # Mountpoint base directory is not used on windows if os.name != "nt": core_config.mountpoint_base_dir.mkdir(mode=0o700, parents=True, exist_ok=True) return core_config
def load_config(config_dir: Path, **extra_config) -> CoreConfig: config_file = config_dir / "config.json" try: raw_conf = config_file.read_text() data_conf = json.loads(raw_conf) except OSError: # Config file not created yet, fallback to default data_conf = {} except (ValueError, json.JSONDecodeError) as exc: # Config file broken, fallback to default logger.warning(f"Ignoring invalid config in {config_file} ({exc})") data_conf = {} try: data_conf["data_base_dir"] = Path(data_conf["data_base_dir"]) except (KeyError, ValueError): pass try: data_conf["cache_base_dir"] = Path(data_conf["cache_base_dir"]) except (KeyError, ValueError): pass try: data_conf["prevent_sync_pattern_path"] = Path( data_conf["prevent_sync_pattern_path"]) except (KeyError, ValueError): pass try: data_conf["disabled_workspaces"] = frozenset( map(EntryID, data_conf["disabled_workspaces"])) except (KeyError, ValueError): pass try: data_conf[ "preferred_org_creation_backend_addr"] = BackendAddr.from_url( data_conf["preferred_org_creation_backend_addr"]) except KeyError: pass except ValueError as exc: logger.warning( f"Invalid value for `preferred_org_creation_backend_addr` ({exc})") data_conf["preferred_org_creation_backend_addr"] = None try: data_conf["gui_geometry"] = base64.b64decode( data_conf["gui_geometry"].encode("ascii")) except (AttributeError, KeyError, UnicodeEncodeError, binascii.Error): data_conf["gui_geometry"] = None # Work around versionning issue with parsec releases: # - v1.12.0, v1.11.4, v1.11.3, v1.11.2, v1.11.1, v1.11.0 and v1.10.0 # A `v` has been incorrectly added to `parsec.__version__`, potentially # affecting the `gui_last_version` entry in the configuration file. if data_conf.get("gui_last_version"): data_conf["gui_last_version"] = data_conf["gui_last_version"].lstrip( "v") return config_factory(config_dir=config_dir, **data_conf, **extra_config, environ=os.environ)
def test_backend_addr_good(url, expected): addr = BackendAddr.from_url(url) assert addr.hostname == "foo" assert addr.port == expected["port"] assert addr.use_ssl == expected["ssl"]