initial commit

This commit is contained in:
mycognosist 2021-12-02 15:12:52 +02:00
commit 143f70b921
9 changed files with 1522 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1025
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

18
Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "golgi"
version = "0.1.0"
authors = ["glyph <glyph@mycelial.technology>"]
edition = "2021"
[dependencies]
async-std = "1.10.0"
base64 = "0.13.0"
futures = "0.3.18"
hex = "0.4.3"
kuska-handshake = { version = "0.2.0", features = ["async_std"] }
kuska-sodiumoxide = "0.2.5-0"
# waiting for a pr merge upstream
kuska-ssb = { path = "../ssb" }
# try to replace with miniserde
serde = "1"
serde_json = "1"

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# golgi
_A Scuttlebutt client written in Rust_
An experimental client which uses the [kuska-ssb](https://github.com/Kuska-ssb) libraries and aims to provide a high-level API for interacting with an sbot instance. Development efforts are currently oriented towards [go-sbot](https://github.com/cryptoscope/ssb) interoperability.
## Example Usage
```rust
pub async fn run() -> Result<(), GolgiError> {
let mut sbot_client = Sbot::init(None, None).await?;
let id = sbot_client.whoami().await?;
println!("{}", id);
}
```

106
src/error.rs Normal file
View File

@ -0,0 +1,106 @@
/*
use async_std::{io::Read, net::TcpStream};
use std::fmt::Debug;
use kuska_handshake::async_std::BoxStream;
use kuska_sodiumoxide::crypto::sign::ed25519;
use kuska_ssb::api::{dto::WhoAmIOut, ApiCaller};
use kuska_ssb::discovery;
use kuska_ssb::keystore;
use kuska_ssb::keystore::OwnedIdentity;
use kuska_ssb::rpc::{RecvMsg, RequestNo, RpcReader, RpcWriter};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
pub fn whoami_res_parse(body: &[u8]) -> Result<WhoAmIOut> {
Ok(serde_json::from_slice(body)?)
}
*/
#[derive(Debug)]
pub enum GolgiError {
DecodeBase64(base64::DecodeError),
Io {
source: std::io::Error,
context: String,
},
Handshake(kuska_handshake::async_std::Error),
KuskaApi(kuska_ssb::api::Error),
KuskaFeed(kuska_ssb::feed::Error),
KuskaRpc(kuska_ssb::rpc::Error),
// error message returned from the go-sbot
Sbot(String),
SerdeJson(serde_json::Error),
WhoAmI(String),
}
impl std::error::Error for GolgiError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
GolgiError::DecodeBase64(ref err) => Some(err),
GolgiError::Io { ref source, .. } => Some(source),
GolgiError::Handshake(_) => None,
GolgiError::KuskaApi(ref err) => Some(err),
GolgiError::KuskaFeed(ref err) => Some(err),
GolgiError::KuskaRpc(ref err) => Some(err),
GolgiError::Sbot(_) => None,
GolgiError::SerdeJson(ref err) => Some(err),
GolgiError::WhoAmI(_) => None,
}
}
}
impl std::fmt::Display for GolgiError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
// TODO: add context (what were we trying to decode?)
GolgiError::DecodeBase64(_) => write!(f, "Failed to decode base64"),
GolgiError::Io { ref context, .. } => write!(f, "IO error: {}", context),
GolgiError::Handshake(ref err) => write!(f, "{}", err),
GolgiError::KuskaApi(_) => write!(f, "SSB API failure"),
GolgiError::KuskaFeed(_) => write!(f, "SSB feed error"),
// TODO: improve this variant with a context message
// then have the core display msg be: "SSB RPC error: {}", context
GolgiError::KuskaRpc(_) => write!(f, "SSB RPC failure"),
GolgiError::Sbot(ref err) => write!(f, "Sbot returned an error response: {}", err),
GolgiError::SerdeJson(_) => write!(f, "Failed to serialize JSON slice"),
GolgiError::WhoAmI(ref err) => write!(f, "{}", err),
}
}
}
impl From<base64::DecodeError> for GolgiError {
fn from(err: base64::DecodeError) -> Self {
GolgiError::DecodeBase64(err)
}
}
impl From<kuska_handshake::async_std::Error> for GolgiError {
fn from(err: kuska_handshake::async_std::Error) -> Self {
GolgiError::Handshake(err)
}
}
impl From<kuska_ssb::api::Error> for GolgiError {
fn from(err: kuska_ssb::api::Error) -> Self {
GolgiError::KuskaApi(err)
}
}
impl From<kuska_ssb::feed::Error> for GolgiError {
fn from(err: kuska_ssb::feed::Error) -> Self {
GolgiError::KuskaFeed(err)
}
}
impl From<kuska_ssb::rpc::Error> for GolgiError {
fn from(err: kuska_ssb::rpc::Error) -> Self {
GolgiError::KuskaRpc(err)
}
}
impl From<serde_json::Error> for GolgiError {
fn from(err: serde_json::Error) -> Self {
GolgiError::SerdeJson(err)
}
}

