Initial Commit (it doesn't work)
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "gamestream-webtransport-proxy"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.98"
|
||||
axum = "0.8.4"
|
||||
getrandom = { version = "0.3.3", features = ["std"] }
|
||||
hex = "0.4.3"
|
||||
libc = "0.2.174"
|
||||
moonlight-common-c-sys = { path = "../moonlight-common-c-sys" }
|
||||
openssl = "0.10.73"
|
||||
rand = "0.9.1"
|
||||
reqwest = { version = "0.12.20", features = [
|
||||
"rustls-tls",
|
||||
], default-features = false }
|
||||
serde = { version = "1.0.219", features = ["serde_derive"] }
|
||||
serde-xml-rs = "0.8.1"
|
||||
tokio = { version = "1.45.1", features = ["full"] }
|
||||
url-constructor = "0.1.0"
|
||||
uuid = { version = "1.17.0", features = ["v4"] }
|
||||
@@ -0,0 +1,112 @@
|
||||
use axum::{Router, routing::get};
|
||||
use std::ffi::CString;
|
||||
|
||||
use moonlight_common_c_sys::{
|
||||
_SERVER_INFORMATION, _STREAM_CONFIGURATION, COLOR_RANGE_LIMITED, COLORSPACE_REC_601,
|
||||
CONNECTION_LISTENER_CALLBACKS, ENCFLG_NONE, PCONNECTION_LISTENER_CALLBACKS, SCM_H264,
|
||||
STREAM_CFG_LOCAL, VIDEO_FORMAT_H264,
|
||||
};
|
||||
|
||||
mod pair;
|
||||
|
||||
fn get_server_info() -> _SERVER_INFORMATION {
|
||||
_SERVER_INFORMATION {
|
||||
// TODO: these all leak
|
||||
address: CString::new("10.0.1.8")
|
||||
.expect("CString::new failed")
|
||||
.into_raw(),
|
||||
serverInfoAppVersion: CString::new("foo").expect("CString::new failed").into_raw(),
|
||||
serverInfoGfeVersion: CString::new("foo").expect("CString::new failed").into_raw(),
|
||||
rtspSessionUrl: CString::new("foo").expect("CString::new failed").into_raw(),
|
||||
serverCodecModeSupport: SCM_H264,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_stream_config() -> _STREAM_CONFIGURATION {
|
||||
let mut remote_input_aes_key_u8: [u8; 16] = [0; 16];
|
||||
let remote_input_aes_iv: [i8; 16] = [0; 16];
|
||||
|
||||
getrandom::fill(&mut remote_input_aes_key_u8)
|
||||
.expect("Failed to generate cryptographic random bytes");
|
||||
|
||||
let remote_input_aes_key: [i8; 16] =
|
||||
unsafe { *(&mut remote_input_aes_key_u8 as *mut [u8; 16] as *mut [i8; 16]) };
|
||||
|
||||
_STREAM_CONFIGURATION {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 60,
|
||||
bitrate: 50 * 1024 * 1024,
|
||||
packetSize: 1024,
|
||||
streamingRemotely: STREAM_CFG_LOCAL,
|
||||
audioConfiguration: (0x3 << 16) | (2 << 8) | 0xCA,
|
||||
supportedVideoFormats: VIDEO_FORMAT_H264,
|
||||
clientRefreshRateX100: 6000,
|
||||
colorSpace: COLORSPACE_REC_601,
|
||||
colorRange: COLOR_RANGE_LIMITED,
|
||||
encryptionFlags: ENCFLG_NONE,
|
||||
remoteInputAesKey: remote_input_aes_key,
|
||||
remoteInputAesIv: remote_input_aes_iv,
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" {
|
||||
fn printf(format: *const i8, ...) -> ();
|
||||
}
|
||||
|
||||
fn get_listener_callbacks() -> CONNECTION_LISTENER_CALLBACKS {
|
||||
CONNECTION_LISTENER_CALLBACKS {
|
||||
stageStarting: None,
|
||||
stageComplete: None,
|
||||
stageFailed: None,
|
||||
connectionStarted: None,
|
||||
connectionTerminated: None,
|
||||
logMessage: Some(printf),
|
||||
rumble: None,
|
||||
connectionStatusUpdate: None,
|
||||
setHdrMode: None,
|
||||
rumbleTriggers: None,
|
||||
setMotionEventState: None,
|
||||
setControllerLED: None,
|
||||
setAdaptiveTriggers: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn barmain() {
|
||||
//let server_info = moonlight_common_c_sys::LiInitializeServerInformation();
|
||||
let mut server_info = get_server_info();
|
||||
let mut stream_config = get_stream_config();
|
||||
let mut listener_callbacks = get_listener_callbacks();
|
||||
let mut ret = 0;
|
||||
unsafe {
|
||||
ret = moonlight_common_c_sys::LiStartConnection(
|
||||
&mut server_info,
|
||||
&mut stream_config,
|
||||
&mut listener_callbacks,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
println!("{ret}");
|
||||
|
||||
loop {}
|
||||
|
||||
//println!("Hello, world!");
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Create a router with a test endpoint
|
||||
let app = Router::new().route("/pair/{url}/{port}", get(pair::get_pair));
|
||||
|
||||
// Bind to port 3000
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
|
||||
// Start the server
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
use axum::Json;
|
||||
use axum::extract::Path;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use openssl::hash::MessageDigest;
|
||||
use openssl::sha::Sha256;
|
||||
use openssl::symm::Cipher;
|
||||
use rand::Rng;
|
||||
use reqwest::Identity;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use openssl::pkey::{PKey, Private};
|
||||
use openssl::rsa::Rsa;
|
||||
use openssl::x509::X509;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ServerCertResponse {
|
||||
paired: i32,
|
||||
plaincert: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ClientChallengeResponse {
|
||||
challengeresponse: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ServerChallengeResponseResponse {
|
||||
paired: i32,
|
||||
pairingsecret: String,
|
||||
}
|
||||
|
||||
fn get_cert_and_private_key() -> Result<(X509, PKey<Private>)> {
|
||||
let rsa = Rsa::generate(2048)?;
|
||||
let key_pair = PKey::from_rsa(rsa)?;
|
||||
let mut cert_builder = X509::builder()?;
|
||||
cert_builder.set_version(2)?;
|
||||
cert_builder.set_pubkey(&key_pair)?;
|
||||
cert_builder.sign(&key_pair, MessageDigest::sha256())?;
|
||||
let cert = cert_builder.build();
|
||||
Ok((cert, key_pair))
|
||||
}
|
||||
|
||||
async fn get_server_cert(
|
||||
mut base_url: url_constructor::UrlConstructor,
|
||||
salt_hex: String,
|
||||
cert_hex: String,
|
||||
) -> Result<ServerCertResponse> {
|
||||
// Generate pairing url
|
||||
let url = base_url
|
||||
.param("phrase", "getservercert")
|
||||
.param("salt", &salt_hex)
|
||||
.param("clientcert", &cert_hex)
|
||||
.build();
|
||||
//let url = format!(
|
||||
// "http://{}/pair?uniqueid={}&uuid={}&devicename=roth&updateState=1&phrase=getservercert&salt={}&clientcert={}",
|
||||
// host, "0123456789ABCDEF", "23060a17e4fc40f5a83fee298995dd18", salt_hex, cert_hex,
|
||||
//);
|
||||
println!("url: {url}");
|
||||
|
||||
//let mut identity_data = private_key.private_key_to_pem_pkcs8().unwrap();
|
||||
//identity_data.extend_from_slice(&cert_pem);
|
||||
|
||||
//let identity = Identity::from_pem(&identity_data).unwrap();
|
||||
//let identity =
|
||||
// Identity::from_pkcs8_pem(&cert_pem, &private_key.private_key_to_pem_pkcs8().unwrap())
|
||||
// .unwrap();
|
||||
|
||||
let mut http_builder = reqwest::Client::builder();
|
||||
http_builder = http_builder.user_agent("gamestream-webtransport-proxy");
|
||||
//http_builder = http_builder.identity(identity);
|
||||
let client = http_builder.build().unwrap();
|
||||
|
||||
let resp = client.get(url).send().await?;
|
||||
let text = resp.text().await.unwrap();
|
||||
let server_response: ServerCertResponse = serde_xml_rs::from_str(&text).unwrap();
|
||||
println!("{server_response:?}");
|
||||
Ok(server_response)
|
||||
}
|
||||
|
||||
async fn get_server_challenge(
|
||||
mut base_url: url_constructor::UrlConstructor,
|
||||
aes_key: &[u8],
|
||||
) -> Result<ClientChallengeResponse> {
|
||||
// 3. Generate random challenge
|
||||
let mut challenge_data = [0u8; 16];
|
||||
openssl::rand::rand_bytes(&mut challenge_data)?;
|
||||
|
||||
// 4. Encrypt challenge
|
||||
let mut cipher_ctx = openssl::cipher_ctx::CipherCtx::new()?;
|
||||
cipher_ctx.encrypt_init(
|
||||
Some(openssl::cipher::Cipher::aes_128_ecb()),
|
||||
Some(aes_key),
|
||||
None,
|
||||
)?;
|
||||
cipher_ctx.set_padding(false);
|
||||
|
||||
let mut challenge_enc = vec![];
|
||||
cipher_ctx.cipher_update_vec(&challenge_data, &mut challenge_enc)?;
|
||||
cipher_ctx.cipher_final(&mut challenge_enc)?;
|
||||
|
||||
// 5. Convert to hex
|
||||
let challenge_hex = hex::encode(challenge_enc);
|
||||
|
||||
let url = base_url.param("clientchallenge", challenge_hex).build();
|
||||
let mut http_builder = reqwest::Client::builder();
|
||||
http_builder = http_builder.user_agent("gamestream-webtransport-proxy");
|
||||
//http_builder = http_builder.identity(identity);
|
||||
let client = http_builder.build().unwrap();
|
||||
|
||||
let resp = client.get(url).send().await?;
|
||||
let text = resp.text().await?;
|
||||
|
||||
Ok(serde_xml_rs::from_str(&text)?)
|
||||
}
|
||||
|
||||
fn generate_challenge_response(
|
||||
client_challenge_response: String,
|
||||
aes_key: &[u8],
|
||||
cert: &X509,
|
||||
) -> Result<String> {
|
||||
let max_size = 64;
|
||||
let hex_len = client_challenge_response.len();
|
||||
|
||||
if hex_len / 2 > max_size {
|
||||
return Err(anyhow::anyhow!("Server challenge response too big"));
|
||||
}
|
||||
|
||||
let client_challenge_response_data_enc = hex::decode(client_challenge_response)?;
|
||||
|
||||
let mut cipher_ctx = openssl::cipher_ctx::CipherCtx::new()?;
|
||||
cipher_ctx.decrypt_init(
|
||||
Some(openssl::cipher::Cipher::aes_128_ecb()),
|
||||
Some(aes_key),
|
||||
None,
|
||||
)?;
|
||||
cipher_ctx.set_padding(false);
|
||||
|
||||
let mut client_challenge_response_data = vec![];
|
||||
cipher_ctx.cipher_update_vec(
|
||||
&client_challenge_response_data_enc,
|
||||
&mut client_challenge_response_data,
|
||||
)?;
|
||||
cipher_ctx.cipher_final(&mut client_challenge_response_data)?;
|
||||
|
||||
let mut client_secret_data = [0u8; 16];
|
||||
openssl::rand::rand_bytes(&mut client_secret_data)?;
|
||||
|
||||
// Extract ASN.1 signature from certificate
|
||||
let asn_signature = cert.signature();
|
||||
let signature_data = asn_signature.as_slice();
|
||||
|
||||
// Build challenge response
|
||||
let mut challenge_response =
|
||||
Vec::with_capacity(16 + signature_data.len() + client_secret_data.len());
|
||||
challenge_response.extend_from_slice(&client_challenge_response_data[32..32 + 16]);
|
||||
challenge_response.extend_from_slice(signature_data);
|
||||
challenge_response.extend_from_slice(&client_secret_data);
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&challenge_response);
|
||||
let challenge_response_hash = hasher.finish().to_vec();
|
||||
|
||||
let mut cipher_ctx = openssl::cipher_ctx::CipherCtx::new()?;
|
||||
cipher_ctx.encrypt_init(
|
||||
Some(openssl::cipher::Cipher::aes_128_ecb()),
|
||||
Some(aes_key),
|
||||
None,
|
||||
)?;
|
||||
cipher_ctx.set_padding(false);
|
||||
|
||||
let mut challenge_response_hash_enc = vec![];
|
||||
cipher_ctx.cipher_update_vec(&challenge_response_hash, &mut challenge_response_hash_enc)?;
|
||||
cipher_ctx.cipher_final(&mut challenge_response_hash_enc)?;
|
||||
|
||||
let challenge_response_hex = hex::encode(challenge_response_hash_enc);
|
||||
|
||||
Ok(challenge_response_hex)
|
||||
}
|
||||
|
||||
async fn send_server_challenge_response(
|
||||
mut base_url: url_constructor::UrlConstructor,
|
||||
server_challenge_response: String,
|
||||
) -> Result<ServerChallengeResponseResponse> {
|
||||
let url = base_url
|
||||
.param("serverchallengeresp", server_challenge_response)
|
||||
.build();
|
||||
|
||||
println!("url: {url}");
|
||||
let mut http_builder = reqwest::Client::builder();
|
||||
http_builder = http_builder.user_agent("gamestream-webtransport-proxy");
|
||||
//http_builder = http_builder.identity(identity);
|
||||
let client = http_builder.build().unwrap();
|
||||
|
||||
let resp = client.get(url).send().await?;
|
||||
let text = resp.text().await?;
|
||||
Ok(serde_xml_rs::from_str(&text)?)
|
||||
}
|
||||
|
||||
async fn generate_aes_key(salt: [u8; 16], pin: [u8; 4]) -> Vec<u8> {
|
||||
let mut salt_pin = Vec::with_capacity(salt.len() + pin.len());
|
||||
salt_pin.extend_from_slice(&salt);
|
||||
salt_pin.extend_from_slice(&pin);
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&salt_pin);
|
||||
hasher.finish().to_vec()
|
||||
}
|
||||
|
||||
async fn do_challenge(
|
||||
base_url: url_constructor::UrlConstructor,
|
||||
pin: [u8; 4],
|
||||
salt: [u8; 16],
|
||||
cert: &X509,
|
||||
) -> Result<()> {
|
||||
let aes_key = generate_aes_key(salt, pin).await;
|
||||
|
||||
let client_challenge_response = get_server_challenge(base_url.clone(), &aes_key).await?;
|
||||
println!("{client_challenge_response:?}");
|
||||
|
||||
let challenge_response =
|
||||
generate_challenge_response(client_challenge_response.challengeresponse, &aes_key, cert)?;
|
||||
println!("{challenge_response:?}");
|
||||
|
||||
let server_challenge_response_response =
|
||||
send_server_challenge_response(base_url, challenge_response).await?;
|
||||
println!("{server_challenge_response_response:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Do a pairing operation
|
||||
pub async fn get_pair(Path((host, port)): Path<(String, u16)>) -> Response {
|
||||
let unique_id = "0123456789ABCDEF";
|
||||
let uuidv2 = "23060a17e4fc40f5a83fee298995dd18";
|
||||
|
||||
let mut base_url = url_constructor::UrlConstructor::new();
|
||||
base_url
|
||||
.scheme("http")
|
||||
.host(&host)
|
||||
.port(port)
|
||||
.subdir("pair")
|
||||
.param("uniqueid", unique_id)
|
||||
.param("uuid", uuidv2)
|
||||
.param("devicename", "roth")
|
||||
.param("updateState", "1");
|
||||
|
||||
let mut pin = [0u8; 4];
|
||||
{
|
||||
print!("pairing pin: ");
|
||||
|
||||
let mut rng = rand::rng();
|
||||
for i in 0..pin.len() {
|
||||
pin[i] = 5;
|
||||
//TODO: reenable random pin
|
||||
// pin[i] = rng.random_range(0..10);
|
||||
print!("{}", pin[i]);
|
||||
}
|
||||
// Print as a four-digit, zero-padded integer
|
||||
println!("");
|
||||
}
|
||||
|
||||
// Generate certificates
|
||||
let (cert, private_key) = match get_cert_and_private_key() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!("Could not generate certs: {e}");
|
||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// Convert to hex
|
||||
let cert_pem = cert.to_pem().unwrap();
|
||||
let cert_hex = hex::encode(&cert_pem);
|
||||
|
||||
// Generate salt and convert to hex
|
||||
let mut salt = [0u8; 16];
|
||||
openssl::rand::rand_bytes(&mut salt).unwrap();
|
||||
let salt_hex = hex::encode(salt);
|
||||
|
||||
let server_cert_response = get_server_cert(base_url.clone(), salt_hex, cert_hex).await;
|
||||
|
||||
if let Err(e) = do_challenge(base_url.clone(), pin, salt, &cert).await {
|
||||
println!("could not do challenge: {e}");
|
||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||
};
|
||||
|
||||
"test".into_response()
|
||||
}
|
||||
Reference in New Issue
Block a user