Skip to content

Commit 59a023c

Browse files
Xeleronclaude
andcommitted
avm2: Add SecureSocket support
Implement SecureSocket AS3 class and runtime support: add SecureSocket AS file and native bindings, integrate TLS certificate status into SocketObject and Sockets manager, add NavigatorBackend secure connect path and frontend-utils TLS support, update Cargo manifests, and add tests + fixtures for secure socket behavior. Co-authored-by: Claude <claude@anthropic.com>
1 parent aadd936 commit 59a023c

23 files changed

Lines changed: 817 additions & 47 deletions

File tree

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/src/avm2/globals/flash/net.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub mod net_connection;
1414
pub mod net_stream;
1515
pub mod object_encoding;
1616
pub mod responder;
17+
pub mod secure_socket;
1718
pub mod shared_object;
1819
pub mod socket;
1920
pub mod url_loader;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package flash.net {
2+
import __ruffle__.stub_method;
3+
import __ruffle__.stub_getter;
4+
5+
import flash.utils.ByteArray;
6+
7+
[API("662")]
8+
public class SecureSocket extends Socket {
9+
public function SecureSocket() {
10+
super();
11+
this.timeout = 20000;
12+
}
13+
14+
public static native function get isSupported():Boolean;
15+
16+
public native function get serverCertificateStatus():String;
17+
18+
public function get serverCertificate():* {
19+
stub_getter("flash.net.SecureSocket", "serverCertificate");
20+
return null;
21+
}
22+
23+
public function addBinaryChainBuildingCertificate(certificate:ByteArray, trusted:Boolean):void {
24+
stub_method("flash.net.SecureSocket", "addBinaryChainBuildingCertificate");
25+
}
26+
27+
public override native function connect(host:String, port:int):void;
28+
}
29+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use crate::avm2::error::make_error_2003;
2+
use crate::avm2::parameters::ParametersExt;
3+
use crate::avm2::string::AvmString;
4+
use crate::avm2::{Activation, Error, Value};
5+
use crate::context::UpdateContext;
6+
7+
/// Implements `SecureSocket.connect`
8+
pub fn connect<'gc>(
9+
activation: &mut Activation<'_, 'gc>,
10+
this: Value<'gc>,
11+
args: &[Value<'gc>],
12+
) -> Result<Value<'gc>, Error<'gc>> {
13+
let this = this.as_object().unwrap();
14+
15+
let socket = match this.as_socket() {
16+
Some(socket) => socket,
17+
None => return Ok(Value::Undefined),
18+
};
19+
20+
let host = args.get_string(activation, 0);
21+
let port = u16::try_from(args.get_i32(1))
22+
.ok()
23+
.filter(|&p| p >= 1)
24+
.ok_or_else(|| make_error_2003(activation))?;
25+
26+
let UpdateContext {
27+
sockets, navigator, ..
28+
} = activation.context;
29+
30+
sockets.connect_avm2_secure(*navigator, socket, host.to_utf8_lossy().into_owned(), port);
31+
32+
Ok(Value::Undefined)
33+
}
34+
35+
/// Implements `SecureSocket.isSupported`
36+
pub fn get_is_supported<'gc>(
37+
_activation: &mut Activation<'_, 'gc>,
38+
_this: Value<'gc>,
39+
_args: &[Value<'gc>],
40+
) -> Result<Value<'gc>, Error<'gc>> {
41+
// SecureSocket is supported in Ruffle (we use native TLS).
42+
Ok(true.into())
43+
}
44+
45+
/// Implements `SecureSocket.serverCertificateStatus`
46+
pub fn get_server_certificate_status<'gc>(
47+
activation: &mut Activation<'_, 'gc>,
48+
this: Value<'gc>,
49+
_args: &[Value<'gc>],
50+
) -> Result<Value<'gc>, Error<'gc>> {
51+
let this = this.as_object().unwrap();
52+
53+
if let Some(socket) = this.as_socket() {
54+
let status = socket.certificate_status();
55+
return Ok(AvmString::new_utf8(activation.gc(), &status).into());
56+
}
57+
58+
Ok(Value::Undefined)
59+
}

core/src/avm2/globals/globals.as

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ include "flash/net/Responder.as"
315315
include "flash/net/SharedObject.as"
316316
include "flash/net/SharedObjectFlushStatus.as"
317317
include "flash/net/Socket.as"
318+
include "flash/net/SecureSocket.as"
318319
include "flash/net/URLLoader.as"
319320
include "flash/net/URLLoaderDataFormat.as"
320321
include "flash/net/URLRequest.as"

core/src/avm2/object/socket_object.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub fn socket_allocator<'gc>(
2929
handle: Cell::new(None),
3030
read_buffer: RefCell::new(VecDeque::new()),
3131
write_buffer: RefCell::new(vec![]),
32+
certificate_status: RefCell::new("unknown".to_string()),
3233
},
3334
))
3435
.into())
@@ -83,6 +84,14 @@ impl<'gc> SocketObject<'gc> {
8384
self.0.handle.replace(Some(handle))
8485
}
8586