106
src/lib.rs Normal file
View File

@ -0,0 +1,106 @@
mod error;
mod utils;
use async_std::net::TcpStream;
//use futures::io::{self, AsyncRead as Read, AsyncWrite as Write};
use kuska_handshake::async_std::{BoxStream, BoxStreamRead, BoxStreamWrite};
use kuska_handshake::HandshakeComplete;
use kuska_sodiumoxide::crypto::{auth, sign::ed25519};
use kuska_ssb::api::{dto::CreateHistoryStreamIn, ApiCaller};
use kuska_ssb::discovery;
use kuska_ssb::keystore;
use kuska_ssb::keystore::OwnedIdentity;
use kuska_ssb::rpc::{RpcReader, RpcWriter};
use crate::error::GolgiError;
struct Sbot {
id: String,
public_key: ed25519::PublicKey,
private_key: ed25519::SecretKey,
address: String,
// aka caps key (scuttleverse identifier)
network_id: auth::Key,
client: ApiCaller<TcpStream>,
rpc_reader: RpcReader<TcpStream>,
}
impl Sbot {
async fn init(ip_port: Option<String>, net_id: Option<String>) -> Result<Sbot, GolgiError> {
let address;
if ip_port.is_none() {
address = "127.0.0.1:8008".to_string();
} else {
address = ip_port.unwrap();
}
let network_id;
if net_id.is_none() {
network_id = discovery::ssb_net_id();
} else {
network_id = auth::Key::from_slice(&hex::decode(net_id.unwrap()).unwrap()).unwrap();
}
let OwnedIdentity { pk, sk, id } = keystore::from_gosbot_local()
.await
.expect("couldn't read local secret");
let socket = TcpStream::connect(&address)
.await
.map_err(|source| GolgiError::Io {
source,
context: "socket error; failed to initiate tcp stream connection".to_string(),
})?;
let handshake = kuska_handshake::async_std::handshake_client(
&mut &socket,
network_id.clone(),
pk,
sk.clone(),
pk,
)
.await
.map_err(GolgiError::Handshake)?;
let (box_stream_read, box_stream_write) =
BoxStream::from_handshake(socket.clone(), socket, handshake, 0x8000).split_read_write();
let rpc_reader = RpcReader::new(box_stream_read);
let client = ApiCaller::new(RpcWriter::new(box_stream_write));
Ok(Self {
id,
public_key: pk,
private_key: sk,
address,
network_id,
client: client,
rpc_reader: rpc_reader,
})
}
async fn whoami(&mut self) -> Result<String, GolgiError> {
let req_id = self.client.whoami_req_send().await?;
utils::get_async(&mut self.rpc_reader, req_id, utils::whoami_res_parse)
.await
.map(|whoami| whoami.id)
}
async fn create_history_stream(&mut self, id: String) -> Result<(), GolgiError> {
let args = CreateHistoryStreamIn::new(id);
let req_id = self.client.create_history_stream_req_send(&args).await?;
utils::print_source_until_eof(&mut self.rpc_reader, req_id, utils::feed_res_parse).await
}
}
pub async fn run() -> Result<(), GolgiError> {
let mut sbot_client = Sbot::init(None, None).await?;
let id = sbot_client.whoami().await?;
println!("{}", id);
sbot_client.create_history_stream(id).await?;
Ok(())
}

