Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion lib/display/DisplayObjectContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,11 @@ export class DisplayObjectContainer extends InteractiveObject {
try {
(<AwayDisplayObjectContainer> this._adaptee).removeChild(child.adaptee);
} catch (e) {
throw (<SecurityDomain> this.sec).createError('ArgumentError', Errors.NotAChildError);
// The adaptee scene graph can diverge from the AS3 display list (orphan/unload
// management), so a child that is still in the AS3 list may already be detached
// here. Throwing Flash's #2025 in that case erupts through whatever frame
// callback is running (e.g. a tween onComplete) and can kill the shared ticker.
console.warn('[DisplayObjectContainer] removeChild: not a child (already removed?) — ignoring');
}
//OrphanManager.addOrphan(child.adaptee);
return child;
Expand Down
5 changes: 5 additions & 0 deletions lib/events/EventDispatcherBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ export class EventDispatcherBase extends ASObject {
const l: ListenerObject = this._listenerObjects[event.type];

if (l) {
// Flash reports the aggregation target (EventDispatcher(target) constructor arg)
// as currentTarget during dispatch — e.g. GreenSock dispatches tween events via an
// internal aggregated dispatcher and handlers cast event.currentTarget to the tween.
if (this._t)
(<any> event).currentTarget = this._t;
if (!event.target)
event.target = this._t;
//console.log("dispatchEvent", event.type, (<any>this).adaptee?.id);
Expand Down
3 changes: 2 additions & 1 deletion lib/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ import { GameInputDevice } from './ui/GameInputDevice';
import { GameInputFinger } from './ui/GameInputFinger';
import { GameInputHand } from './ui/GameInputHand';
import { Mouse } from './ui/Mouse';
import { MouseCursorData } from './ui/MouseCursorData';
import { MultitouchInputMode } from './ui/MultitouchInputMode';
import { Multitouch } from './ui/Multitouch';
import { Timer } from './utils/Timer';
Expand Down Expand Up @@ -640,7 +641,7 @@ export function initLink() {
//M('flash.ui.KeyLocation', KeyLocation);
M('flash.ui.Mouse', Mouse);
//M('flash.ui.MouseCursor', MouseCursor);
//M('flash.ui.MouseCursorData', MouseCursorData);
M('flash.ui.MouseCursorData', MouseCursorData);
M('flash.ui.Multitouch', <any>Multitouch);
M('flash.ui.MultitouchInputMode', <any>MultitouchInputMode);

Expand Down
4 changes: 2 additions & 2 deletions lib/net/FileReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,12 @@ export class FileReference extends EventDispatcher {
const mime = knownTypes[ext].mime;
types[0] = {
description: knownTypes[ext].description,
accept: { [mime] : ext },
accept: { [mime] : ext as `.${string}` },
};
} else {
types[0] = {
description: '',
accept: { 'application/unknown' : ext },
accept: { 'application/unknown' : ext as `.${string}` },
};
}

Expand Down
310 changes: 243 additions & 67 deletions lib/net/NetConnection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EventDispatcher } from '../events/EventDispatcher';
import { axCoerceString, ASArray, ASObject } from '@awayfl/avm2';
import { axCoerceString, ASArray, ASObject, Multiname } from '@awayfl/avm2';
import { NetStatusEvent } from '../events/NetStatusEvent';
import { somewhatImplemented, release, notImplemented } from '@awayfl/swf-loader';
import { Responder } from './Responder';

Expand Down Expand Up @@ -46,8 +47,234 @@ export class NetConnection extends EventDispatcher {
}

call(command: string, responder: Responder /* more args can be provided */): void {
arguments[0] = axCoerceString(command);
this._invoke(2, <any>arguments);
command = axCoerceString(command);
// Native methods receive AS3 arguments flat (no ...rest collection) — everything
// after `responder` is a call argument to be serialised.
const callArgs = Array.prototype.slice.call(arguments, 2);
this._httpCall(command, responder, callArgs);
}

/* ------------------------------------------------------------------------------------
* Flash Remoting (AMF over HTTP POST) — the transport used by Zend_Amf / AMFPHP / etc.
* Serialisation is delegated to the AVM2 ByteArray AMF codec so arguments and results
* stay proper AVM2 objects; only the remoting packet envelope is hand-written here.
* ---------------------------------------------------------------------------------- */

private static _netStatusEventClass: any = null;
private _httpCallId: number = 0;
private _pendingResponders: Record<string, Responder>;

private _debugAMF(...args: any[]): void {
if (typeof self !== 'undefined' && (<any> self).__AWAYFL_AMF_DEBUG)
console.log.apply(console, ['[AMF]', ...args]);
}

private _makeStatusInfo(code: string, level: string, description?: string): ASObject {
const info: any = { code: code, level: level };
if (description !== undefined)
info.description = description;
return this.sec.createObjectFromJS(info);
}

private _dispatchStatus(code: string, level: string, description?: string): void {
this._debugAMF('netStatus ->', code, description || '');
try {
// Events handed to compiled AS3 must be real AVM2 instances (axConstruct on the
// AXClass); a raw `new NetStatusEvent(...)` lacks runtime traits and property
// access from compiled code fails on it.
if (!NetConnection._netStatusEventClass) {
NetConnection._netStatusEventClass = this.sec.application.getClass(
Multiname.FromFQNString('flash.events.NetStatusEvent', 0 /* NamespaceType.Public */));
}
const event = NetConnection._netStatusEventClass.axConstruct(
[NetStatusEvent.NET_STATUS, false, false, this._makeStatusInfo(code, level, description)]);
event.currentTarget = this;
event.target = this;
this.dispatchEvent(event);
} catch (e) {
console.warn('[NetConnection] netStatus dispatch failed:', e);
}
}

private static _byteArrayClass: any = null;

private _newAMFByteArray(): any {
// ByteArray's buffer machinery lives on DataBuffer.prototype and is merged onto the
// VM-linked prototype via instanceNatives — a raw `new ByteArray()` lacks it, so the
// instance must be constructed through the AXClass.
if (!NetConnection._byteArrayClass) {
NetConnection._byteArrayClass = this.sec.application.getClass(
Multiname.FromFQNString('flash.utils.ByteArray', 0 /* NamespaceType.Public */));
}
const ba: any = NetConnection._byteArrayClass.axConstruct([]);
// the AMF codec resolves classes/objects through the security domain
ba.sec = this.sec;
return ba;
}

private _httpCall(command: string, responder: Responder, callArgs: any[]): void {
const id = String(++this._httpCallId);
this._debugAMF('call ->', command, 'id', id);
try {
if (!this._pendingResponders)
this._pendingResponders = {};
if (responder)
this._pendingResponders[id] = responder;

const ba = this._newAMFByteArray();
ba._objectEncoding = 3; // AMF3
ba.writeByte(0x11); // AMF0 avmplus marker: body is AMF3
ba.writeObject(this.sec.createArray(callArgs));
const body = new Uint8Array(ba._buffer.slice(0, ba._length));
const packet = NetConnection._buildRemotingPacket(command, id, body);

if (!this._uri) {
this._httpFail(id, 'NetConnection is not connected (uri is null)');
return;
}
fetch(this._uri, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/x-amf' },
body: packet
}).then((r) => {
if (!r.ok)
throw new Error('HTTP ' + r.status);
const ct = (r.headers.get('content-type') || '').toLowerCase();
// A misconfigured gateway can answer 200 with an HTML error page; surface it
// as a fault instead of feeding non-AMF bytes to the parser.
if (ct.indexOf('application/x-amf') === -1) {
return r.text().then((t) => {
throw new Error('Non-AMF response (content-type: ' + (ct || 'none') + '): '
+ t.replace(/\s+/g, ' ').slice(0, 300));
});
}
return r.arrayBuffer();
}).then((buf) => {
this._handleRemotingResponse(<ArrayBuffer> buf);
}).catch((err) => {
this._httpFail(id, String((err && err.message) || err));
});
} catch (e) {
this._debugAMF('call FAILED synchronously:', e);
this._httpFail(id, String((e && (<any> e).message) || e));
}
}

private _handleRemotingResponse(buf: ArrayBuffer): void {
let messages: { target: string; body: ArrayBuffer }[];
try {
messages = NetConnection._parseRemotingPacket(buf);
} catch (e) {
console.warn('[NetConnection] AMF response parse failed:', e);
return;
}
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
let value: any;
try {
const ba = this._newAMFByteArray();
ba.setArrayBuffer(msg.body);
ba._position = 0;
ba._objectEncoding = 0; // AMF0 reader; handles the 0x11 -> AMF3 marker
value = ba.readObject();
} catch (e) {
console.warn('[NetConnection] AMF body decode failed for', msg.target, e);
continue;
}
const match = /^\/([^\/]*)\/(onResult|onStatus)$/.exec(msg.target);
if (!match) {
console.warn('[NetConnection] unrecognised AMF response target:', msg.target);
continue;
}
const id = match[1], kind = match[2];
const responder = this._pendingResponders && this._pendingResponders[id];
if (this._pendingResponders)
delete this._pendingResponders[id];
this._debugAMF('response', kind, 'id', id, '->', responder ? 'responder' : '(no responder)');
if (responder) {
try {
kind === 'onResult' ? responder.invokeResult(value) : responder.invokeStatus(value);
} catch (e) {
console.warn('[NetConnection] responder callback threw:', e);
}
} else if (kind === 'onStatus') {
this._dispatchStatus('NetConnection.Call.Failed', 'error');
}
}
}

private _httpFail(id: string, description: string): void {
this._debugAMF('call failed id', id, ':', description);
const responder = this._pendingResponders && this._pendingResponders[id];
if (this._pendingResponders)
delete this._pendingResponders[id];
if (responder) {
try {
responder.invokeStatus(this._makeStatusInfo('NetConnection.Call.Failed', 'error', description));
return;
} catch (e) {
console.warn('[NetConnection] responder status callback threw:', e);
}
}
this._dispatchStatus('NetConnection.Call.Failed', 'error', description);
}

/* AMF remoting packet: u16 version | u16 headerCount | u16 messageCount, then per
* message: utf targetURI | utf responseURI | u32 bodyLength | AMF body. */
private static _buildRemotingPacket(target: string, responseId: string, body: Uint8Array): Uint8Array {
const enc = new TextEncoder();
const head: number[] = [];
const u16 = (v: number) => head.push((v >>> 8) & 0xff, v & 0xff);
const u32 = (v: number) => head.push((v >>> 24) & 0xff, (v >>> 16) & 0xff, (v >>> 8) & 0xff, v & 0xff);
const utf = (str: string) => {
const b = enc.encode(str);
u16(b.length);
for (let i = 0; i < b.length; i++) head.push(b[i]);
};
u16(3); // version 3 (AMF3-capable)
u16(0); // headers
u16(1); // messages
utf(target);
utf('/' + responseId);
u32(body.length);
const out = new Uint8Array(head.length + body.length);
out.set(head, 0);
out.set(body, head.length);
return out;
}

private static _parseRemotingPacket(buf: ArrayBuffer): { target: string; body: ArrayBuffer }[] {
const dv = new DataView(buf);
const dec = new TextDecoder();
let pos = 0;
const u16 = () => { const v = dv.getUint16(pos); pos += 2; return v; };
const u32 = () => { const v = dv.getUint32(pos); pos += 4; return v; };
const utf = () => { const n = u16(); const s = dec.decode(new Uint8Array(buf, pos, n)); pos += n; return s; };
const messages: { target: string; body: ArrayBuffer }[] = [];
u16(); // version
const headers = u16();
for (let h = 0; h < headers; h++) {
utf(); pos += 1; // name, mustUnderstand
const len = u32();
if (len !== 0xffffffff) pos += len;
}
const count = u16();
for (let m = 0; m < count; m++) {
const target = utf();
utf(); // response uri (unused in replies)
const len = u32();
let body: ArrayBuffer;
if (len !== 0xffffffff && pos + len <= buf.byteLength) {
body = buf.slice(pos, pos + len);
pos += len;
} else {
body = buf.slice(pos);
pos = buf.byteLength;
}
messages.push({ target: target, body: body });
}
return messages;
}

// AS -> JS Bindings
Expand Down Expand Up @@ -88,70 +315,19 @@ export class NetConnection extends EventDispatcher {
}

connect(command: string): void {
/*
command = axCoerceString(command);

release || somewhatImplemented("public flash.net.NetConnection::connect");
this._uri = command;
if (!command) {
this._connected = true;
this.dispatchEvent(new NetStatusEvent(NetStatusEvent.NET_STATUS, false, false,
this.sec.createObjectFromJS({ level : 'status', code : 'NetConnection.Connect.Success'})));
} else {
var parsedURL = RtmpJs.parseConnectionString(command);
if (!parsedURL || !parsedURL.host ||
(parsedURL.protocol !== 'rtmp' && parsedURL.protocol !== 'rtmpt' && parsedURL.protocol !== 'rtmps')) {
this.dispatchEvent(new NetStatusEvent(NetStatusEvent.NET_STATUS, false, false,
this.sec.createObjectFromJS({ level : 'status', code : 'NetConnection.Connect.Failed'})));
return;
}

var service: IRootElementService = this.sec.player;

var rtmpProps = this.sec.createObjectFromJS({
app: parsedURL.app,
flashver: Capabilities.version,
swfUrl: service.swfUrl,
tcUrl: command,
fpad: false,
audioCodecs: 0x0FFF,
videoCodecs: 0x00FF,
videoFunction: 1,
pageUrl: service.pageUrl || service.swfUrl,
objectEncoding: 0
});

this._protocol = parsedURL.protocol;

var secured = parsedURL.protocol === 'rtmps' ||
(parsedURL.protocol === 'rtmpt' && (parsedURL.port === 443 || parsedURL.port === 8443));
this._usingTLS = secured;
var rtmpConnection: RtmpJs.BaseTransport = parsedURL.protocol === 'rtmp' || parsedURL.protocol === 'rtmps' ?
new RtmpJs.Browser.RtmpTransport({ host: parsedURL.host, port: parsedURL.port || 1935, ssl: secured }) :
new RtmpJs.Browser.RtmptTransport({ host: parsedURL.host, port: parsedURL.port || 80, ssl: secured });
this._rtmpConnection = rtmpConnection;
this._rtmpCreateStreamCallbacks = [null, null]; // reserve first two

rtmpConnection.onresponse = function (e) {
//
};
rtmpConnection.onevent = function (e) {
//
};
rtmpConnection.onconnected = function (e) {
this._connected = true;
this.dispatchEvent(new this.sec.flash.events.NetStatusEvent(events.NetStatusEvent.NET_STATUS,
false, false,
this.sec.createObjectFromJS({ level : 'status', code : 'NetConnection.Connect.Success'})));
}.bind(this);
rtmpConnection.onstreamcreated = function (e) {
console.log('#streamcreated: ' + e.streamId);
var callback = this._rtmpCreateStreamCallbacks[e.transactionId];
delete this._rtmpCreateStreamCallbacks[e.transactionId];
callback(e.stream, e.streamId);
}.bind(this);
rtmpConnection.connect(rtmpProps);
}*/
command = axCoerceString(command);
this._uri = command;
// A null command or an HTTP(S) url is a Flash Remoting gateway connection: it always
// "succeeds" immediately — calls are independent HTTP POSTs. Dispatch async so
// listeners registered right after connect() still receive the event.
if (!command || !/^rtmpt?s?:/i.test(command)) {
this._connected = true;
Promise.resolve().then(() => this._dispatchStatus('NetConnection.Connect.Success', 'status'));
return;
}
// RTMP / RTMPT / RTMPS streaming transports are not implemented.
console.warn('[NetConnection] rtmp transports are not implemented:', command);
this._dispatchStatus('NetConnection.Connect.Failed', 'error');
}

_createRtmpStream(callback) {
Expand Down
Loading