├── .gitignore ├── LICENSE ├── README.md ├── examples ├── http-client │ ├── .gitignore │ ├── NodeAmfConnection.as │ ├── NodeAmfDocument.as │ ├── README.md │ └── http-client.fla ├── http-server │ ├── README.md │ ├── amf-methods.js │ └── run-server.js ├── rtmp-client │ ├── .gitignore │ ├── NodeRtmpConnection.as │ ├── NodeRtmpDocument.as │ ├── README.md │ └── rtmp-client.fla └── rtmp-server │ ├── README.md │ └── rtmp-test.js ├── node-amf ├── amf.js ├── bin.js ├── deserialize.js ├── header.js ├── http-server.js ├── message.js ├── packet.js ├── serialize.js ├── traits.js ├── utf8.js └── utils.js ├── node-rtmp ├── README.md ├── RtmpChunk.js ├── RtmpConnection.js ├── RtmpHandshake.js └── RtmpMessage.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .gitignore 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | node-amf is dual licensed under the MIT (below) 3 | and GPL (http://www.gnu.org/licenses/gpl-3.0.txt) licenses. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL KEVIN VAN ZONNEVELD BE LIABLE FOR ANY CLAIM, DAMAGES 20 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NO LONGER MAINTAINED 2 | 3 | 4 | You're recommended to use an active fork of this library instead of this version. 5 | 6 | I wrote this library several years ago, but I no longer do any Flash work and have no use for this library myself. 7 | I don't even have a Flash compiler on my system to test pull requests. 8 | 9 | As Github doesn't provide a mechanism to retire projects, this will live here for posterity. 10 | 11 | --- 12 | 13 | 14 | # node-amf 15 | 16 | AMF module for NodeJS by [Tim Whitlock](http://twitter.com/timwhitlock) 17 | 18 | ## About 19 | 20 | A pure JavaScript implementation of [AMF](http://en.wikipedia.org/wiki/Action_Message_Format) designed for NodeJS. 21 | Can be used to create an AMF web services gateway over HTTP allowing a Flash Player client to communicate with a remote server. 22 | 23 | Run "node test.js" to perform a simple serialize & deserialize test. Check that the output matches the variables show in the script. 24 | 25 | To test a simple AMF gateway: configure and run the HTTP server in examples/http-server, then configure and run the Flash client in example/http-client. 26 | 27 | Also in this project is some experimental work implementing RTMP. This should probably be split into its own project. 28 | 29 | 30 | ## Status 31 | 32 | * The AMF library is largely complete and working, but not thoroughly tested. 33 | * The RTMP work is experimental and not ready for use. 34 | 35 | 36 | ## Known issues 37 | 38 | NodeJS has a habit of changing and depreciating function names. 39 | If you get missing function errors, then you need to upgrade NodeJS. 40 | See http://github.com/timwhitlock/node-amf/commit/62a85b8531307c86ff27daa21e48012a7558552a 41 | 42 | AMF0 is only partially supported, due to it being depreciated. AMF3 should be used by all clients. 43 | 44 | 45 | ## License 46 | 47 | node-amf is dual licensed under the MIT and GPL licenses, See LICENSE. 48 | -------------------------------------------------------------------------------- /examples/http-client/.gitignore: -------------------------------------------------------------------------------- 1 | http-client.swf 2 | -------------------------------------------------------------------------------- /examples/http-client/NodeAmfConnection.as: -------------------------------------------------------------------------------- 1 | /** 2 | * Extended NetConnection to test AMF requests 3 | * Set "host" variable to your nodejs http end point 4 | */ 5 | 6 | 7 | 8 | package { 9 | 10 | import flash.utils.setTimeout; 11 | import flash.net.NetConnection; 12 | import flash.net.ObjectEncoding; 13 | import flash.net.Responder; 14 | 15 | import flash.events.NetStatusEvent; 16 | import flash.events.IOErrorEvent; 17 | import flash.events.AsyncErrorEvent; 18 | import flash.events.SecurityErrorEvent; 19 | 20 | public class NodeAmfConnection extends NetConnection { 21 | 22 | private var host:String = 'http://192.168.51.6:8081'; 23 | private var responder:Responder; 24 | 25 | /** 26 | * Constructor 27 | */ 28 | function NodeAmfConnection(){ 29 | // connect to AMF gateway and be ready to call methods 30 | responder = new Responder( this.onResponse, this.onFault ); 31 | connect( host ); 32 | this.client = this; 33 | addEventListener( NetStatusEvent.NET_STATUS, onNetStatus ); 34 | addEventListener( IOErrorEvent.IO_ERROR, onError ); 35 | addEventListener( AsyncErrorEvent.ASYNC_ERROR, onError ); 36 | addEventListener( SecurityErrorEvent.SECURITY_ERROR, onError ); 37 | proxyType = 'HTTP'; 38 | // call all available AMF methods defined by the gateway 39 | call("test", responder ); 40 | // test a complex structure with all data types 41 | call( "echo", responder, 42 | //strings 43 | 'Hello World', 'a£b', 44 | // numbers 45 | Number.MIN_VALUE, -1234.5, -20000, -1, 0, 0.0123456, 2000000, 289764372, 1234.5678, Number.MAX_VALUE, Number.NaN, 46 | // objects 47 | [ ,,{ test:'OK'}, new Date, new Date(0) ], 48 | // other scalars 49 | true, false, null, undefined 50 | ); 51 | // test cyclic references; Array and Object 52 | var cyclicObj = { test:'ok' }; 53 | cyclicObj.self = cyclicObj; 54 | var cyclicArr = [ 'hello world' ]; 55 | cyclicArr.push( cyclicArr ); 56 | call( "echo", responder, cyclicObj, cyclicArr ); 57 | // test an async function that cannot return immediately 58 | call( "testAsync", responder ); 59 | // test a function that will timeout 60 | //call( "testDead", responder ); 61 | } 62 | 63 | 64 | /** NetStatusEvent handler - print out event info */ 65 | public function onNetStatus( Evt:NetStatusEvent ):void { 66 | trace('[onNetStatus]'); 67 | for( var s:String in Evt.info ){ 68 | trace( " > "+s+": "+Evt.info[s]); 69 | } 70 | } 71 | 72 | /** Standard error event handler - print out event info */ 73 | public function onError( Evt:Object ):void { 74 | trace('[onError]'); 75 | for( var s:String in Evt ){ 76 | trace( " > "+s+": "+String(Evt[s]) ); 77 | } 78 | } 79 | 80 | /** User-defined error event handler - this is sent to Flash by custom AMF headers in response packet */ 81 | public function onCustomError( Err:Object ):void { 82 | trace('[onCustomError]'); 83 | for( var s:String in Err ){ 84 | trace( " > "+s+": "+String(Err[s]) ); 85 | } 86 | } 87 | 88 | /** Successful Response handler - simple trace of top-level response data */ 89 | public function onResponse( returnValue:Object, s:Object = ''):void { 90 | trace('[onResponse] '+s); 91 | dump( returnValue, '', '' ); 92 | } 93 | 94 | 95 | /** Failed response handler - simple print out of fault - which is probably scalar */ 96 | public function onFault( f:Object ):void{ 97 | trace('[onFault] `'+ ( f as String ) + '`'); 98 | } 99 | 100 | 101 | /** simple dump function for inspecting returned structures */ 102 | private static function dump( value:*, tab:String = '', name:String = '' ):void { 103 | var prefix:String = tab; 104 | if( name ){ 105 | prefix += '['+name+'] '; 106 | } 107 | if( value === undefined ){ 108 | return trace( prefix + '(undefined)' ); 109 | } 110 | if( value === null ){ 111 | return trace( prefix + '(null)' ); 112 | } 113 | if( value is Number ){ 114 | return trace( prefix + '(Number) '+ value ); 115 | } 116 | if( value is String ){ 117 | return trace( prefix + '(String) "'+ value+'"' ); 118 | } 119 | if( value is Boolean ){ 120 | return trace( prefix + '(Boolean) '+ (value?'true':'false') ); 121 | } 122 | if( value is Date ){ 123 | return trace( prefix + '(Date) '+ (value as Date).toString() ); 124 | } 125 | if( value.__amfref != null ){ 126 | return trace( prefix + '(Cyclic)' ); 127 | } 128 | value.__amfref = true; 129 | if( value is Array || value.length ){ 130 | trace( prefix + '(Array length:'+value.length+') '); 131 | } 132 | else { 133 | trace( prefix + '(Object)'); 134 | } 135 | for( var s:String in value ){ 136 | if( s !== '__amfref' ){ 137 | dump( value[s], ' . '+tab, s ); 138 | } 139 | } 140 | } 141 | 142 | 143 | } 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | } -------------------------------------------------------------------------------- /examples/http-client/NodeAmfDocument.as: -------------------------------------------------------------------------------- 1 | /** 2 | * node-amf Test Flash application. 3 | * simply fires an AMF request when movie is initialized 4 | */ 5 | 6 | 7 | 8 | package { 9 | 10 | import flash.display.MovieClip; 11 | import NodeAmfConnection; 12 | 13 | public class NodeAmfDocument extends MovieClip { 14 | 15 | public function NodeAmfDocument(){ 16 | var conn:NodeAmfConnection = new NodeAmfConnection(); 17 | } 18 | 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /examples/http-client/README.md: -------------------------------------------------------------------------------- 1 | # Example Flash AMF client 2 | 3 | This client connects to the simple AMF gateway example in ../http-server 4 | 5 | * Configure and run the ../http-server/run-server.js example to run the server under NodeJS. 6 | * Configure network settings in NodeAmfConnection.as and test movie from http-client.fla 7 | -------------------------------------------------------------------------------- /examples/http-client/http-client.fla: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timwhitlock/node-amf/90e5e66cdd8c26a59728314479dd9f5b255bf6d5/examples/http-client/http-client.fla -------------------------------------------------------------------------------- /examples/http-server/README.md: -------------------------------------------------------------------------------- 1 | # Example AMF gateway for NodeJS 2 | 3 | This is the standard way to implement a RESTful AMF web services gateway over HTTP. 4 | * Configure your available web service methods in amf-methods.js 5 | * Configure and execute run-server.js to start the server. -------------------------------------------------------------------------------- /examples/http-server/amf-methods.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Available AMF gateway methods 3 | 4 | * These will get called from the AMF server. 5 | * - arguments passed in will already be native JavaScript types 6 | * - arguments returned should be native, and server will serialize response 7 | */ 8 | 9 | 10 | 11 | 12 | /** 13 | * Most basic method simply returns the string "Hello World" 14 | */ 15 | exports.test = function( callback ){ 16 | return 'Hello World'; 17 | } 18 | 19 | 20 | /** 21 | * Send back whatever was sent to us 22 | */ 23 | exports.echo = function( callback, more ){ 24 | return [].slice.call( arguments, 1 ); 25 | } 26 | 27 | 28 | /** 29 | * simulate an asynchronous function that cannot return 30 | */ 31 | exports.testAsync = function( callback ){ 32 | setTimeout( function(){ callback('Hello Again') }, 100 ); 33 | } 34 | 35 | 36 | /** 37 | * simulate an error whereby the function never returns and fails to callback 38 | */ 39 | exports.testDead = function( callback ){ 40 | // do nothing - gateway will timeout 41 | } 42 | 43 | -------------------------------------------------------------------------------- /examples/http-server/run-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example AMF gateway HTTP server 3 | */ 4 | 5 | 6 | // configure for your environment 7 | var listenPort = 8081; 8 | var listenHost = '127.0.0.1'; 9 | // maximum length of time server will wait for a method call to callback, defaults to 1000ms 10 | var timeout = 5000; 11 | 12 | // require your gateway methods into an object 13 | var methods = require('./amf-methods'); 14 | 15 | // require the HTTP gateway server, ensuring relative paths are correct 16 | var server = require('../../node-amf/http-server'); 17 | 18 | // start the server with the required params and gateway methods 19 | server.start( listenPort, listenHost, methods, timeout ); 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/rtmp-client/.gitignore: -------------------------------------------------------------------------------- 1 | *.swf 2 | -------------------------------------------------------------------------------- /examples/rtmp-client/NodeRtmpConnection.as: -------------------------------------------------------------------------------- 1 | /** 2 | * Extended NetConnection to test AMF requests 3 | * Set "host" variable to your nodejs http end point 4 | */ 5 | 6 | 7 | 8 | package { 9 | 10 | import flash.utils.setTimeout; 11 | import flash.net.NetConnection; 12 | import flash.net.NetStream; 13 | import flash.net.ObjectEncoding; 14 | import flash.net.Responder; 15 | 16 | import flash.events.NetStatusEvent; 17 | import flash.events.IOErrorEvent; 18 | import flash.events.AsyncErrorEvent; 19 | import flash.events.SecurityErrorEvent; 20 | 21 | public class NodeRtmpConnection extends NetConnection { 22 | 23 | private var host:String = 'rtmp://192.168.51.6:1935/test'; 24 | private var responder:Responder; 25 | private var stream:NetStream; 26 | 27 | /** 28 | * Constructor 29 | */ 30 | function NodeRtmpConnection(){ 31 | addEventListener( NetStatusEvent.NET_STATUS, onNetStatus ); 32 | addEventListener( IOErrorEvent.IO_ERROR, onError ); 33 | addEventListener( AsyncErrorEvent.ASYNC_ERROR, onError ); 34 | addEventListener( SecurityErrorEvent.SECURITY_ERROR, onError ); 35 | objectEncoding = ObjectEncoding.AMF3; 36 | connect(host); 37 | } 38 | 39 | 40 | private function connectStream():void { 41 | stream = new NetStream(this); 42 | stream.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); 43 | stream.client = this; 44 | } 45 | 46 | public function onMetaData(info:Object):void { 47 | trace("metadata: duration=" + info.duration + " width=" + info.width + " height=" + info.height + " framerate=" + info.framerate); 48 | } 49 | 50 | public function onCuePoint(info:Object):void { 51 | trace("cuepoint: time=" + info.time + " name=" + info.name + " type=" + info.type); 52 | } 53 | 54 | 55 | 56 | 57 | /** NetStatusEvent handler - print out event info */ 58 | public function onNetStatus( Evt:NetStatusEvent ):void { 59 | trace('[onNetStatus]'); 60 | for( var s:String in Evt.info ){ 61 | trace( " > "+s+": "+Evt.info[s]); 62 | } 63 | } 64 | 65 | /** Standard error event handler - print out event info */ 66 | public function onError( Evt:Object ):void { 67 | trace('[onError]'); 68 | for( var s:String in Evt ){ 69 | trace( " > "+s+": "+String(Evt[s]) ); 70 | } 71 | } 72 | 73 | /** User-defined error event handler - this is sent to Flash by custom AMF headers in response packet */ 74 | public function onCustomError( Err:Object ):void { 75 | trace('[onCustomError]'); 76 | for( var s:String in Err ){ 77 | trace( " > "+s+": "+String(Err[s]) ); 78 | } 79 | } 80 | 81 | /** Successful Response handler - simple trace of top-level response data */ 82 | public function onResponse( returnValue:Object, s:Object = ''):void { 83 | trace('[onResponse] '+s); 84 | dump( returnValue, '', '' ); 85 | } 86 | 87 | 88 | /** Failed response handler - simple print out of fault - which is probably scalar */ 89 | public function onFault( f:Object ):void{ 90 | trace('[onFault] `'+ ( f as String ) + '`'); 91 | } 92 | 93 | 94 | /** simple dump function for inspecting returned structures */ 95 | private static function dump( value:*, tab:String = '', name:String = '' ):void { 96 | var prefix:String = tab; 97 | if( name ){ 98 | prefix += '['+name+'] '; 99 | } 100 | if( value === undefined ){ 101 | return trace( prefix + '(undefined)' ); 102 | } 103 | if( value === null ){ 104 | return trace( prefix + '(null)' ); 105 | } 106 | if( value is Number ){ 107 | return trace( prefix + '(Number) '+ value ); 108 | } 109 | if( value is String ){ 110 | return trace( prefix + '(String) "'+ value+'"' ); 111 | } 112 | if( value is Boolean ){ 113 | return trace( prefix + '(Boolean) '+ (value?'true':'false') ); 114 | } 115 | if( value is Date ){ 116 | return trace( prefix + '(Date) '+ (value as Date).toString() ); 117 | } 118 | if( value.__amfref != null ){ 119 | return trace( prefix + '(Cyclic)' ); 120 | } 121 | value.__amfref = true; 122 | if( value is Array || value.length ){ 123 | trace( prefix + '(Array length:'+value.length+') '); 124 | } 125 | else { 126 | trace( prefix + '(Object)'); 127 | } 128 | for( var s:String in value ){ 129 | if( s !== '__amfref' ){ 130 | dump( value[s], ' . '+tab, s ); 131 | } 132 | } 133 | } 134 | 135 | 136 | } 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | } -------------------------------------------------------------------------------- /examples/rtmp-client/NodeRtmpDocument.as: -------------------------------------------------------------------------------- 1 | /** 2 | * node-amf Test RTMP application. 3 | */ 4 | 5 | 6 | 7 | package { 8 | 9 | import flash.display.MovieClip; 10 | import NodeRtmpConnection; 11 | import flash.display.LoaderInfo; 12 | 13 | public class NodeRtmpDocument extends MovieClip { 14 | 15 | public function NodeRtmpDocument(){ 16 | var conn:NodeRtmpConnection = new NodeRtmpConnection(); 17 | //trace( this.loaderInfo.url ); 18 | //trace( this.loaderInfo.url.length ); 19 | } 20 | 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /examples/rtmp-client/README.md: -------------------------------------------------------------------------------- 1 | # Example RTMP client 2 | 3 | Used for testing the experimental RTMP server at ../rtmp-server 4 | 5 | This work is incomplete and not ready for use 6 | 7 | -------------------------------------------------------------------------------- /examples/rtmp-client/rtmp-client.fla: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timwhitlock/node-amf/90e5e66cdd8c26a59728314479dd9f5b255bf6d5/examples/rtmp-client/rtmp-client.fla -------------------------------------------------------------------------------- /examples/rtmp-server/README.md: -------------------------------------------------------------------------------- 1 | # Example RTMP server 2 | 3 | See ../../node-rtmp -------------------------------------------------------------------------------- /examples/rtmp-server/rtmp-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test RTMP socket - unstable - do not use 3 | */ 4 | var sys = require('sys'); 5 | var net = require('net'); 6 | 7 | var RtmpConnection = require('../../node-rtmp/RtmpConnection').RtmpConnection; 8 | 9 | 10 | 11 | // Global variables 12 | var server = net.createServer(); 13 | 14 | 15 | /** */ 16 | server.addListener('connection', function( socket ) { 17 | try { 18 | sys.puts('server.connection'); 19 | new RtmpConnection( socket ); 20 | } 21 | catch( Er ){ 22 | sys.puts('Error in server connect handler: '+Er.message ); 23 | } 24 | } ); 25 | 26 | 27 | 28 | /** */ 29 | server.addListener('close', function(errno) { 30 | sys.puts('server.close '+errno); 31 | } ); 32 | 33 | 34 | 35 | server.listen( 1935, "192.168.51.6" ); 36 | sys.puts('Server ready'); 37 | -------------------------------------------------------------------------------- /node-amf/amf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Top level entry to AMF library for Node JS 3 | */ 4 | 5 | // object prototypes 6 | var AMFTraits = require('./traits').AMFTraits; 7 | var AMFPacket = require('./packet').AMFPacket; 8 | var AMFMessage = require('./message').AMFMessage; 9 | var AMFSerializer = require('./serialize').AMFSerializer; 10 | var AMFDeserializer = require('./deserialize').AMFDeserializer; 11 | 12 | 13 | /** 14 | * @return AMFPacket 15 | */ 16 | exports.packet = function( src ){ 17 | var Packet; 18 | if( src ){ 19 | Packet = AMFPacket.deserialize( src ); 20 | } 21 | if( ! Packet ){ 22 | Packet = new AMFPacket; 23 | } 24 | return Packet; 25 | } 26 | 27 | 28 | /** 29 | * @return AMFMessage 30 | */ 31 | exports.message = function( value, requestURI, responseURI ){ 32 | return new AMFMessage( value, requestURI, responseURI ); 33 | } 34 | 35 | 36 | /** 37 | * @return AMFHeader 38 | */ 39 | exports.header = function( value, requestURI, responseURI ){ 40 | return new AMFMessage( value, requestURI, responseURI ); 41 | } 42 | 43 | 44 | /** 45 | * @return AMFDeserializer 46 | */ 47 | exports.deserializer = function( src ){ 48 | return new AMFDeserializer( src ); 49 | } 50 | 51 | 52 | /** 53 | * @return AMFSerializer 54 | */ 55 | exports.serializer = function(){ 56 | return new AMFSerializer( 3 ); 57 | } 58 | 59 | 60 | /** 61 | * @return AMFTraits 62 | */ 63 | exports.traits = function(){ 64 | return new AMFTraits; 65 | } 66 | 67 | 68 | 69 | /** 70 | * pseudo constants 71 | */ 72 | exports.AMF0 = 0; 73 | exports.AMF3 = 3; 74 | // AMF0 markers 75 | exports.AMF0_NUMBER = 0; 76 | exports.AMF0_BOOLEAN = 1; 77 | exports.AMF0_STRING = 2; 78 | exports.AMF0_OBJECT = 3; 79 | exports.AMF0_MOVIECLIP = 4; 80 | exports.AMF0_NULL = 5; 81 | exports.AMF0_UNDEFINED = 6; 82 | exports.AMF0_REFERENCE = 7; 83 | exports.AMF0_ECMA_ARRAY = 8; 84 | exports.AMF0_OBJECT_END = 9; 85 | exports.AMF0_STRICT_ARRAY = 0x0A; 86 | exports.AMF0_DATE = 0x0B; 87 | exports.AMF0_LONG_STRING = 0x0C; 88 | exports.AMF0_UNSUPPORTED = 0x0D; 89 | exports.AMF0_RECORDSET = 0x0E; 90 | exports.AMF0_XML_DOC = 0x0F; 91 | exports.AMF0_TYPED_OBJECT = 0x10; 92 | exports.AMF0_AMV_PLUS = 0x11; 93 | // AMF3 markers 94 | exports.AMF3_UNDEFINED = 0; 95 | exports.AMF3_NULL = 1; 96 | exports.AMF3_FALSE = 2; 97 | exports.AMF3_TRUE = 3; 98 | exports.AMF3_INTEGER = 4; 99 | exports.AMF3_DOUBLE = 5; 100 | exports.AMF3_STRING = 6; 101 | exports.AMF3_XML_DOC = 7; 102 | exports.AMF3_DATE = 8; 103 | exports.AMF3_ARRAY = 9; 104 | exports.AMF3_OBJECT = 0x0A; 105 | exports.AMF3_XML = 0x0B; 106 | exports.AMF3_BYTE_ARRAY = 0x0C; 107 | 108 | 109 | -------------------------------------------------------------------------------- /node-amf/bin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Binary tools for node-amf 3 | */ 4 | 5 | var BinaryParser; 6 | 7 | exports.parser = function( bigEndian, allowExceptions ){ 8 | return new BinaryParser( bigEndian, allowExceptions ); 9 | } 10 | 11 | 12 | 13 | 14 | 15 | 16 | // code below copied asis from http://jsfromhell.com/ 17 | 18 | 19 | //+ Jonas Raoni Soares Silva 20 | //@ http://jsfromhell.com/classes/binary-parser [rev. #1] 21 | 22 | BinaryParser = function(bigEndian, allowExceptions){ 23 | this.bigEndian = bigEndian, this.allowExceptions = allowExceptions; 24 | }; 25 | with({p: BinaryParser.prototype}){ 26 | p.encodeFloat = function(number, precisionBits, exponentBits){ 27 | var bias = Math.pow(2, exponentBits - 1) - 1, minExp = -bias + 1, maxExp = bias, minUnnormExp = minExp - precisionBits, 28 | status = isNaN(n = parseFloat(number)) || n == -Infinity || n == +Infinity ? n : 0, 29 | exp = 0, len = 2 * bias + 1 + precisionBits + 3, bin = new Array(len), 30 | signal = (n = status !== 0 ? 0 : n) < 0, n = Math.abs(n), intPart = Math.floor(n), floatPart = n - intPart, 31 | i, lastBit, rounded, j, result; 32 | for(i = len; i; bin[--i] = 0); 33 | for(i = bias + 2; intPart && i; bin[--i] = intPart % 2, intPart = Math.floor(intPart / 2)); 34 | for(i = bias + 1; floatPart > 0 && i; (bin[++i] = ((floatPart *= 2) >= 1) - 0) && --floatPart); 35 | for(i = -1; ++i < len && !bin[i];); 36 | if(bin[(lastBit = precisionBits - 1 + (i = (exp = bias + 1 - i) >= minExp && exp <= maxExp ? i + 1 : bias + 1 - (exp = minExp - 1))) + 1]){ 37 | if(!(rounded = bin[lastBit])) 38 | for(j = lastBit + 2; !rounded && j < len; rounded = bin[j++]); 39 | for(j = lastBit + 1; rounded && --j >= 0; (bin[j] = !bin[j] - 0) && (rounded = 0)); 40 | } 41 | for(i = i - 2 < 0 ? -1 : i - 3; ++i < len && !bin[i];); 42 | 43 | (exp = bias + 1 - i) >= minExp && exp <= maxExp ? ++i : exp < minExp && 44 | (exp != bias + 1 - len && exp < minUnnormExp && this.warn("encodeFloat::float underflow"), i = bias + 1 - (exp = minExp - 1)); 45 | (intPart || status !== 0) && (this.warn(intPart ? "encodeFloat::float overflow" : "encodeFloat::" + status), 46 | exp = maxExp + 1, i = bias + 2, status == -Infinity ? signal = 1 : isNaN(status) && (bin[i] = 1)); 47 | for(n = Math.abs(exp + bias), j = exponentBits + 1, result = ""; --j; result = (n % 2) + result, n = n >>= 1); 48 | for(n = 0, j = 0, i = (result = (signal ? "1" : "0") + result + bin.slice(i, i + precisionBits).join("")).length, r = []; 49 | i; n += (1 << j) * result.charAt(--i), j == 7 && (r[r.length] = String.fromCharCode(n), n = 0), j = (j + 1) % 8); 50 | r[r.length] = n ? String.fromCharCode(n) : ""; 51 | return (this.bigEndian ? r.reverse() : r).join(""); 52 | }; 53 | p.encodeInt = function(number, bits, signed){ 54 | var max = Math.pow(2, bits), r = []; 55 | (number >= max || number < -(max >> 1)) && this.warn("encodeInt::overflow") && (number = 0); 56 | number < 0 && (number += max); 57 | for(; number; r[r.length] = String.fromCharCode(number % 256), number = Math.floor(number / 256)); 58 | for(bits = -(-bits >> 3) - r.length; bits--; r[r.length] = "\0"); 59 | return (this.bigEndian ? r.reverse() : r).join(""); 60 | }; 61 | p.decodeFloat = function(data, precisionBits, exponentBits){ 62 | var b = ((b = new this.Buffer(this.bigEndian, data)).checkBuffer(precisionBits + exponentBits + 1), b), 63 | bias = Math.pow(2, exponentBits - 1) - 1, signal = b.readBits(precisionBits + exponentBits, 1), 64 | exponent = b.readBits(precisionBits, exponentBits), significand = 0, 65 | divisor = 2, curByte = b.buffer.length + (-precisionBits >> 3) - 1, 66 | byteValue, startBit, mask; 67 | do 68 | for(byteValue = b.buffer[ ++curByte ], startBit = precisionBits % 8 || 8, mask = 1 << startBit; 69 | mask >>= 1; (byteValue & mask) && (significand += 1 / divisor), divisor *= 2); 70 | while(precisionBits -= startBit); 71 | return exponent == (bias << 1) + 1 ? significand ? NaN : signal ? -Infinity : +Infinity 72 | : (1 + signal * -2) * (exponent || significand ? !exponent ? Math.pow(2, -bias + 1) * significand 73 | : Math.pow(2, exponent - bias) * (1 + significand) : 0); 74 | }; 75 | p.decodeInt = function(data, bits, signed){ 76 | var b = new this.Buffer(this.bigEndian, data), x = b.readBits(0, bits), max = Math.pow(2, bits); 77 | return signed && x >= max / 2 ? x - max : x; 78 | }; 79 | with({p: (p.Buffer = function(bigEndian, buffer){ 80 | this.bigEndian = bigEndian || 0, this.buffer = [], this.setBuffer(buffer); 81 | }).prototype}){ 82 | p.readBits = function(start, length){ 83 | //shl fix: Henri Torgemane ~1996 (compressed by Jonas Raoni) 84 | function shl(a, b){ 85 | for(++b; --b; a = ((a %= 0x7fffffff + 1) & 0x40000000) == 0x40000000 ? a * 2 : (a - 0x40000000) * 2 + 0x7fffffff + 1); 86 | return a; 87 | } 88 | if(start < 0 || length <= 0) 89 | return 0; 90 | this.checkBuffer(start + length); 91 | for(var offsetLeft, offsetRight = start % 8, curByte = this.buffer.length - (start >> 3) - 1, 92 | lastByte = this.buffer.length + (-(start + length) >> 3), diff = curByte - lastByte, 93 | sum = ((this.buffer[ curByte ] >> offsetRight) & ((1 << (diff ? 8 - offsetRight : length)) - 1)) 94 | + (diff && (offsetLeft = (start + length) % 8) ? (this.buffer[ lastByte++ ] & ((1 << offsetLeft) - 1)) 95 | << (diff-- << 3) - offsetRight : 0); diff; sum += shl(this.buffer[ lastByte++ ], (diff-- << 3) - offsetRight) 96 | ); 97 | return sum; 98 | }; 99 | p.setBuffer = function(data){ 100 | if(data){ 101 | for(var l, i = l = data.length, b = this.buffer = new Array(l); i; b[l - i] = data.charCodeAt(--i)); 102 | this.bigEndian && b.reverse(); 103 | } 104 | }; 105 | p.hasNeededBits = function(neededBits){ 106 | return this.buffer.length >= -(-neededBits >> 3); 107 | }; 108 | p.checkBuffer = function(neededBits){ 109 | if(!this.hasNeededBits(neededBits)) 110 | throw new Error("checkBuffer::missing bytes"); 111 | }; 112 | } 113 | p.warn = function(msg){ 114 | if(this.allowExceptions) 115 | throw new Error(msg); 116 | return 1; 117 | }; 118 | p.toSmall = function(data){return this.decodeInt(data, 8, true);}; 119 | p.fromSmall = function(number){return this.encodeInt(number, 8, true);}; 120 | p.toByte = function(data){return this.decodeInt(data, 8, false);}; 121 | p.fromByte = function(number){return this.encodeInt(number, 8, false);}; 122 | p.toShort = function(data){return this.decodeInt(data, 16, true);}; 123 | p.fromShort = function(number){return this.encodeInt(number, 16, true);}; 124 | p.toWord = function(data){return this.decodeInt(data, 16, false);}; 125 | p.fromWord = function(number){return this.encodeInt(number, 16, false);}; 126 | p.toInt = function(data){return this.decodeInt(data, 32, true);}; 127 | p.fromInt = function(number){return this.encodeInt(number, 32, true);}; 128 | p.toDWord = function(data){return this.decodeInt(data, 32, false);}; 129 | p.fromDWord = function(number){return this.encodeInt(number, 32, false);}; 130 | p.toFloat = function(data){return this.decodeFloat(data, 23, 8);}; 131 | p.fromFloat = function(number){return this.encodeFloat(number, 23, 8);}; 132 | p.toDouble = function(data){return this.decodeFloat(data, 52, 11);}; 133 | p.fromDouble = function(number){return this.encodeFloat(number, 52, 11);}; 134 | } 135 | -------------------------------------------------------------------------------- /node-amf/deserialize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AMF deserializer 3 | */ 4 | 5 | /** dependencies */ 6 | var amf = require('./amf'); 7 | var utils = require('./utils'); 8 | var utf8 = require('./utf8'); 9 | var bin = require('./bin'); 10 | 11 | 12 | /** export constructor */ 13 | exports.AMFDeserializer = AMFDeserializer; 14 | 15 | 16 | 17 | // ---------------------------------- 18 | 19 | 20 | 21 | /** Constructor */ 22 | function AMFDeserializer( src ){ 23 | this.s = src || ''; 24 | this.i = 0; 25 | this.resetRefs(); 26 | this.beParser = bin.parser( true, true ); // <- big endian binary unpacker 27 | this.leParser = bin.parser( false, true ); // <- little endian binary unpacker 28 | } 29 | 30 | 31 | 32 | /** */ 33 | AMFDeserializer.prototype.resetRefs = function(){ 34 | this.refObj = []; // object references 35 | this.refStr = []; // string references 36 | this.refTra = []; // trait references 37 | } 38 | 39 | 40 | 41 | /** */ 42 | AMFDeserializer.prototype.shiftBytes = function( n ){ 43 | if( n === 0 ){ 44 | return ''; 45 | } 46 | var s = this.s.slice( 0, n ); 47 | if( s.length !== n ){ 48 | throw new Error("Not enough input to read "+n+" bytes, got "+s.length+", offset "+this.i); 49 | } 50 | this.s = this.s.slice(n); 51 | this.i += n; 52 | return s; 53 | } 54 | 55 | 56 | /** */ 57 | AMFDeserializer.prototype.readU8 = function(){ 58 | var s = this.shiftBytes(1); 59 | return s.charCodeAt(0); 60 | } 61 | 62 | 63 | /** */ 64 | AMFDeserializer.prototype.readU16 = function (){ 65 | var s = this.shiftBytes(2); 66 | return this.beParser.toWord( s ); 67 | } 68 | 69 | 70 | /** */ 71 | AMFDeserializer.prototype.readU32 = function(){ 72 | var s = this.shiftBytes(4); 73 | return this.beParser.toDWord( s ); 74 | } 75 | 76 | 77 | /** */ 78 | AMFDeserializer.prototype.readDouble = function(){ 79 | var s = this.shiftBytes(8); 80 | if( '\0\0\0\0\0\0\xF8\x7F' === s ){ 81 | return Number.NaN; 82 | } 83 | return this.beParser.toDouble( s ); 84 | } 85 | 86 | 87 | /** */ 88 | AMFDeserializer.prototype.readU29 = function(){ 89 | var i; 90 | var n = 0; 91 | var t = 0; 92 | while ( true ){ 93 | if( ++t === 5 ){ 94 | throw new Error("U29 range error, offset "+this.i); 95 | } 96 | i = this.readU8(); 97 | // final whole byte if fourth bit 98 | if( 4 === t ){ 99 | n = i | ( n << 1 ); 100 | break; 101 | } 102 | // else take just 7 bits 103 | n |= ( i & 0x7F ); 104 | // next byte is part of the sequence if high bit is set 105 | if( i & 0x80 ){ 106 | n <<= 7; 107 | continue; 108 | } 109 | // else is final byte 110 | else { 111 | break; 112 | } 113 | } 114 | return n; 115 | } 116 | 117 | 118 | 119 | /** 120 | * @return int signed integer 121 | */ 122 | AMFDeserializer.prototype.readInteger = function( version ){ 123 | if( version === amf.AMF0 ){ 124 | return this.readDouble(); 125 | } 126 | // else AMF3 U29 127 | return this.readU29(); 128 | } 129 | 130 | 131 | 132 | 133 | 134 | /** */ 135 | AMFDeserializer.prototype.readUTF8 = function( version ){ 136 | var str, len; 137 | // AMF3 supports string references 138 | if( version === amf.AMF3 || version == null ){ 139 | var n = this.readU29(); 140 | if( n & 1 ){ 141 | len = n >> 1; 142 | // index string unless empty 143 | if( len === 0 ){ 144 | return ''; 145 | } 146 | str = this.shiftBytes( len ); 147 | this.refStr.push( str ); 148 | } 149 | else { 150 | var idx = n >> 1; 151 | if( this.refStr[idx] == null ){ 152 | throw new Error("No string reference at index "+idx+", offset "+this.i); 153 | } 154 | str = this.refStr[idx]; 155 | } 156 | } 157 | // else simple AMF0 string 158 | else { 159 | len = this.readU16(); 160 | str = this.shiftBytes( len ); 161 | } 162 | return utf8.collapse(str); 163 | } 164 | 165 | 166 | 167 | /** */ 168 | AMFDeserializer.prototype.readValue = function( version ){ 169 | var marker = this.readU8(); 170 | // support AMV+ switch 171 | if( version === amf.AMF0 && marker === amf.AMF0_AMV_PLUS ){ 172 | version = amf.AMF3; 173 | marker = this.readU8(); 174 | } 175 | switch( version ){ 176 | // AMF 3 types 177 | case amf.AMF3: 178 | switch( marker ){ 179 | case amf.AMF3_UNDEFINED: 180 | return undefined; 181 | case amf.AMF3_NULL: 182 | return null; 183 | case amf.AMF3_FALSE: 184 | return false; 185 | case amf.AMF3_TRUE: 186 | return true; 187 | case amf.AMF3_INTEGER: 188 | return this.readInteger(); 189 | case amf.AMF3_DOUBLE: 190 | return this.readDouble(); 191 | case amf.AMF3_STRING: 192 | return this.readUTF8( amf.AMF3 ); 193 | case amf.AMF3_ARRAY: 194 | return this.readArray(); 195 | case amf.AMF3_OBJECT: 196 | return this.readObject( amf.AMF3 ); 197 | case amf.AMF3_DATE: 198 | return this.readDate(); 199 | case amf.AMF3_BYTE_ARRAY: 200 | throw new Error('ByteArrays not yet supported, sorry'); 201 | //return this.readByteArray(); 202 | default: 203 | throw new Error('Type error, unsupported AMF3 marker: 0x' +utils.leftPad(marker.toString(16),2,'0')+ ', offset '+this.i); 204 | } 205 | // default to AMF0 206 | default: 207 | switch( marker ){ 208 | case amf.AMF0_NUMBER: 209 | return this.readDouble(); 210 | case amf.AMF0_STRING: 211 | return this.readUTF8( amf.AMF0 ); 212 | case amf.AMF0_UNDEFINED: 213 | return undefined; 214 | case amf.AMF0_NULL: 215 | return null; 216 | case amf.AMF0_BOOLEAN: 217 | return this.readBoolean(); 218 | case amf.AMF0_STRICT_ARRAY: 219 | return this.readStrictArray(); 220 | case amf.AMF0_DATE: 221 | return this.readDate(); 222 | case amf.AMF0_OBJECT: 223 | return this.readObject( amf.AMF0 ); 224 | default: 225 | throw new Error('Type error, unsupported AMF0 marker: 0x' +utils.leftPad(marker.toString(16),2,'0')+ ', offset '+this.i); 226 | } 227 | } 228 | } 229 | 230 | 231 | /** */ 232 | AMFDeserializer.prototype.readBoolean = function(){ 233 | return Boolean( this.readU8() ); 234 | } 235 | 236 | 237 | /** */ 238 | AMFDeserializer.prototype.readStrictArray = function(){ 239 | var a = []; 240 | var n = this.readU32(); 241 | for( var i = 0; i < n; i++ ){ 242 | a.push( this.readValue( amf.AMF0 ) ); 243 | } 244 | return a; 245 | } 246 | 247 | 248 | /** */ 249 | AMFDeserializer.prototype.readArray = function(){ 250 | var a = []; 251 | var n = this.readU29(); 252 | // reference or value 253 | if( n & 1 ){ 254 | this.refObj.push(a); 255 | // count dense portion 256 | var len = n >> 1; 257 | // iterate over over associative portion, until empty string terminates 258 | var key; 259 | while( key = this.readUTF8(amf.AMF3) ){ 260 | a[key] = this.readValue(amf.AMF3); 261 | } 262 | // append dense values 263 | for( var i = 0; i < len; i++ ){ 264 | a.push( this.readValue( amf.AMF3 ) ); 265 | } 266 | } 267 | // else is reference index 268 | else { 269 | var idx = n >> 1; 270 | if( this.refObj[idx] == null ){ 271 | throw new Error("No array reference at index "+idx+", offset "+this.i); 272 | } 273 | a = this.refObj[idx]; 274 | } 275 | return a; 276 | } 277 | 278 | 279 | 280 | /** */ 281 | AMFDeserializer.prototype.readObject = function( version ){ 282 | var prop, Obj = {}; 283 | // support AMF0 objects 284 | if( version === amf.AMF0 ){ 285 | while( prop = this.readUTF8( amf.AMF0 ) ){ 286 | Obj[prop] = this.readValue( amf.AMF0 ); 287 | } 288 | // next must be object end marker 289 | var end = this.readU8(); 290 | if( end !== amf.AMF0_OBJECT_END ){ 291 | throw new Error('Expected object end marker, got 0x'+end.toString(16) ); 292 | } 293 | return Obj; 294 | } 295 | // else assume AMF3 296 | var Traits; 297 | // check if instance follows (U29O-traits) 298 | var n = this.readU29(); 299 | if( n & 1 ){ 300 | // check if trait data follows 301 | if( n & 2 ){ 302 | Traits = amf.traits(); 303 | this.refTra.push( Traits ); 304 | // check if traits externalizable follows (U29O-traits-ext) 305 | if( n & 4 ){ 306 | Traits.clss = this.readUTF8( amf.AMF3 ); 307 | // follows an indeterminable number of bytes 308 | // Extenalizable server-side class must perform custom deserialization 309 | // @todo Externalizable class deserializing 310 | throw new Error('Externalizable classes not yet supported, sorry'); 311 | } 312 | else { 313 | Traits.dyn = Boolean( n & 8 ); 314 | Traits.clss = this.readUTF8( amf.AMF3 ); 315 | // iterate over declared member names 316 | var proplen = n >> 4; 317 | for( var i = 0, prop; i < proplen; i++ ){ 318 | prop = this.readUTF8( amf.AMF3 ); 319 | Traits.props.push( prop ); 320 | } 321 | } 322 | } 323 | // else trait reference (U29O-traits-ref) 324 | else { 325 | var idx = n >> 2; 326 | if( this.refTra[idx] == null ){ 327 | throw new Error("No traits reference at index "+idx+", offset "+this.i); 328 | } 329 | Traits = this.refTra[idx]; 330 | } 331 | // Have traits - Construct instance 332 | // @todo support class mapping somehow? 333 | this.refObj.push( Obj ); 334 | for( var i = 0; i < Traits.props.length; i++ ){ 335 | prop = Traits.props[i]; 336 | Obj[prop] = this.readValue( amf.AMF3 ); 337 | } 338 | // adding type to JSON object so we can remember it and pass back to server 339 | if( Traits.clss){ 340 | Obj["type"] = Traits.clss; 341 | } 342 | 343 | // iterate over dynamic properties until empty string 344 | if( Traits.dyn ){ 345 | while( prop = this.readUTF8( amf.AMF3 ) ){ 346 | Obj[prop] = this.readValue( amf.AMF3 ); 347 | } 348 | } 349 | } 350 | // else object reference ( U29O-ref ) 351 | else { 352 | var idx = n >> 1; 353 | if( this.refObj[idx] == null ){ 354 | throw new Error("No object reference at index "+idx+", offset "+this.i); 355 | } 356 | Obj = this.refObj[idx]; 357 | } 358 | return Obj; 359 | } 360 | 361 | 362 | /** */ 363 | AMFDeserializer.prototype.readDate = function(){ 364 | var u, d; 365 | // check if instance follows (U29O-ref) 366 | var n = this.readU29(); 367 | if( n & 1 ){ 368 | // create and index a new date object 369 | u = this.readDouble(); 370 | d = new Date( u ); 371 | this.refObj.push( d ); 372 | } 373 | else { 374 | var idx = n >> 1; 375 | if( this.refObj[idx] == null || ! this.refObj[idx] instanceof Date ){ 376 | throw new Error("No date object reference at index "+idx+", offset "+this.i); 377 | } 378 | d = this.refObj[idx]; 379 | } 380 | return d; 381 | } 382 | 383 | 384 | 385 | 386 | 387 | /* @todo port byte array object to JS? - below is PHP 388 | AMFDeserializer.prototype.readByteArray = function(){ 389 | $n = $this->read_U29(); 390 | // test if instance follows 391 | if( $n & 1 ){ 392 | $len = $n >> 1; 393 | $raw = $this->shift_bytes( $len ); 394 | $Obj = new AMFByteArray( $raw ); 395 | // index byte array, even if zero length 396 | $this->ref_obj[] = $Obj; 397 | } 398 | // else object reference ( U29O-ref ) 399 | else { 400 | $idx = $n >> 1; 401 | if( ! isset($this->ref_obj[$idx]) ){ 402 | throw new Error("No byte array reference at index $idx, offset $this->i"); 403 | } 404 | $Obj = $this->ref_obj[$idx]; 405 | } 406 | return $Obj; 407 | } 408 | */ 409 | 410 | 411 | /** 412 | * @return AMFHeader 413 | */ 414 | AMFDeserializer.prototype.readHeader = function(){ 415 | this.resetRefs(); 416 | var name = this.readUTF8( amf.AMF0 ); 417 | var Header = amf.header( name, '' ); 418 | Header.mustunderstand = Boolean( this.readU8() ); 419 | var len = this.readU32(); // we won't actually use the length 420 | // @todo lazy creation of header by storing known header byte length 421 | Header.value = this.readValue( amf.AMF0 ); 422 | return Header; 423 | } 424 | 425 | 426 | 427 | /** 428 | * @return AMFMessage 429 | */ 430 | AMFDeserializer.prototype.readMessage = function(){ 431 | this.resetRefs(); 432 | var Msg = amf.message('','',''); 433 | // request URI - AMF0 UTF-8 434 | Msg.requestURI = this.readUTF8( amf.AMF0 ); 435 | // response URI - AMF0 UTF-8 436 | Msg.responseURI = this.readUTF8( amf.AMF0 ); 437 | // message length, which may be -1, shall be ignored 438 | var len = this.readU32(); // we won't actually use the length 439 | // message value always AMF0 even in AMF3 440 | Msg.value = this.readValue( amf.AMF0 ); 441 | return Msg; 442 | } 443 | 444 | 445 | 446 | 447 | 448 | 449 | -------------------------------------------------------------------------------- /node-amf/header.js: -------------------------------------------------------------------------------- 1 | 2 | /** dependencies */ 3 | var amf = require('./amf'); 4 | 5 | 6 | /** export constructor */ 7 | exports.AMFHeader = AMFHeader; 8 | 9 | 10 | 11 | // ---------------------------------- 12 | 13 | 14 | function AMFHeader( name, value ){ 15 | this.mustunderstand = false; 16 | this.name = name; 17 | this.value = value; 18 | } 19 | 20 | 21 | /** */ 22 | AMFHeader.prototype.toString = function(){ 23 | return '[Object AMFHeader]'; 24 | } 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /node-amf/http-server.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Require system libraries 4 | var sys = require('sys'); 5 | var http = require('http'); 6 | 7 | 8 | // Require node-amf module libraries 9 | var amf = require('./amf'); 10 | var utils = require('./utils'); 11 | 12 | 13 | /** 14 | * @param Number port on which to listen 15 | * @param String address on which to listen 16 | * @param Object an object containing available gateway methods 17 | * @return bool 18 | */ 19 | exports.start = function( listenPort, listenHost, methods, timeout ){ 20 | 21 | var server = http.createServer(); 22 | server.addListener( 'request', onRequest ); 23 | //server.addListener( 'connection', function( conn ){ sys.puts('[server.connection]'); } ); 24 | //server.addListener( 'close', function( errno ){ sys.puts('[server.close]'); } ); 25 | server.listen( listenPort, listenHost ); 26 | sys.log('AMF gateway listening on : '+listenHost+':'+listenPort); 27 | return true; 28 | 29 | /** 30 | * Request handler: 31 | * - deserializes request packet 32 | * - calls AMF service 33 | * - serializes response packet 34 | */ 35 | function onRequest( req, res ){ 36 | 37 | // we must work in binary, UTF-8 strings will be handled by deserializer 38 | req.setEncoding('binary'); 39 | 40 | // collect body on data events 41 | var body = ''; 42 | req.addListener('data', function( chunk ){ 43 | body += chunk; 44 | } ); 45 | 46 | // ready for processing when body fully collected 47 | req.addListener('end', function(){ 48 | //sys.puts( utils.hex(body) ); 49 | try { 50 | var requestPacket = amf.packet(body); 51 | if( requestPacket.version !== amf.AMF0 && requestPacket.version !== amf.AMF3 ){ 52 | throw new Error('Bad AMF request packet, sorry'); 53 | } 54 | //sys.puts( sys.inspect(requestPacket) ); 55 | 56 | // prepare response packet with the same AMF version 57 | var responsePacket = amf.packet(); 58 | responsePacket.version = requestPacket.version; 59 | 60 | // process all messages as function calls 61 | var requestMessage, responseMessage, func, args, uri; 62 | // queue up all web service calls to be executed asynchronously 63 | var queue = []; 64 | for( var m in requestPacket.messages ){ 65 | requestMessage = requestPacket.messages[m]; 66 | try { 67 | // get function to call and arguments to pass from request message 68 | func = methods[ requestMessage.requestURI ]; 69 | args = requestMessage.value; 70 | uri = requestMessage.responseURI; 71 | if( typeof func !== 'function' ){ 72 | throw new Error('No such method "'+requestMessage.requestURI+'"'); 73 | } 74 | if( typeof args !== 'object' && typeof args.push !== 'function' ){ 75 | throw new Error('Arguments to "'+requestMessage.requestURI+'" must be sent as an array'); 76 | } 77 | queue.push( [ uri, func, args ] ); 78 | } 79 | // errors respond with an onStatus method request to the client - no responseURI required 80 | catch( Er ){ 81 | console.warn('Error on request message "'+requestMessage.requestURI+'": ' + Er.message); 82 | responseMessage = amf.message( Er.message, uri+'/onStatus', '' ); 83 | responsePacket.messages.push( responseMessage ); 84 | } 85 | } 86 | // execute all web services, responding only when all are complete 87 | function shiftQueue(){ 88 | var q = queue.shift(); 89 | var uri = q[0], func = q[1], args = q[2]; 90 | function callback( value, func ){ 91 | func = func || 'onResult'; 92 | // ensure callback isn't executed twice 93 | if( uri !== null ){ 94 | clearTimeout(t); 95 | responseMessage = amf.message( value, uri+'/'+func, '' ); 96 | responsePacket.messages.push( responseMessage ); 97 | url = null; 98 | if( ++processed === qlen ){ 99 | respond(); 100 | } 101 | } 102 | } 103 | function onTimeout(){ 104 | callback('method timeout', 'onStatus'); 105 | } 106 | // hand off to method - any return value other than undefined assume to be a syncronous method 107 | try { 108 | args.unshift( callback ); 109 | var t = setTimeout( onTimeout, timeout || 10000 ); 110 | var value = func.apply( null, args ); 111 | if( value !== undefined ){ 112 | callback( value ); 113 | } 114 | } 115 | catch( Er ){ 116 | console.warn('Error on AMF method: ' + Er.message); 117 | callback( Er.message, 'onStatus' ); 118 | } 119 | } 120 | // final response when all messages have been processed 121 | function respond(){ 122 | // flush HTTP response 123 | var bin = responsePacket.serialize(); 124 | //sys.puts( utils.hex(bin) ); 125 | //sys.puts( sys.inspect(responsePacket) ); 126 | res.writeHead( 200, { 127 | 'Content-Type': 'application/x-amf', 128 | 'Content-Length': bin.length 129 | } ); 130 | res.write( bin, "binary" ); 131 | res.end(); 132 | } 133 | // process queue without a recursive call stack 134 | var qlen = queue.length, processed = 0; 135 | for( var i = 0; i < qlen; i++ ){ 136 | setTimeout( shiftQueue, 0 ); 137 | } 138 | if( ! qlen ){ 139 | throw new Error('no messages to process'); 140 | } 141 | } 142 | catch( e ){ 143 | console.warn( 'Error: ' + e.message ); 144 | res.writeHead( 500, {'Content-Type': 'text/plain'} ); 145 | res.write( 'Error on AMF packet:\n' + e.message ); 146 | res.end(); 147 | } 148 | } ); 149 | } 150 | 151 | } 152 | 153 | 154 | -------------------------------------------------------------------------------- /node-amf/message.js: -------------------------------------------------------------------------------- 1 | 2 | /** dependencies */ 3 | var amf = require('./amf'); 4 | 5 | 6 | 7 | /** export constructor */ 8 | exports.AMFMessage = AMFMessage; 9 | 10 | 11 | 12 | // ---------------------------------- 13 | 14 | 15 | function AMFMessage ( value, requestURI, responseURI ){ 16 | this.value = value; 17 | this.requestURI = requestURI || ''; 18 | this.responseURI = responseURI || ''; 19 | } 20 | 21 | 22 | /** */ 23 | AMFMessage.prototype.toString = function(){ 24 | return '[Object AMFMessage]'; 25 | } 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /node-amf/packet.js: -------------------------------------------------------------------------------- 1 | 2 | /** dependencies */ 3 | var amf = require('./amf'); 4 | 5 | 6 | 7 | /** export constructor */ 8 | exports.AMFPacket = AMFPacket; 9 | 10 | 11 | 12 | // ----------------------------------- 13 | 14 | 15 | /** Constructor */ 16 | function AMFPacket( v ){ 17 | if( v == null ){ 18 | v = amf.AMF3; 19 | } 20 | this.version = v; 21 | this.headers = {}; 22 | this.nheaders = 0; 23 | this.messages = []; 24 | } 25 | 26 | 27 | /** */ 28 | AMFPacket.prototype.toString = function(){ 29 | return '[Object AMFPacket]'; 30 | } 31 | 32 | 33 | 34 | /** */ 35 | AMFPacket.prototype.addHeader = function( header, value ){ 36 | if( typeof header === 'string' ){ 37 | header = amf.header( header, value ); 38 | } 39 | if( this.headers[header.name] ){ 40 | // @todo multiple headers of same name? currently replaces 41 | } 42 | else { 43 | this.nheaders++; 44 | } 45 | this.headers[header.name] = header 46 | return header; 47 | }; 48 | 49 | 50 | 51 | /** */ 52 | AMFPacket.prototype.addMessage = function( value, requestURI, responseURI ){ 53 | var message = amf.message( value, requestURI, responseURI ); 54 | this.messages.push( message ); 55 | return message; 56 | }; 57 | 58 | 59 | 60 | /** */ 61 | AMFPacket.prototype.serialize = function(){ 62 | var s = amf.serializer( this.version ); 63 | // write version flag 64 | s.writeU16( this.version ); 65 | // write packet headers 66 | s.writeU16( this.nheaders ); 67 | for( var h in this.headers ){ 68 | s.writeHeader( this.headers[h] ); 69 | } 70 | // write packet messages 71 | s.writeU16( this.messages.length ); 72 | for( var i = 0; i < this.messages.length; i++ ){ 73 | s.writeMessage( this.messages[i], this.version ); 74 | } 75 | // return serialized string 76 | return s.toString(); 77 | }; 78 | 79 | 80 | 81 | /** 82 | * @static 83 | */ 84 | AMFPacket.deserialize = function( src ){ 85 | var Packet = new AMFPacket(); 86 | var d = amf.deserializer( src ); 87 | var v = d.readU16(); 88 | if( v !== amf.AMF0 && v !== amf.AMF3 ){ 89 | throw new Error('Invalid AMF packet'); 90 | } 91 | // read headers 92 | var nheaders = d.readU16(); 93 | while( 0 < nheaders-- ){ 94 | Packet.addHeader( d.readHeader() ); 95 | } 96 | // read messages 97 | var nmessages = d.readU16(); 98 | while( 0 < nmessages-- ){ 99 | Packet.messages.push( d.readMessage() ); 100 | } 101 | return Packet; 102 | }; 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /node-amf/serialize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AMF serializer 3 | */ 4 | 5 | 6 | /** dependencies */ 7 | var amf = require('./amf'); 8 | var utils = require('./utils'); 9 | var utf8 = require('./utf8'); 10 | var bin = require('./bin'); 11 | 12 | 13 | exports.AMFSerializer = AMFSerializer; 14 | 15 | 16 | // ---------------------------------- 17 | 18 | 19 | function AMFSerializer( v ){ 20 | this.version = v; 21 | this.s = ''; 22 | this.resetRefs(); 23 | this.beParser = bin.parser( true, true ); // <- big endian binary packer 24 | this.leParser = bin.parser( false, true ); // <- little endian binary packer 25 | } 26 | 27 | 28 | /** */ 29 | AMFSerializer.prototype.toString = function(){ 30 | return this.s; 31 | } 32 | 33 | 34 | 35 | /** */ 36 | AMFSerializer.prototype.resetRefs = function(){ 37 | this.refObj = []; // object references 38 | this.refStr = []; // string references 39 | this.refTra = []; // trait references 40 | } 41 | 42 | 43 | /** */ 44 | AMFSerializer.prototype.writeHeader = function ( Header ){ 45 | this.resetRefs(); 46 | // header must be AMF0 47 | var v = this.version; 48 | this.version = amf.AMF0; 49 | this.writeUTF8( Header.name ); 50 | this.writeU8( Header.mustunderstand ? 1 : 0 ); 51 | // header of unknown length until serialized (U32)-1 52 | this.s += '\xFF\xFF\xFF\xFF'; 53 | var s = this.writeValue( Header.value ); 54 | // reinstate version if it wasn't AMF0 55 | this.version = v; 56 | return s; 57 | } 58 | 59 | 60 | 61 | /** */ 62 | AMFSerializer.prototype.writeMessage = function ( Message, v ){ 63 | this.resetRefs(); 64 | // message wrappers must be AMF0 65 | var vv = this.version; 66 | this.version = amf.AMF0; 67 | this.writeUTF8( Message.requestURI ); 68 | this.writeUTF8( Message.responseURI ); 69 | // message of unknown length until serialized (U32)-1 70 | this.s += '\xFF\xFF\xFF\xFF'; 71 | // switch version if AMF3 72 | if( v === amf.AMF3 ){ 73 | this.writeU8( amf.AMF0_AMV_PLUS ); 74 | } 75 | this.version = v; 76 | this.writeValue( Message.value ); 77 | this.version = vv; 78 | return this.s; 79 | } 80 | 81 | 82 | 83 | 84 | /** 85 | * Write any JavaScript value, automatically chooses which data type to use 86 | * @param mixed 87 | * @return string 88 | */ 89 | AMFSerializer.prototype.writeValue = function( value ){ 90 | // undefined 91 | if( value === undefined ){ 92 | return this.writeUndefined(); 93 | } 94 | // null 95 | if( value === null ){ 96 | return this.writeNull(); 97 | } 98 | // strings 99 | if( 'string' === typeof value ){ 100 | return this.writeUTF8( value, true ); 101 | } 102 | // numbers 103 | if( 'number' === typeof value ){ 104 | return this.writeNumber( value, true ); 105 | } 106 | // booleans 107 | if( 'boolean' === typeof value ){ 108 | return this.writeBool( value ); 109 | } 110 | // arrays 111 | if( 'function' === typeof value.push ){ 112 | return this.writeArray( value ); 113 | } 114 | // special object types 115 | if( value instanceof Date ){ 116 | return this.writeDate( value ); 117 | } 118 | // else write vanilla Object 119 | return this.writeObject( value ); 120 | } 121 | 122 | 123 | 124 | /** */ 125 | AMFSerializer.prototype.writeUndefined = function(){ 126 | var marker = this.version === amf.AMF3 ? amf.AMF3_UNDEFINED : amf.AMF0_UNDEFINED; 127 | return this.writeU8( marker ); 128 | } 129 | 130 | 131 | 132 | /** */ 133 | AMFSerializer.prototype.writeNull = function(){ 134 | var marker = this.version === amf.AMF3 ? amf.AMF3_NULL : amf.AMF0_NULL; 135 | return this.writeU8( marker ); 136 | } 137 | 138 | 139 | 140 | /** */ 141 | AMFSerializer.prototype.writeBool = function( value ){ 142 | // AMF3 143 | if( this.version === amf.AMF3 ){ 144 | var marker = value ? amf.AMF3_TRUE : amf.AMF3_FALSE; 145 | return this.writeU8( marker ); 146 | } 147 | // AMF0 148 | else { 149 | this.writeU8( amf.AMF0_BOOLEAN ); 150 | return this.writeU8( value ? 1 : 0 ); 151 | } 152 | } 153 | 154 | 155 | /** */ 156 | AMFSerializer.prototype.writeUTF8 = function( value, writeMarker ){ 157 | if( typeof value !== 'string' ){ 158 | value = ''; 159 | } 160 | var bin = utf8.expand( value ); 161 | var len = bin.length; 162 | // AMF3 163 | if( this.version === amf.AMF3 ){ 164 | if( writeMarker ){ 165 | this.writeU8( amf.AMF3_STRING ); 166 | } 167 | var flag = ( len<<1 ) | 1; 168 | this.writeU29( flag ); 169 | } 170 | // AMF0 171 | else { 172 | if( writeMarker ){ 173 | this.writeU8( amf.AMF0_STRING ); 174 | } 175 | this.writeU16( len ); 176 | } 177 | // append string as-is 178 | return this.s += bin; 179 | } 180 | 181 | 182 | 183 | /** */ 184 | AMFSerializer.prototype.writeArray = function( value ){ 185 | var len = value.length; 186 | // AMF3 187 | if( this.version === amf.AMF3 ){ 188 | this.writeU8( amf.AMF3_ARRAY ); 189 | // support object references 190 | var n = this.refObj.indexOf( value ); 191 | if( n !== -1 ){ 192 | return this.writeU29( n << 1 ); 193 | } 194 | // else index object reference 195 | this.refObj.push( value ); 196 | // flag with XXXXXXX1 indicating length of dense portion with instance 197 | var flag = ( len << 1 ) | 1; 198 | this.writeU29( flag ); 199 | // no assoc values in JavaScript - end with empty string 200 | this.writeUTF8(''); 201 | } 202 | // AMF0 strict array 203 | else { 204 | this.writeU8( amf.AMF0_STRICT_ARRAY ); 205 | this.writeU32( len ); 206 | } 207 | // write members (the dense portion - all we need in JS) 208 | for( var i = 0; i < len; i++ ){ 209 | this.writeValue( value[i] ); 210 | } 211 | return this.s; 212 | } 213 | 214 | 215 | 216 | /** */ 217 | AMFSerializer.prototype.writeObject = function( value ){ 218 | if( this.version !== amf.AMF3 ){ 219 | throw new Error("This library doesn't support AMF0 objects, use AMF3"); 220 | } 221 | this.writeU8( amf.AMF3_OBJECT ); 222 | // support object references 223 | var n = this.refObj.indexOf( value ); 224 | if( n !== -1 ){ 225 | return this.writeU29( n << 1 ); 226 | } 227 | // else index object reference 228 | this.refObj.push( value ); 229 | // flag with instance, no traits, no externalizable 230 | this.writeU29( 11 ); 231 | // Override object type if present 232 | if(value['type'] != null) { 233 | this.writeUTF8(value['type']); 234 | } else { 235 | this.writeUTF8('Object'); 236 | } 237 | // write serializable properties 238 | for( var s in value ){ 239 | if( typeof value[s] !== 'function' ){ 240 | this.writeUTF8(s); 241 | this.writeValue( value[s] ); 242 | } 243 | } 244 | // terminate dynamic props with empty string 245 | return this.writeUTF8(''); 246 | } 247 | 248 | 249 | 250 | /** */ 251 | AMFSerializer.prototype.writeDate = function( d ){ 252 | if( this.version !== amf.AMF3 ){ 253 | throw new Error("This library doesn't support AMF0 objects, use AMF3"); 254 | } 255 | this.writeU8( amf.AMF3_DATE ); 256 | this.writeU29( 1 ); 257 | return this.writeDouble( d.getTime() ); 258 | } 259 | 260 | 261 | 262 | 263 | 264 | /** */ 265 | AMFSerializer.prototype.writeNumber = function( value, writeMarker ){ 266 | // serialize as integers if possible 267 | var n = parseInt( value ); 268 | if( n === value && n >= 0 && n < 0x20000000 ){ 269 | return this.writeU29( value, writeMarker ); 270 | } 271 | return this.writeDouble( value, writeMarker ); 272 | } 273 | 274 | 275 | 276 | 277 | /** */ 278 | AMFSerializer.prototype.writeDouble = function( value, writeMarker ){ 279 | if( writeMarker ){ 280 | var marker = this.version === amf.AMF3 ? amf.AMF3_DOUBLE : amf.AMF0_NUMBER; 281 | this.writeU8( marker ); 282 | } 283 | // support for NaN as double "00 00 00 00 00 00 F8 7F" 284 | if( isNaN(value) ){ 285 | this.s += '\0\0\0\0\0\0\xF8\x7F'; 286 | } 287 | else { 288 | this.s += this.beParser.fromDouble( value ); 289 | } 290 | return this.s; 291 | } 292 | 293 | 294 | 295 | /** */ 296 | AMFSerializer.prototype.writeU8 = function( n ){ 297 | return this.s += this.beParser.fromByte(n); 298 | } 299 | 300 | 301 | 302 | /** */ 303 | AMFSerializer.prototype.writeU16 = function( n ){ 304 | return this.s += this.beParser.fromWord(n); 305 | } 306 | 307 | 308 | 309 | /** */ 310 | AMFSerializer.prototype.writeU32 = function( n ){ 311 | return this.s += this.beParser.fromDWord(n); 312 | } 313 | 314 | 315 | 316 | /** AMF3 only */ 317 | AMFSerializer.prototype.writeU29 = function( n, writeMarker ){ 318 | // unsigned range: 0 -> pow(2,29)-1; 0 -> 0x1FFFFFFF 319 | // signed range: -pow(2,28) -> pow(2,28)-1; -0x10000000 -> 0x0FFFFFFF 320 | if( n < 0 ){ 321 | throw new Error('U29 range error, '+n+' < 0'); 322 | //n += 0x20000000; 323 | } 324 | var a, b, c, d; 325 | if( n < 0x00000080 ){ 326 | // 0AAAAAAA 327 | a = n; 328 | } 329 | else if( n < 0x00004000 ){ 330 | // 0x80-FF 0x00-7F 331 | // 00AAAAAA ABBBBBBB -> 1AAAAAAA 0BBBBBBB 332 | b = n & 0x7F; 333 | a = 0x80 | ( n>>7 & 0x7F ); 334 | } 335 | else if( n < 0x00200000 ){ 336 | // 0x80-FF 0x80-FF 0x00-7F 337 | // 000AAAAA AABBBBBB BCCCCCCC -> 1AAAAAAA 1BBBBBBB 0CCCCCCC 338 | c = n & 0x7F; 339 | b = 0x80 | ( (n>>=7) & 0x7F ); 340 | a = 0x80 | ( (n>>=7) & 0x7F ); 341 | } 342 | else if( n < 0x20000000 ){ 343 | // 0x80-FF 0x80-FF 0x80-FF 0x00-FF 344 | // 000AAAAA AABBBBBB BCCCCCCC DDDDDDDD -> 1AAAAAAA 1BBBBBBB 1CCCCCCC DDDDDDDD 345 | d = n & 0xFF; 346 | c = 0x80 | ( (n>>=8) & 0x7F ); 347 | b = 0x80 | ( (n>>=7) & 0x7F ); 348 | a = 0x80 | ( (n>>=7) & 0x7F ); 349 | } 350 | else { 351 | throw new Error('U29 range error, '+n+' > 0x1FFFFFFF'); 352 | } 353 | if( writeMarker ){ 354 | this.writeU8( amf.AMF3_INTEGER ); 355 | } 356 | this.writeU8( a ); 357 | if( b != null ){ 358 | this.writeU8( b ); 359 | if( c != null ){ 360 | this.writeU8( c ); 361 | if( d != null ){ 362 | this.writeU8( d ); 363 | } 364 | } 365 | } 366 | return this.s; 367 | } 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | -------------------------------------------------------------------------------- /node-amf/traits.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /** export constructor */ 5 | exports.AMFTraits = AMFTraits; 6 | 7 | 8 | 9 | // ---------------------------------- 10 | 11 | 12 | function AMFTraits(){ 13 | this.clss = 'Object'; // object class 14 | this.dyn = false; // whether object is dynamic (i.e. non strict about members) 15 | this.props = []; // class members 16 | } 17 | 18 | 19 | /** */ 20 | AMFTraits.prototype.toString = function(){ 21 | return '[Object AMFTraits]'; 22 | } 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /node-amf/utf8.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unicode string operations 3 | */ 4 | 5 | 6 | 7 | /** 8 | * Decode native AS string to string of single byte characters. 9 | * @link http://en.wikipedia.org/wiki/UTF-8 10 | * @param String native UTF-8 string 11 | * @return String decoded string 12 | */ 13 | exports.expand = function( u ) { 14 | var s = ''; 15 | for( var i = 0; i < u.length; i++ ){ 16 | var n = u.charCodeAt(i); 17 | s += exports.chr( n ); 18 | } 19 | return s; 20 | } 21 | 22 | 23 | 24 | /** 25 | * Convert UTF-8 codepoint to multi-byte sequence 26 | * @param integer utf-8 code point 27 | * @return string sequence of [one to four] single byte characters 28 | */ 29 | exports.chr = function chr( n ) { 30 | // 7 bit ASCII character - transparent to Unicode 31 | if( n < 0x80 ){ 32 | return String.fromCharCode( n ); 33 | } 34 | // compile 1-4 byte string depending on size of code point. 35 | // this could be more compact but shows the algorithms nicely ;) 36 | var w = null; 37 | var x = null; 38 | var y = null; 39 | var z = null; 40 | // Double byte sequence 41 | // 00000yyy yyzzzzzz ==> 110yyyyy 10zzzzzz 42 | if( n < 0x800 ){ 43 | z = n & 63; // get z bits 44 | y = n >> 6; // get y bits 45 | y |= 192; // "110yyyyy" 46 | z |= 128; // "10zzzzzz" 47 | } 48 | // Triple byte sequence 49 | // xxxxyyyy yyzzzzzz ==> 1110xxxx 10yyyyyy 10zzzzzz 50 | else if( n < 0x10000 ){ 51 | z = n & 63; // get z bits 52 | y = ( n >>= 6 ) & 63; // get y bits 53 | x = ( n >>= 6 ) & 15; // get x bits 54 | z |= 128; // prefix "10zzzzzz" 55 | y |= 128; // prefix "10yyyyyy" 56 | x |= 224; // prefix "1110xxxx" 57 | } 58 | // Four byte sequence 59 | // 000wwwxx xxxxyyyy yyzzzzzz ==> 11110www 10xxxxxx 10yyyyyy 10zzzzzz 60 | else if( n <= 0x10FFFF ){ 61 | z = n & 63; // get z bits 62 | y = ( n >>= 6 ) & 63; // get y bits 63 | x = ( n >>= 6 ) & 63; // get x bits 64 | w = ( n >>= 6 ) & 7; // get w bits 65 | z |= 128; // prefix "10zzzzzz" 66 | y |= 128; // prefix "10yyyyyy" 67 | x |= 128; // prefix "10xxxxxx" 68 | w |= 240; // prefix "11110www" 69 | } 70 | else { 71 | // UTF allows up to 1114111 72 | trace('UTF8 code points cannot be greater than 0x10FFFF [0x'+n.toString(16)+']'); 73 | return '?'; 74 | } 75 | // compile multi byte sequence 76 | var s = ''; 77 | ( w == null ) || ( s += String.fromCharCode(w) ); 78 | ( x == null ) || ( s += String.fromCharCode(x) ); 79 | ( y == null ) || ( s += String.fromCharCode(y) ); 80 | ( z == null ) || ( s += String.fromCharCode(z) ); 81 | return s; 82 | } 83 | 84 | 85 | 86 | 87 | /** 88 | * Collapse a multibyte sequence to native UTF-8 89 | * @param String 90 | * @return String 91 | */ 92 | exports.collapse = function( s ){ 93 | 94 | // inner peeking function for skipping over multi-byte sequence 95 | function peek(){ 96 | var n = s.charCodeAt( ++i ); 97 | if( isNaN(n) ){ 98 | throw new Error("Unexpected end of string, offset "+i); 99 | } 100 | return n; 101 | } 102 | 103 | // make a code point from a leading byte and aribitrary number of following bytes 104 | function make( t, num ) { 105 | for( var j = 0; j < num; j++ ){ 106 | // get trailing 10xxxxxx byte 107 | var m = peek(); 108 | if( ( m & 192 ) !== 128 ){ 109 | throw new Error('Invalid byte 0x'+m.toString(16).toUpperCase()+' "'+String.fromCharCode(m)+'" at offset '+i); 110 | } 111 | t <<= 6; 112 | t |= ( m & 63 ); 113 | } 114 | return String.fromCharCode(t); 115 | } 116 | 117 | // start iteration, skipping multibyte sequences wwhen leading byte found 118 | var u = ''; 119 | for( var i = 0; i < s.length; i++ ){ 120 | var n = s.charCodeAt(i); 121 | // 7-bit ASCII is transparent to Unicode 122 | if( ( n & 128 ) === 0 ){ 123 | u += String.fromCharCode( n ); 124 | continue; 125 | } 126 | // check for leading byte in UTF8 sequence, most likely first for speed 127 | if( ( n & 224 ) === 192 ){ 128 | // is leading char in 2 byte sequence "110xxxxx" 129 | u += make( n & 31, 1 ); 130 | } 131 | else if( ( n & 192 ) === 128 ){ 132 | // is a solitary 10xxxxxx character - technically invalid, but common! 133 | // - todo - map Windows-1252 special cases in range 128-159 134 | u += String.fromCharCode( n ); 135 | } 136 | else if( ( n & 240 ) === 224 ){ 137 | // is leading char in 3 byte sequence "1110xxxx" 138 | u += make( n & 15, 2 ); 139 | } 140 | else if( ( n & 248 ) === 240 ){ 141 | // is leading char in 4 byte sequence "11110xxx" 142 | u += make( n & 7, 3 ); 143 | } 144 | else { 145 | throw new Error( 'Invalid character "'+String.fromCharCode(n)+'" at offset '+ i ); 146 | u += '?'; 147 | } 148 | } 149 | return u; 150 | } 151 | 152 | 153 | 154 | 155 | /** 156 | * Calculate real byte size of multibyte character string 157 | * @param String 158 | * @return Number 159 | */ 160 | exports.size = function( s ) { 161 | var b = 0; 162 | for( var i = 0; i < s.length; i++ ){ 163 | var n = s.charCodeAt(i); 164 | if( n < 0x80 ){ 165 | b += 1; 166 | } 167 | else if( n < 0x800 ){ 168 | b += 2; 169 | } 170 | else if( n < 0x10000 ){ 171 | b += 3; 172 | } 173 | else if( n <= 0x10FFFF ){ 174 | b += 4; 175 | } 176 | } 177 | return b; 178 | } 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /node-amf/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility functions for use with node-amf module 3 | */ 4 | 5 | 6 | //var expandUTF8 = require('./utf8').expand; 7 | 8 | 9 | 10 | 11 | /** */ 12 | exports.leftPad = function( s, n, c ){ 13 | while( s.length < n ){ 14 | s = c + s; 15 | } 16 | return s; 17 | } 18 | 19 | 20 | 21 | /** */ 22 | exports.reverseString = function( s ){ 23 | var r = '', i = 0, n = - s.length; 24 | while( i > n ){ 25 | r += s.substr(--i,1); 26 | } 27 | return r; 28 | } 29 | 30 | 31 | 32 | /** */ 33 | exports.toHex = function ( d, n ){ 34 | var h = d.toString(16).toUpperCase(); 35 | return exports.leftPad( h, 2, '0' ); 36 | } 37 | 38 | 39 | 40 | /** 41 | * Hex dump function. 42 | */ 43 | exports.hex = function( bin, cols ){ 44 | //bin = expandUTF8( bin ); 45 | cols || ( cols = 24 ); 46 | var s = '', line = []; 47 | var c, d, i = 0; 48 | while( bin ){ 49 | c = bin.charAt(0); 50 | d = bin.charCodeAt(0); 51 | bin = bin.substr(1); 52 | // print hex 53 | s += exports.toHex( d, 2 )+ ' '; 54 | // add printable to line 55 | if( d === 9 ){ 56 | line.push(' '); // <- tab 57 | } 58 | else if ( d < 32 || d > 126 ) { 59 | line.push('.'); // <- unprintable // well, non-ascii 60 | } 61 | else { 62 | line.push(c); // <- printable 63 | } 64 | // wrap at cols, and print plain text 65 | if( ++i === cols ){ 66 | s += ' '+line.join('') + '\n'; 67 | line = []; 68 | i = 0; 69 | } 70 | else if( i % 8 === 0 ){ 71 | s += ' '; 72 | } 73 | } 74 | // pick up remainder 75 | if( line.length ){ 76 | while( i++ < cols ){ 77 | s += ' '; 78 | } 79 | s += ' '+line.join('') + '\n'; 80 | } 81 | return s; 82 | } -------------------------------------------------------------------------------- /node-rtmp/README.md: -------------------------------------------------------------------------------- 1 | # node-rtmp 2 | 3 | ## About 4 | 5 | An attempt to partially implement [RTMP](http://en.wikipedia.org/wiki/Real_Time_Messaging_Protocol) for NodeJS in pure JavaScript. 6 | Only the messaging aspects of the protocol are of interest at this point. Streaming media is not currently a concern. 7 | 8 | ## Status 9 | 10 | This is only in early development. Don't try and use it for anything. 11 | I am currently struggling with Adobe's badly written specification. -------------------------------------------------------------------------------- /node-rtmp/RtmpChunk.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A chunk object represents a chunk of data that is part of a chunk stream. 3 | * Multiple chunks form complete messages, but chunks in the same stream may or may not belong to the same message 4 | */ 5 | 6 | var sys = require('sys'); 7 | 8 | var utils = require('../node-amf/utils'); 9 | var amf = require('../node-amf/amf'); 10 | var bin = require('../node-amf/bin'); 11 | 12 | 13 | 14 | // export single class 15 | exports.RtmpChunk = RtmpChunk; 16 | 17 | 18 | 19 | // utility to unpack numbers 20 | var beParser = bin.parser( true, true ); // big endian binary parser 21 | var leParser = bin.parser( false, true ); // little endian binary parser 22 | 23 | 24 | 25 | /** 26 | * Constructor 27 | */ 28 | function RtmpChunk( data ){ 29 | this.payload = ''; 30 | // 6.1.1 chunk basic header - defines stream id and chunk type 31 | var b1 = data.charCodeAt(0); 32 | this.chunkStreamId = b1 & 63; // <- (0-5) 6 bit mask 33 | // two byte header with 0: stream id between 64-319 @todo TEST ME 34 | if( this.chunkStreamId === 0 ){ 35 | this.chunkStreamId = data.charCodeAt(1) + 64; 36 | this.offset = 2; 37 | } 38 | // three byte header with 1: stream ids between 64-65599 @todo TEST ME 39 | else if( this.chunkStreamId === 1 ){ 40 | this.chunkStreamId = ( data.charCodeAt(2) * 256 ) + data.charCodeAt(1) + 64; 41 | this.offset = 3; 42 | } 43 | // 1 byte header - stream id between 2-63 44 | else { 45 | this.offset = 1; 46 | } 47 | // format always the first two bits 48 | this.format = b1 & 192; 49 | if( this.format === 0 ){ 50 | this.headerLen = 11; 51 | } 52 | else if( this.format === 1 ){ 53 | this.headerLen = 7; 54 | } 55 | else if( this.format === 2 ){ 56 | this.headerLen = 3; 57 | } 58 | else if( this.format === 3 ){ 59 | this.headerLen = 0; 60 | } 61 | else { 62 | throw new Error('Unsupported header type: '+sys.inspect(fmt)); 63 | } 64 | } 65 | 66 | 67 | 68 | /** 69 | * Inherit properties from an existing Chunk 70 | * @var Array existing chunks in our stream 71 | */ 72 | RtmpChunk.prototype.inheritPrevious = function( Previous ){ 73 | // format:0 should only be the first chunk in a stream 74 | if( this.format === 0 || ! Previous ){ 75 | return; 76 | } 77 | // always inherit message stream id 78 | this.messageStreamId = Previous.messageStreamId; 79 | // inherit timestamp 80 | if( this.format === 3 ){ 81 | this.timestamp = Previous.timestamp; 82 | } 83 | // inherit message length and message type id 84 | if( this.format === 2 || this.format === 3 ){ 85 | this.messageLen = Previous.messageLen; 86 | this.messageType = Previous.messageType; 87 | } 88 | // inherit payload and save memory in previous chunk 89 | this.payload += Previous.payload; 90 | Previous.payload = null; 91 | } 92 | 93 | 94 | 95 | /** 96 | * 97 | */ 98 | RtmpChunk.prototype.parse = function( data ){ 99 | // Parse header according to our format type 100 | this.header = data.substr( this.offset, this.headerLen ); 101 | // timestamp delta - 3 bytes (always a timestamp, if there's a header at all) 102 | if( this.format !== 3 ){ 103 | this.timestamp = beParser.decodeInt( this.header.slice(0,3), 24, false ); 104 | } 105 | else if( this.timestamp == null ){ 106 | throw new Error('Chunk has not inherited timestamp from previous chunk in stream'); 107 | } 108 | // message length - 3 bytes / 24 bit unsigned 109 | if( this.format === 0 || this.format === 1 ){ 110 | this.messageLen = beParser.decodeInt( this.header.slice(3,6), 24, false ); 111 | } 112 | else if( this.messageLen == null ){ 113 | throw new Error('Chunk has not inherited message length from previous chunk in stream'); 114 | } 115 | // message type id - 1 byte / 8 bit unsigned 116 | if( this.format === 0 || this.format === 1 ){ 117 | this.messageType = beParser.toByte( this.header.slice(6,7) ); 118 | } 119 | else if( this.messageType == null ){ 120 | throw new Error('Chunk has not inherited message type from previous chunk in stream'); 121 | } 122 | // message stream id - unsigned long - 4 bytes (little Endian) 123 | if( this.format === 0 ){ 124 | this.messageStreamId = leParser.toDWord( this.header.slice(7,11) ); 125 | } 126 | else if( this.messageStreamId == null ){ 127 | throw new Error('Chunk has not inherited message stream id from previous chunk in stream'); 128 | } 129 | // 6.1.3 extended timestamp 130 | if( this.timestamp === 0x00ffffff ){ 131 | throw new Error('@todo extended timestamp'); 132 | } 133 | // Snip off the payload, @todo 128 bytes at a time 134 | this.payload += data.slice( this.offset + this.headerLen ); 135 | }; 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /node-rtmp/RtmpConnection.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var sys = require('sys'); 4 | 5 | var utils = require('../node-amf/utils'); 6 | 7 | var RtmpHandshake = require('./RtmpHandshake').RtmpHandshake; 8 | var RtmpMessage = require('./RtmpMessage').RtmpMessage; 9 | var RtmpChunk = require('./RtmpChunk').RtmpChunk; 10 | 11 | // export single class 12 | exports.RtmpConnection = RtmpConnection; 13 | 14 | 15 | 16 | /** 17 | * RTMP constructor function 18 | */ 19 | function RtmpConnection( socket ){ 20 | socket.setEncoding("binary"); 21 | this.socket = socket; 22 | this.handShake = null; 23 | this.messageStreams = []; 24 | this.chunkStreams = []; 25 | // temporary buffer for incomplete packets 26 | this.buffer = ''; 27 | // listen for data 28 | var Conn = this; 29 | socket.addListener( 'data', function(data){ 30 | Conn.onSocketData( data ); 31 | } ); 32 | socket.addListener( 'connect', function(){ 33 | sys.puts('socket:connect'); 34 | } ); 35 | socket.addListener( 'end', function(){ 36 | sys.puts('socket.end'); 37 | socket.end(); 38 | } ); 39 | socket.addListener('timeout', function(){ 40 | sys.puts('socket.timeout'); 41 | } ); 42 | socket.addListener('drain', function(){ 43 | sys.puts('socket.drain'); 44 | } ); 45 | socket.addListener('close', function( had_error ){ 46 | sys.puts('socket.close, had_error='+had_error); 47 | } ); 48 | socket.addListener('error', function( Er ){ 49 | sys.puts('socket.error: '+ Er.message ); // stack,errno,syscall 50 | } ); 51 | } 52 | 53 | 54 | 55 | 56 | /** 57 | * Common socket writing function 58 | */ 59 | RtmpConnection.prototype.write = function( data ){ 60 | return this.socket.write( data, 'binary' ); 61 | } 62 | 63 | 64 | 65 | 66 | /** 67 | * Data listener 68 | */ 69 | RtmpConnection.prototype.onSocketData = function( data ){ 70 | try { 71 | var response; 72 | sys.puts('socket.data [length:'+data.length+']'); 73 | // complete handshake if not already 74 | if( ! this.handShake ){ 75 | // wait for full 1537 bytes 76 | this.buffer += data; 77 | if( this.buffer.length < 1537 ){ 78 | return; 79 | } 80 | sys.puts('# handshake 1'); 81 | this.handShake = new RtmpHandshake(); 82 | //sys.puts( sys.inspect(this.handShake) ); 83 | response = this.handShake.initialize( this.buffer ); 84 | this.buffer = ''; 85 | return this.write( response ); 86 | } 87 | if( ! this.handShake.acknowledged ){ 88 | // wait for at least 1536 bytes 89 | this.buffer += data; 90 | if( this.buffer.length < 1536 ){ 91 | return; 92 | } 93 | sys.puts('# handshake 2'); 94 | response = this.handShake.acknowledge( this.buffer ); 95 | //sys.puts( sys.inspect(this.handShake) ); 96 | this.write( response ); 97 | data = this.buffer.slice(1536); 98 | this.buffer = ''; 99 | } 100 | 101 | sys.puts('# processing chunk, have '+data.length+' bytes'); 102 | //sys.puts( utils.hex(data,16) ); 103 | // process a chunk 104 | var Chunk = new RtmpChunk( data ); 105 | // add to stream and inherit any known properties 106 | var Previous = this.chunkStreams[Chunk.chunkStreamId]; 107 | if( Previous ){ 108 | Chunk.inheritPrevious( Previous ); 109 | } 110 | else { 111 | this.chunkStreams[Chunk.chunkStreamId] = Previous; 112 | } 113 | Chunk.parse( data ); 114 | if( Chunk.payload.length >= Chunk.messageLen ){ 115 | sys.puts('# processing message, have '+Chunk.payload.length+'/'+Chunk.messageLen+' bytes'); 116 | var Msg = new RtmpMessage( Chunk.payload, Chunk.messageLen ); 117 | } 118 | } 119 | catch( Er ){ 120 | sys.puts( 'Error onSocketData: '+Er.message ); 121 | } 122 | } 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /node-rtmp/RtmpHandshake.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var sys = require('sys'); 4 | 5 | var utils = require('../node-amf/utils'); 6 | var bin = require('../node-amf/bin'); 7 | 8 | 9 | // export single class 10 | exports.RtmpHandshake = RtmpHandshake; 11 | 12 | 13 | 14 | // utility to unpack numbers 15 | var beParser = bin.parser( true, true ); // big endian binary parser 16 | //var leParser = bin.parser( false, true ); // little endian binary parser 17 | 18 | 19 | 20 | /** 21 | * Constructor 22 | * @todo what to do about client/server epoch? 23 | */ 24 | function RtmpHandshake( data ){ 25 | this.acknowledged = false; 26 | this.serverRandom = randomString(1528); 27 | // take a benchmark and set a zero epoch 28 | // this means all timestamps are milliseconds since this moment 29 | this.serverBench = ( new Date ).getTime(); 30 | this.serverEpoch = 0; 31 | this.serverEpochStr = '\0\0\0\0'; 32 | } 33 | 34 | 35 | /** 36 | * @return Number 37 | */ 38 | RtmpHandshake.prototype.timestamp = function( t ){ 39 | if( t == null ){ 40 | t = (new Date).getTime(); 41 | } 42 | var delta = t - this.serverBench; 43 | return delta; 44 | } 45 | 46 | 47 | 48 | 49 | /** 50 | * Receive C0, C1 and send S0, S1 51 | * @param string data received on socket 52 | * @return string data to write in response 53 | */ 54 | RtmpHandshake.prototype.initialize = function( data ){ 55 | if( data.length !== 1537 ){ 56 | throw new Error('Expecting 1537 octets, got ' + data.length); 57 | } 58 | // 5.2 C0 and S0 59 | if( 3 !== data.charCodeAt(0)){ 60 | // @todo should we abandon the connectipon or reply with \3? spec contradicts itself 61 | throw new Error('Unexpected version, Only version 3 is supported'); 62 | return '\3'; 63 | } 64 | // 5.3 - C1 and S1 - 65 | // recieve C1 66 | if( '\0\0\0\0' !== data.slice(5,9) ){ 67 | throw new Error('Handshake error: zeroed string expected'); 68 | } 69 | this.clientEpochStr = data.slice(1,5); 70 | this.clientEpoch = beParser.toInt( this.clientEpochStr ); 71 | this.clientRandom = data.slice(9); 72 | // @todo whould we mirror the client's epoch?? 73 | //this.serverEpochStr = this.clientEpochStr; 74 | //this.serverEpoch = this.clientEpoch; 75 | // return S0 + S1 packet to respond with 76 | return '\3' + this.serverEpochStr + '\0\0\0\0' + this.serverRandom; 77 | } 78 | 79 | 80 | 81 | 82 | /** Receive C1 and send S1 */ 83 | RtmpHandshake.prototype.acknowledge = function( data ){ 84 | // recieve C2 85 | if( this.serverRandom !== data.slice( 8, 1536 ) ){ 86 | throw new Error('Handshake error: server random echo does not match'); 87 | } 88 | if( this.serverEpochStr !== data.slice(0,4) ){ 89 | throw new Error('Handshake error: server epoch echo does not match'); 90 | } 91 | this.clientBench = beParser.toInt( data.slice(4,8) ); // always seems to be zero? 92 | // return S2 packet to respond with 93 | this.acknowledged = true; 94 | // @todo unsure about this timestamp as it will be equal to the epoch 95 | var time2 = this.timestamp( this.serverBench ); 96 | return this.clientEpochStr + beParser.fromDWord(time2) + this.clientRandom; 97 | } 98 | 99 | 100 | 101 | 102 | 103 | // utility 104 | function randomString( len ){ 105 | var s = ''; 106 | for( var i = 0; i < len; i++ ){ 107 | s += String.fromCharCode( Math.round( 255 * Math.random() ) ); 108 | } 109 | return s; 110 | } 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /node-rtmp/RtmpMessage.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var sys = require('sys'); 4 | 5 | var bin = require('../node-amf/bin'); 6 | var utils = require('../node-amf/utils'); 7 | var amf = require('../node-amf/amf'); 8 | 9 | 10 | 11 | // export single class 12 | exports.RtmpMessage = RtmpMessage; 13 | 14 | 15 | 16 | // utility to unpack numbers 17 | var beParser = bin.parser( true, true ); 18 | var leParser = bin.parser( false, true ); 19 | 20 | 21 | 22 | /** 23 | * Constructor 24 | */ 25 | function RtmpMessage( data, messageLen ){ 26 | // 4.1. messages begin with a type - dictates payload structure 27 | this.type = beParser.toByte( data.slice(0,1) ); 28 | /* 29 | this.length = beParser.decodeInt( data.slice(1,4), 24, false ); 30 | this.timestamp = beParser.decodeInt( data.slice(4,8), 32, false ); 31 | this.streamId = leParser.decodeInt( data.slice(8,11), 24, false ); // <- BE/LE?? 32 | this.payload = data.slice(11); 33 | */ 34 | 35 | // type 2: message is an AMF encoded command from the client 36 | // @todo test message type from chunk to determine AMF0/AMF3 37 | if( this.type === 2 ){ 38 | // AMF payload is separated every 128 bytes by "0xC3" 39 | // todo optimize this, and check if needed for other types 40 | var message = '', i = 0; 41 | while( message.length < messageLen ){ 42 | message += data.substr( i, 128 ); 43 | i += 129; 44 | } 45 | var des = amf.deserializer( message.slice(1) ); 46 | var cmd = des.readUTF8( amf.AMF0 ); 47 | sys.puts('command = ' + cmd ); 48 | var unknown = des.shiftBytes( 9 ); // <- ? 49 | sys.puts( 'unknown = '+utils.hex(unknown) ); 50 | var obj = des.readValue( amf.AMF0 ); 51 | sys.puts( sys.inspect(obj) ); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amflib", 3 | "version": "1.0.1", 4 | "author": "Tim Whitlock (http://timwhitlock.info)", 5 | "description": "AMF library for NodeJS", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "main": "./node-amf/http-server", 10 | "repository": { 11 | "type": "git", 12 | "url": "http://github.com/timwhitlock/node-amf.git" 13 | }, 14 | "keywords": [ 15 | "flash", 16 | "AMF" 17 | ], 18 | "license": "MIT", 19 | "engines": { 20 | "node": ">=0.6" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is not a server. 3 | * This is simply a test script to ensure the node-amf libraries are working. 4 | */ 5 | 6 | // require system libraries 7 | var sys = require('sys'); 8 | 9 | // require node-amf module libraries from relative directory path 10 | var amf = require('./node-amf/amf'); 11 | var utils = require('./node-amf/utils'); 12 | 13 | 14 | // data types to test with human-readable description 15 | var tests = [ 16 | // strings 17 | ['empty string', ''], 18 | ['ascii string', 'Hello World'], 19 | ['unicode string', '£今\u4ECA"\u65E5日'], 20 | // numbers 21 | ['zero', 0 ], 22 | ['integer in 1 byte u29 range', 0x7F ], 23 | ['integer in 2 byte u29 range', 0x00003FFF ], 24 | ['integer in 3 byte u29 range', 0x001FFFFF ], 25 | ['integer in 4 byte u29 range', 0x1FFFFFFF ], 26 | ['large integer', 4294967296 ], 27 | ['large negative integer', -4294967296 ], 28 | ['small negative integer', -1 ], 29 | ['small floating point', 0.123456789 ], 30 | ['small negative floating point', -0.987654321 ], 31 | ['Number.MIN_VALUE', Number.MIN_VALUE ], 32 | ['Number.MAX_VALUE', Number.MAX_VALUE ], 33 | ['Number.NaN', Number.NaN], 34 | // other scalars 35 | ['Boolean false', false], 36 | ['Boolean true', true ], 37 | ['undefined', undefined ], 38 | ['null', null], 39 | // Arrays 40 | ['empty array', [] ], 41 | ['sparse array', [undefined,undefined,undefined,undefined,undefined,undefined] ], 42 | ['multi-dimensional array', [[[],[]],[],] ], 43 | // special objects 44 | ['date object (epoch)', new Date(0) ], 45 | ['date object (now)', new Date() ], 46 | // plain objects 47 | ['empty object', {} ], 48 | ['keyed object', { foo:'bar', 'foo bar':'baz' } ], 49 | ['refs object', { foo: _ = { a: 12 }, bar: _ } ] 50 | ]; 51 | 52 | 53 | 54 | // Test each type individually through serializer and then deserializer 55 | // note that this doesn't prove it works with Flash, just that it agrees with itself. 56 | sys.puts('Serializing and deserializing '+tests.length+' test values'); 57 | 58 | for( var t = 0, n = 0; t < tests.length; t++ ){ 59 | try { 60 | var descr = tests[t][0]; 61 | var value = tests[t][1]; 62 | var s = sys.inspect(value).replace(/\n/g,' '); 63 | sys.puts( ' > ' +descr+ ': ' + s); 64 | // serializing twice must not affect results 65 | amf.serializer().writeValue( value ); 66 | // serialize and show AMF packet 67 | var Ser = amf.serializer(); 68 | var bin = Ser.writeValue( value ); 69 | //sys.puts( utils.hex(bin,16) ); 70 | // deserialize and compare value 71 | var Des = amf.deserializer( bin ); 72 | var value2 = Des.readValue( amf.AMF3 ); 73 | var s2 = sys.inspect(value2).replace(/\n/g,' '); 74 | // simple value test if value is scalar 75 | if( typeof value2 !== typeof value ){ 76 | throw new Error('deserialized value of wrong type; ' + s2); 77 | } 78 | if( s !== s2 ){ 79 | throw new Error('deserialized value does not match; ' + s2); 80 | } 81 | sys.puts(' OK'); 82 | n++; 83 | } 84 | catch( Er ){ 85 | sys.puts('**FAIL** ' + Er.message ); 86 | } 87 | } 88 | sys.puts('Tests '+n+'/'+tests.length+' successful\n'); 89 | 90 | 91 | 92 | 93 | 94 | // Test a full AMF packet with headers and messages 95 | sys.puts('Testing a full AMF packet'); 96 | 97 | 98 | // initialize a new response packet 99 | try { 100 | var Packet = amf.packet(); 101 | 102 | // add a simple header with a name and string value 103 | Packet.addHeader( 'header 1', 'Example header 1' ); 104 | Packet.addHeader( 'header 2', 'Example header 2' ); 105 | 106 | // Dummy request/response URIs 107 | var requestURI = '/1/onResult'; 108 | var responseURI = '/1'; 109 | 110 | // add construct as a single AMF message and return serialized, binary string 111 | for( var t = 0, n = 0; t < tests.length; t++ ){ 112 | var struct = {}; 113 | var descr = tests[t][0]; 114 | var value = tests[t][1]; 115 | struct[descr] = value; 116 | Packet.addMessage( struct, requestURI, responseURI ); 117 | } 118 | 119 | // dump test packet in hex display 120 | var bin = Packet.serialize(); 121 | sys.puts(' > Packet serialization ok'); 122 | //sys.puts( utils.hex( bin ) ); 123 | } 124 | catch( Er ){ 125 | sys.puts('***FAIL*** error serializing packet: ' + Er.message ); 126 | return; 127 | } 128 | 129 | 130 | // now attempt to deserialize the packet and get the data back 131 | try { 132 | Packet = amf.packet( bin ); 133 | sys.puts(' > Packet deserialization ok'); 134 | //sys.puts( sys.inspect(Packet) ); 135 | } 136 | catch( Er ){ 137 | sys.puts('***FAIL*** error deserializing packet: ' + Er.message ); 138 | return; 139 | } 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | --------------------------------------------------------------------------------