161
src/lib.rs_lifetimes Normal file
View File

@ -0,0 +1,161 @@
mod error;
mod utils;
use async_std::net::TcpStream;
//use futures::io::{self, AsyncRead as Read, AsyncWrite as Write};
use kuska_handshake::async_std::{BoxStream, BoxStreamRead, BoxStreamWrite};
use kuska_handshake::HandshakeComplete;
use kuska_sodiumoxide::crypto::{auth, sign::ed25519};
use kuska_ssb::api::{dto::CreateHistoryStreamIn, ApiCaller};
use kuska_ssb::discovery;
use kuska_ssb::keystore;
use kuska_ssb::keystore::OwnedIdentity;
use kuska_ssb::rpc::{RpcReader, RpcWriter};
use crate::error::GolgiError;
//struct Sbot<R: Read + std::marker::Unpin, W: Write + std::marker::Unpin> {
struct Sbot<'a> {
id: String,
public_key: ed25519::PublicKey,
private_key: ed25519::SecretKey,
address: String,
// aka caps key (scuttleverse identifier)
network_id: auth::Key,
//socket: TcpStream,
//client: ApiCaller<W>,
client: ApiCaller<TcpStream>,
//rpc_reader: RpcReader<R>,
rpc_reader: RpcReader<&'a TcpStream>,
}
//impl<R: Read + std::marker::Unpin, W: Write + std::marker::Unpin> Sbot<R, W> {
//impl Sbot<'static> {
impl Sbot<'_> {
async fn init(
ip_port: Option<String>,
net_id: Option<String>,
) -> Result<Sbot<'static>, GolgiError> {
let address;
if ip_port.is_none() {
// set default
address = "127.0.0.1:8008".to_string();
} else {
address = ip_port.unwrap().to_string();
}
let network_id;
if net_id.is_none() {
network_id = discovery::ssb_net_id();
} else {
network_id = auth::Key::from_slice(&hex::decode(net_id.unwrap()).unwrap()).unwrap();
}
let OwnedIdentity { pk, sk, id } = keystore::from_gosbot_local()
.await
.expect("couldn't read local secret");
let socket = TcpStream::connect(&address)
.await
.map_err(|source| GolgiError::Io {
source,
context: "socket error; failed to initiate tcp stream connection".to_string(),
})?;
let handshake = kuska_handshake::async_std::handshake_client(
&mut &socket,
network_id.clone(),
pk,
sk.clone(),
pk,
)
.await
.map_err(|err| GolgiError::Handshake(err))?;
let (box_stream_read, box_stream_write) =
BoxStream::from_handshake(socket, socket, handshake, 0x8000).split_read_write();
let rpc_reader = RpcReader::new(box_stream_read);
let client = ApiCaller::new(RpcWriter::new(box_stream_write));
Ok(Self {
id,
public_key: pk,
private_key: sk,
address,
network_id,
client: client,
rpc_reader: rpc_reader,
})
}
}
pub async fn run() -> Result<(), GolgiError> {
let sbot_client = Sbot::init(None, None);
/*
// read go-sbot id details from `/.ssb-go/secret
let OwnedIdentity { pk, sk, id } = keystore::from_gosbot_local()
.await
.expect("read local secret");
println!("connecting with identity {}", id);
// set the public key, ip and port for the sbot instance
let sbot_public_key = "a0SsCiZkBu6qaQ6tWVvzQPDSzvO0JqMAqPXt0LBIl30=".to_string();
let server_pk =
ed25519::PublicKey::from_slice(&base64::decode(&sbot_public_key)?).expect("bad public key");
let server_ipport = "127.0.0.1:8008".to_string();
// connect to the local go-sbot instance
let mut socket = TcpStream::connect(server_ipport)
.await
.map_err(|source| GolgiError::Io {
source,
context: "socket error; failed to initiate tcp stream connection".to_string(),
})?;
// initiate secret handshake
let handshake = kuska_handshake::async_std::handshake_client(
&mut socket,
discovery::ssb_net_id(),
pk,
sk.clone(),
server_pk,
)
.await
.expect("handshake error");
println!("💃 handshake complete");
// call `whoami`
let (box_stream_read, box_stream_write) =
BoxStream::from_handshake(&socket, &socket, handshake, 0x8000).split_read_write();
let mut rpc_reader = RpcReader::new(box_stream_read);
let mut client = ApiCaller::new(RpcWriter::new(box_stream_write));
let req_id = client.whoami_req_send().await?;
let whoami = match utils::get_async(&mut rpc_reader, req_id, utils::whoami_res_parse).await {
Ok(res) => {
println!("😊 server says hello to {}", res.id);
id
}
Err(err) => {
if !err
.to_string()
.contains("method:whoami is not in list of allowed methods")
{
println!("Cannot ask for whoami {}", err);
}
id
}
};
// call `createhistorystream`
let args = CreateHistoryStreamIn::new(whoami.clone());
// TODO: this should return an error if the args are wrong but it doesn't?
let req_id = client.create_history_stream_req_send(&args).await?;
utils::print_source_until_eof(&mut rpc_reader, req_id, utils::feed_res_parse).await?;
*/
Ok(())
}

