lzc-sdk 0.1.1

Rust SDK for LazyCat application and device APIs
Documentation
use std::sync::Arc;

use http::Uri;
use hyper_util::rt::TokioIo;
use lzc_sdk::proto::sys::h_portal_sys_server::{HPortalSys, HPortalSysServer};
use lzc_sdk::proto::sys::{
    ChangeRoleReqeust, ChangeTrustEndDeviceReply, ChangeTrustEndDeviceRequest,
    CheckPasswordRequest, CreateUserRequest, DeleteUserRequest, EmitBoxServiceChangedRequest,
    GetPasswordHashRequest, GetPasswordHashResponse, HServerInfo, ListEndDeviceReply,
    ListEndDeviceRequest, ListUsersReply, PeersInfo, QueryBoxServicePeerCredRequest,
    QueryBoxServicePeerCredResponse, QueryRoleReply, RegisterBoxServiceReply,
    RegisterBoxServiceRequest, RemoteSocksReply, RemoteSocksRequest, ResetHServerReply,
    ResetHServerRequest, ResetPasswordRequest, SetPasswordHashRequest, SetRelayRequest,
    SetupHServerReply, SetupHServerRequest, UserId, remote_socks_request,
};
use lzc_sdk::{Error, HServerClient, RemoteSocksEndpoint};
use tempfile::tempdir;
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::Mutex;
use tokio_stream::wrappers::{ReceiverStream, UnixListenerStream};
use tonic::transport::{Channel, Endpoint, Server};
use tonic::{Request, Response, Status};
use tower::service_fn;

#[tokio::test]
async fn hserver_uses_uds_and_maps_local_and_remote_locations() {
    let fixture = PortalFixture::default();
    let (_directory, channel, server) = start_portal(fixture.clone()).await;
    let portal = HServerClient::from_channel(channel);

    let local = portal
        .remote_socks_endpoint("")
        .await
        .expect("local SOCKS endpoint");
    assert_eq!(local.authority(), "127.0.0.1:1080");
    assert!(!local.resolves_hostname_remotely());

    let remote = portal
        .remote_socks_endpoint("peer-42")
        .await
        .expect("remote SOCKS endpoint");
    assert_eq!(remote.authority(), "[::1]:2080");
    assert!(remote.resolves_hostname_remotely());

    let requests = fixture.requests.lock().await;
    assert_eq!(
        requests.as_slice(),
        [
            RemoteSocksRequest {
                location_type: remote_socks_request::LocationType::Local.into(),
                target: String::new(),
            },
            RemoteSocksRequest {
                location_type: remote_socks_request::LocationType::Remote.into(),
                target: "peer-42".to_owned(),
            },
        ]
    );
    server.abort();
}

#[tokio::test]
async fn hserver_exposes_the_complete_generated_client() {
    let fixture = PortalFixture::default();
    let (_directory, channel, server) = start_portal(fixture).await;
    let portal = HServerClient::from_channel(channel);

    let info = portal
        .client()
        .query_h_server_info(())
        .await
        .expect("query HServer info")
        .into_inner();
    assert_eq!(info.box_id, "box-fixture");
    server.abort();
}

#[tokio::test]
async fn hserver_rejects_invalid_remote_socks_endpoints() {
    let fixture = PortalFixture::default();
    let (_directory, channel, server) = start_portal(fixture).await;
    let portal = HServerClient::from_channel(channel);

    let error = portal
        .remote_socks_endpoint("invalid-endpoint")
        .await
        .expect_err("HTTP endpoint must be rejected");
    assert!(matches!(error, Error::InvalidRemoteSocksEndpoint));
    assert!("socks5://127.0.0.1".parse::<RemoteSocksEndpoint>().is_err());
    server.abort();
}

async fn start_portal(
    fixture: PortalFixture,
) -> (tempfile::TempDir, Channel, tokio::task::JoinHandle<()>) {
    let directory = tempdir().expect("tempdir");
    let socket_path = Arc::new(directory.path().join("portal.socket"));
    let listener = UnixListener::bind(socket_path.as_ref()).expect("bind portal socket");
    let server = tokio::spawn(async move {
        Server::builder()
            .add_service(HPortalSysServer::new(fixture))
            .serve_with_incoming(UnixListenerStream::new(listener))
            .await
            .expect("serve HPortalSys fixture");
    });

    let endpoint = Endpoint::from_static("http://lazycat-hportal");
    let channel = endpoint
        .connect_with_connector(service_fn(move |_: Uri| {
            let socket_path = Arc::clone(&socket_path);
            async move {
                UnixStream::connect(socket_path.as_ref())
                    .await
                    .map(TokioIo::new)
            }
        }))
        .await
        .expect("connect portal UDS");
    (directory, channel, server)
}

#[derive(Clone, Default)]
struct PortalFixture {
    requests: Arc<Mutex<Vec<RemoteSocksRequest>>>,
}

