diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs index aba4cdcfb..cf55c0ce4 100644 --- a/core/src/api/mod.rs +++ b/core/src/api/mod.rs @@ -85,7 +85,6 @@ pub struct SanitisedNodeConfig { pub name: String, pub identity: RemoteIdentity, pub p2p: NodeConfigP2P, - pub p2p_discovery: P2PDiscoveryState, pub features: Vec, pub preferences: NodePreferences, pub image_labeler_version: Option, @@ -98,7 +97,6 @@ impl From for SanitisedNodeConfig { name: value.name, identity: value.identity.to_remote_identity(), p2p: value.p2p, - p2p_discovery: value.p2p_discovery, features: value.features, preferences: value.preferences, image_labeler_version: value.image_labeler_version, diff --git a/core/src/api/nodes.rs b/core/src/api/nodes.rs index dd013a2fc..b5be20f3d 100644 --- a/core/src/api/nodes.rs +++ b/core/src/api/nodes.rs @@ -23,6 +23,7 @@ pub(crate) fn mount() -> AlphaRouter { pub p2p_ipv4_enabled: Option, pub p2p_ipv6_enabled: Option, pub p2p_discovery: Option, + pub p2p_remote_access: Option, pub image_labeler_version: Option, } R.mutation(|node, args: ChangeNodeNameArgs| async move { @@ -53,6 +54,12 @@ pub(crate) fn mount() -> AlphaRouter { if let Some(enabled) = args.p2p_ipv6_enabled { config.p2p.ipv6 = enabled; }; + if let Some(discovery) = args.p2p_discovery { + config.p2p.discovery = discovery; + }; + if let Some(remote_access) = args.p2p_remote_access { + config.p2p.remote_access = remote_access; + }; #[cfg(feature = "ai")] if let Some(version) = args.image_labeler_version { diff --git a/core/src/node/config.rs b/core/src/node/config.rs index e975f2f28..25e225af7 100644 --- a/core/src/node/config.rs +++ b/core/src/node/config.rs @@ -65,22 +65,32 @@ fn skip_if_true(value: &bool) -> bool { *value } +fn skip_if_false(value: &bool) -> bool { + !*value +} + #[derive(Debug, Clone, Serialize, Deserialize, Type)] pub struct NodeConfigP2P { + #[serde(default)] + pub discovery: P2PDiscoveryState, #[serde(default, skip_serializing_if = "Port::is_random")] pub port: Port, #[serde(default = "default_as_true", skip_serializing_if = "skip_if_true")] pub ipv4: bool, #[serde(default = "default_as_true", skip_serializing_if = "skip_if_true")] pub ipv6: bool, + #[serde(default, skip_serializing_if = "skip_if_false")] + pub remote_access: bool, } impl Default for NodeConfigP2P { fn default() -> Self { Self { + discovery: P2PDiscoveryState::Everyone, port: Port::Random, ipv4: true, ipv6: true, + remote_access: false, } } } @@ -102,8 +112,6 @@ pub struct NodeConfig { /// P2P config #[serde(default)] pub p2p: NodeConfigP2P, - #[serde(default)] - pub p2p_discovery: P2PDiscoveryState, /// Feature flags enabled on the node #[serde(default)] pub features: Vec, @@ -186,7 +194,6 @@ impl ManagedVersion for NodeConfig { name, identity: Identity::default(), p2p: NodeConfigP2P::default(), - p2p_discovery: P2PDiscoveryState::Everyone, version: Self::LATEST_VERSION, features: vec![], notifications: vec![], diff --git a/core/src/p2p/manager.rs b/core/src/p2p/manager.rs index 8919b27f9..8bffca74c 100644 --- a/core/src/p2p/manager.rs +++ b/core/src/p2p/manager.rs @@ -184,7 +184,7 @@ impl P2PManager { .ipv6 = Some(err.to_string()); } - let should_revert = match config.p2p_discovery { + let should_revert = match config.p2p.discovery { P2PDiscoveryState::Everyone // TODO: Make `ContactsOnly` work | P2PDiscoveryState::ContactsOnly => { @@ -225,7 +225,7 @@ impl P2PManager { if should_revert { let _ = self .node_config - .write(|c| c.p2p_discovery = P2PDiscoveryState::Disabled) + .write(|c| c.p2p.discovery = P2PDiscoveryState::Disabled) .await; } } @@ -293,8 +293,6 @@ async fn start( let mut service = unwrap_infallible(service.call(()).await); tokio::spawn(async move { - println!("APPLICATION GOT STREAM: {:?}", stream); // TODO - let Ok(header) = Header::from_stream(&mut stream).await.map_err(|err| { error!("Failed to read header from stream: {}", err); }) else { @@ -348,7 +346,8 @@ async fn start( } Header::Http => { let remote = stream.remote_identity(); - let Err(err) = operations::rspc::receiver(stream, &mut service).await else { + let Err(err) = operations::rspc::receiver(stream, &mut service, &node).await + else { return; }; diff --git a/core/src/p2p/operations/rspc.rs b/core/src/p2p/operations/rspc.rs index d6823eed1..25396c0bd 100644 --- a/core/src/p2p/operations/rspc.rs +++ b/core/src/p2p/operations/rspc.rs @@ -6,7 +6,7 @@ use sd_p2p::{RemoteIdentity, UnicastStream, P2P}; use tokio::io::AsyncWriteExt; use tracing::debug; -use crate::p2p::Header; +use crate::{p2p::Header, Node}; /// Transfer an rspc query to a remote node. #[allow(unused)] @@ -37,6 +37,7 @@ pub async fn remote_rspc( pub(crate) async fn receiver( stream: UnicastStream, service: &mut Router, + node: &Node, ) -> Result<(), Box> { debug!( "Received http request from peer '{}'", @@ -45,8 +46,8 @@ pub(crate) async fn receiver( // TODO: Authentication #[allow(clippy::todo)] - if true { - todo!("You wouldn't download a car!"); + if node.config.get().await.p2p.remote_access { + todo!("No way buddy!"); } Http::new() diff --git a/interface/app/$libraryId/settings/client/general.tsx b/interface/app/$libraryId/settings/client/general.tsx index 25d1b6d36..4a850f276 100644 --- a/interface/app/$libraryId/settings/client/general.tsx +++ b/interface/app/$libraryId/settings/client/general.tsx @@ -5,6 +5,7 @@ import { useBridgeQuery, useConnectedPeers, useDebugState, + useFeatureFlag, useZodForm } from '@sd/client'; import { Button, Card, Input, Select, SelectOption, Slider, Switch, toast, tw, z } from '@sd/ui'; @@ -59,6 +60,14 @@ export const Component = () => { ]), p2p_ipv4_enabled: z.boolean().optional(), p2p_ipv6_enabled: z.boolean().optional(), + p2p_discovery: z + .union([ + z.literal('Everyone'), + z.literal('ContactsOnly'), + z.literal('Disabled') + ]) + .optional(), + p2p_remote_access: z.boolean().optional(), image_labeler_version: z.string().optional(), background_processing_percentage: z.coerce .number({ @@ -75,6 +84,8 @@ export const Component = () => { p2p_port: node.data?.p2p.port || { type: 'random' }, p2p_ipv4_enabled: node.data?.p2p.ipv4 || true, p2p_ipv6_enabled: node.data?.p2p.ipv6 || true, + p2p_discovery: node.data?.p2p.discovery || 'Everyone', + p2p_remote_access: node.data?.p2p.remote_access || false, image_labeler_version: node.data?.image_labeler_version ?? undefined, background_processing_percentage: node.data?.preferences.thumbnailer.background_processing_percentage || 50 @@ -92,9 +103,8 @@ export const Component = () => { p2p_port: (value.p2p_port as any) ?? null, p2p_ipv4_enabled: value.p2p_ipv4_enabled ?? null, p2p_ipv6_enabled: value.p2p_ipv6_enabled ?? null, - p2p_discovery: null, - // p2p_port: value.customOrDefault === 'Default' ? 0 : Number(value.p2p_port), - // p2p_enabled: value.p2p_enabled ?? null, + p2p_discovery: value.p2p_discovery ?? null, + p2p_remote_access: value.p2p_remote_access ?? null, image_labeler_version: value.image_labeler_version ?? null }); @@ -116,6 +126,8 @@ export const Component = () => { const { t } = useLocale(); + const isP2PWipFeatureEnabled = useFeatureFlag('wipP2P'); + return ( { } /> + + {isP2PWipFeatureEnabled && ( + <> + + {t('spacedrop_description')} +

+ } + > + +
+ + +

+ {t('remote_access_description')} +

+

+ WARNING: This protocol has no security at the moment + and effectively gives root access! +

+ + } + > + + form.setValue('p2p_remote_access', checked) + } + /> +
+ + )} ) : null} diff --git a/interface/locales/en/common.json b/interface/locales/en/common.json index a73c138d3..bcd38d736 100644 --- a/interface/locales/en/common.json +++ b/interface/locales/en/common.json @@ -139,6 +139,12 @@ "enable_networking": "Enable Networking", "enable_networking_description": "Allow your node to communicate with other Spacedrive nodes around you.", "enable_networking_description_required": "Required for library sync or Spacedrop!", + "spacedrop": "Spacedrop visibility", + "spacedrop_everyone": "Everyone", + "spacedrop_contacts_only": "Contacts Only", + "spacedrop_disabled": "Disabled", + "remote_access": "Enable remote access", + "remote_access_description": "Enable other nodes to directly connect to this node.", "encrypt": "Encrypt", "encrypt_library": "Encrypt Library", "encrypt_library_coming_soon": "Library encryption coming soon", diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 3da3a6c63..c38ecdade 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -169,7 +169,7 @@ export type CacheNode = { __type: string; __id: string; "#node": any } export type CameraData = { device_make: string | null; device_model: string | null; color_space: string | null; color_profile: ColorProfile | null; focal_length: number | null; shutter_speed: number | null; flash: Flash | null; orientation: Orientation; lens_make: string | null; lens_model: string | null; bit_depth: number | null; red_eye: boolean | null; zoom: number | null; iso: number | null; software: string | null; serial_number: string | null; lens_serial_number: string | null; contrast: number | null; saturation: number | null; sharpness: number | null; composite: Composite | null } -export type ChangeNodeNameArgs = { name: string | null; p2p_port: Port | null; p2p_ipv4_enabled: boolean | null; p2p_ipv6_enabled: boolean | null; p2p_discovery: P2PDiscoveryState | null; image_labeler_version: string | null } +export type ChangeNodeNameArgs = { name: string | null; p2p_port: Port | null; p2p_ipv4_enabled: boolean | null; p2p_ipv6_enabled: boolean | null; p2p_discovery: P2PDiscoveryState | null; p2p_remote_access: boolean | null; image_labeler_version: string | null } export type CloudInstance = { id: string; uuid: string; identity: RemoteIdentity; nodeId: string; metadata: { [key in string]: string } } @@ -450,7 +450,7 @@ export type MediaLocation = { latitude: number; longitude: number; pluscode: Plu export type MediaMetadata = ({ type: "Image" } & ImageMetadata) | ({ type: "Video" } & VideoMetadata) | ({ type: "Audio" } & AudioMetadata) -export type NodeConfigP2P = { port: Port; ipv4: boolean; ipv6: boolean } +export type NodeConfigP2P = { discovery?: P2PDiscoveryState; port: Port; ipv4: boolean; ipv6: boolean; remote_access: boolean } export type NodePreferences = { thumbnailer: ThumbnailerPreferences } @@ -462,7 +462,7 @@ id: string; /** * name is the display name of the current node. This is set by the user and is shown in the UI. // TODO: Length validation so it can fit in DNS record */ -name: string; identity: RemoteIdentity; p2p: NodeConfigP2P; p2p_discovery: P2PDiscoveryState; features: BackendFeature[]; preferences: NodePreferences; image_labeler_version: string | null }) & { data_path: string; device_model: string | null } +name: string; identity: RemoteIdentity; p2p: NodeConfigP2P; features: BackendFeature[]; preferences: NodePreferences; image_labeler_version: string | null }) & { data_path: string; device_model: string | null } export type NonIndexedPathItem = { path: string; name: string; extension: string; kind: number; is_dir: boolean; date_created: string; date_modified: string; size_in_bytes_bytes: number[]; hidden: boolean } diff --git a/packages/client/src/stores/featureFlags.tsx b/packages/client/src/stores/featureFlags.tsx index 9591f3ca9..4001c6f23 100644 --- a/packages/client/src/stores/featureFlags.tsx +++ b/packages/client/src/stores/featureFlags.tsx @@ -11,7 +11,8 @@ export const features = [ 'solidJsDemo', 'hostedLocations', 'debugDragAndDrop', - 'searchTargetSwitcher' + 'searchTargetSwitcher', + 'wipP2P' ] as const; // This defines which backend feature flags show up in the UI.