9
src/main.rs Normal file
View File

@ -0,0 +1,9 @@
use std::process;
#[async_std::main]
async fn main() {
if let Err(e) = golgi::run().await {
eprintln!("Application error: {}", e);
process::exit(1);
}
}

80
src/utils.rs Normal file
View File

@ -0,0 +1,80 @@
/*
use kuska_handshake::async_std::BoxStream;
use kuska_sodiumoxide::crypto::sign::ed25519;
use kuska_ssb::discovery;
use kuska_ssb::keystore;
use kuska_ssb::keystore::OwnedIdentity;
*/
use std::fmt::Debug;
use async_std::io::Read;
use kuska_ssb::api::dto::WhoAmIOut;
use kuska_ssb::feed::Feed;
use kuska_ssb::rpc::{RecvMsg, RequestNo, RpcReader};
use crate::error::GolgiError;
pub fn feed_res_parse(body: &[u8]) -> Result<Feed, GolgiError> {
Ok(Feed::from_slice(body)?)
}
pub fn whoami_res_parse(body: &[u8]) -> Result<WhoAmIOut, GolgiError> {
Ok(serde_json::from_slice(body)?)
}
pub async fn get_async<'a, R, T, F>(
rpc_reader: &mut RpcReader<R>,
req_no: RequestNo,
f: F,
) -> Result<T, GolgiError>
where
R: Read + Unpin,
F: Fn(&[u8]) -> Result<T, GolgiError>,
T: Debug,
{
loop {
let (id, msg) = rpc_reader.recv().await?;
if id == req_no {
match msg {
RecvMsg::RpcResponse(_type, body) => {
return f(&body).map_err(|err| err);
}
RecvMsg::ErrorResponse(message) => {
return Err(GolgiError::Sbot(message));
}
_ => {}
}
}
}
}
pub async fn print_source_until_eof<'a, R, T, F>(
rpc_reader: &mut RpcReader<R>,
req_no: RequestNo,
f: F,
) -> Result<(), GolgiError>
where
R: Read + Unpin,
F: Fn(&[u8]) -> Result<T, GolgiError>,
T: Debug + serde::Deserialize<'a>,
{
loop {
let (id, msg) = rpc_reader.recv().await?;
if id == req_no {
match msg {
RecvMsg::RpcResponse(_type, body) => {
let display = f(&body)?;
println!("{:?}", display);
}
RecvMsg::ErrorResponse(message) => {
return Err(GolgiError::Sbot(message));
}
RecvMsg::CancelStreamRespose() => break,
_ => {}
}
}
}
Ok(())
}