87+
pub fn certificate_status(self) -> String {
88+
self.0.certificate_status.borrow().clone()
89+
}
90+
91+
pub fn set_certificate_status(self, status: String) {
92+
*self.0.certificate_status.borrow_mut() = status;
93+
}
94+
8695
pub fn read_buffer(&self) -> RefMut<'_, VecDeque<u8>> {
8796
self.0.read_buffer.borrow_mut()
8897
}
@@ -202,6 +211,10 @@ pub struct SocketObjectData<'gc> {
202211

203212
read_buffer: RefCell<VecDeque<u8>>,
204213
write_buffer: RefCell<Vec<u8>>,
214+
215+
/// The server certificate status (used by SecureSocket).
216+
#[collect(require_static)]
217+
certificate_status: RefCell<String>,
205218
}
206219

207220
impl fmt::Debug for SocketObject<'_> {

core/src/backend/navigator.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,22 @@ pub trait NavigatorBackend: Any {
316316
receiver: Receiver<Vec<u8>>,
317317
sender: Sender<SocketAction>,
318318
);
319+
320+
/// Handle a secure (TLS) Socket connection request.
321+
///
322+
/// This is used by SecureSocket. By default it delegates to [connect_socket].
323+
/// Backends that support TLS should override this to establish a TLS connection.
324+
fn connect_secure_socket(
325+
&mut self,
326+
host: String,
327+
port: u16,
328+
timeout: Duration,
329+
handle: SocketHandle,
330+
receiver: Receiver<Vec<u8>>,
331+
sender: Sender<SocketAction>,
332+
) {
333+
self.connect_socket(host, port, timeout, handle, receiver, sender);
334+
}
319335
}
320336

321337
#[cfg(not(target_family = "wasm"))]

core/src/socket.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ pub enum SocketAction {
5858
Connect(SocketHandle, ConnectionState),
5959
Data(SocketHandle, Vec<u8>),
6060
Close(SocketHandle),
61+
/// Sets the TLS certificate status string on the target socket.
62+
/// Must be sent before the corresponding Connect action.
63+
CertificateStatus(SocketHandle, String),
6164
}
6265

6366
/// Manages the collection of Sockets.
@@ -112,6 +115,35 @@ impl<'gc> Sockets<'gc> {
112115
}
113116
}
114117

118+
pub fn connect_avm2_secure(
119+
&mut self,
120+
backend: &mut dyn NavigatorBackend,
121+
target: SocketObject<'gc>,
122+
host: String,
123+
port: u16,
124+
) {
125+
let (sender, receiver) = unbounded();
126+
127+
let socket = Socket::new(SocketKind::Avm2(target), sender);
128+
let handle = self.sockets.insert(socket);
129+
130+
// NOTE: This call will send SocketAction::Connect to sender with connection status.
131+
backend.connect_secure_socket(
132+
sanitize_host(&host).to_string(),
133+
port,
134+
Duration::from_millis(target.timeout().into()),
135+
handle,
136+
receiver,
137+
self.sender.clone(),
138+
);
139+
140+
if let Some(existing_handle) = target.set_handle(handle) {
141+
// As written in the AS3 docs, we are supposed to close the existing connection,
142+
// when a new one is created.
143+
self.close(existing_handle)
144+
}
145+
}
146+
115147
pub fn connect_avm1(
116148
&mut self,
117149
backend: &mut dyn NavigatorBackend,
@@ -353,6 +385,13 @@ impl<'gc> Sockets<'gc> {
353385
}
354386
}
355387
}
388+
SocketAction::CertificateStatus(handle, status) => {
389+
if let Some(socket) = context.sockets.sockets.get(handle)
390+
&& let SocketKind::Avm2(target) = socket.target
391+
{
392+
target.set_certificate_status(status);
393+
}
394+
}
356395
SocketAction::Close(handle) => {
357396
let target = match context.sockets.sockets.remove(handle) {
358397
Some(socket) => {

frontend-utils/Cargo.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ workspace = true
1313
[features]
1414
cpal = ["dep:cpal", "dep:bytemuck", "ruffle_core/audio"]
1515
fs = []
16-
navigator = ["fs", "dep:async-io", "dep:tokio"]
16+
navigator = ["fs", "dep:async-io", "dep:tokio", "dep:rustls", "dep:tokio-rustls", "dep:rustls-native-certs", "dep:rustls-pki-types"]
17+
rustls = ["dep:rustls"]
18+
tokio-rustls = ["dep:tokio-rustls"]
19+
rustls-native-certs = ["dep:rustls-native-certs"]
20+
rustls-pki-types = ["dep:rustls-pki-types"]
1721

1822
[dependencies]
1923
toml_edit = { version = "0.23.6", features = ["parse"] }
@@ -38,6 +42,10 @@ reqwest = { version = "0.12.28", default-features = false, features = [
3842
tokio = { workspace = true, features = ["net", "macros"], optional = true }
3943
cpal = { workspace = true, optional = true }
4044
bytemuck = { workspace = true, optional = true }
45+
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "logging", "tls12"], optional = true }
46+
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "logging", "tls12"], optional = true }
47+
rustls-native-certs = { version = "0.8", optional = true }
48+
rustls-pki-types = { version = "1.0", optional = true }
4149

4250
[dev-dependencies]
4351
tempfile = { workspace = true }

0 commit comments

Comments
 (0)