Initial Commit (it doesn't work)

This commit is contained in:
2025-06-26 19:35:13 -06:00
commit 9232c0414a
13 changed files with 3014 additions and 0 deletions
+22
View File
@@ -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"] }
+112
View File
@@ -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();
}
+291
View File
@@ -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()
}