#[tonic::async_trait]
impl HPortalSys for PortalFixture {
    async fn query_h_server_info(
        &self,
        _request: Request<()>,
    ) -> Result<Response<HServerInfo>, Status> {
        Ok(Response::new(HServerInfo {
            box_id: "box-fixture".to_owned(),
            ..HServerInfo::default()
        }))
    }

    async fn remote_socks(
        &self,
        request: Request<RemoteSocksRequest>,
    ) -> Result<Response<RemoteSocksReply>, Status> {
        let request = request.into_inner();
        let server_url = match request.target.as_str() {
            "" => "socks5://127.0.0.1:1080",
            "invalid-endpoint" => "http://127.0.0.1:1080",
            _ => "socks5h://[::1]:2080",
        };
        self.requests.lock().await.push(request);
        Ok(Response::new(RemoteSocksReply {
            server_url: server_url.to_owned(),
        }))
    }

    type RegisterBoxServiceStream = ReceiverStream<Result<RegisterBoxServiceReply, Status>>;

    async fn list_users(&self, _request: Request<()>) -> Result<Response<ListUsersReply>, Status> {
        Err(Status::unimplemented("list_users"))
    }

    async fn create_user(
        &self,
        _request: Request<CreateUserRequest>,
    ) -> Result<Response<()>, Status> {
        Err(Status::unimplemented("create_user"))
    }

    async fn delete_user(
        &self,
        _request: Request<DeleteUserRequest>,
    ) -> Result<Response<()>, Status> {
        Err(Status::unimplemented("delete_user"))
    }

    async fn reset_password(
        &self,
        _request: Request<ResetPasswordRequest>,
    ) -> Result<Response<()>, Status> {
        Err(Status::unimplemented("reset_password"))
    }

    async fn check_password(
        &self,
        _request: Request<CheckPasswordRequest>,
    ) -> Result<Response<()>, Status> {
        Err(Status::unimplemented("check_password"))
    }

    async fn get_password_hash(
        &self,
        _request: Request<GetPasswordHashRequest>,
    ) -> Result<Response<GetPasswordHashResponse>, Status> {
        Err(Status::unimplemented("get_password_hash"))
    }

    async fn set_password_hash(
        &self,
        _request: Request<SetPasswordHashRequest>,
    ) -> Result<Response<()>, Status> {
        Err(Status::unimplemented("set_password_hash"))
    }

    async fn query_role(
        &self,
        _request: Request<UserId>,
    ) -> Result<Response<QueryRoleReply>, Status> {
        Err(Status::unimplemented("query_role"))
    }

    async fn change_role(
        &self,
        _request: Request<ChangeRoleReqeust>,
    ) -> Result<Response<()>, Status> {
        Err(Status::unimplemented("change_role"))
    }

    async fn change_trust_end_device(
        &self,
        _request: Request<ChangeTrustEndDeviceRequest>,
    ) -> Result<Response<ChangeTrustEndDeviceReply>, Status> {
        Err(Status::unimplemented("change_trust_end_device"))
    }

    async fn list_end_devices(
        &self,
        _request: Request<ListEndDeviceRequest>,
    ) -> Result<Response<ListEndDeviceReply>, Status> {
        Err(Status::unimplemented("list_end_devices"))
    }

    async fn setup_h_server(
        &self,
        _request: Request<SetupHServerRequest>,
    ) -> Result<Response<SetupHServerReply>, Status> {
        Err(Status::unimplemented("setup_h_server"))
    }

    async fn reset_h_server(
        &self,
        _request: Request<ResetHServerRequest>,
    ) -> Result<Response<ResetHServerReply>, Status> {
        Err(Status::unimplemented("reset_h_server"))
    }

    async fn register_box_service(
        &self,
        _request: Request<RegisterBoxServiceRequest>,
    ) -> Result<Response<Self::RegisterBoxServiceStream>, Status> {
        Err(Status::unimplemented("register_box_service"))
    }

    async fn emit_box_service_changed(
        &self,
        _request: Request<EmitBoxServiceChangedRequest>,
    ) -> Result<Response<()>, Status> {
        Err(Status::unimplemented("emit_box_service_changed"))
    }

    async fn query_box_service_peer_cred(
        &self,
        _request: Request<QueryBoxServicePeerCredRequest>,
    ) -> Result<Response<QueryBoxServicePeerCredResponse>, Status> {
        Err(Status::unimplemented("query_box_service_peer_cred"))
    }

    async fn set_relay(&self, _request: Request<SetRelayRequest>) -> Result<Response<()>, Status> {
        Err(Status::unimplemented("set_relay"))
    }

    async fn dump_peers(&self, _request: Request<()>) -> Result<Response<PeersInfo>, Status> {
        Err(Status::unimplemented("dump_peers"))
    }
}