Relocate code into src folder

This commit is contained in:
adelphes
2017-01-22 16:09:35 +00:00
parent 8337bc3e00
commit ec258a6ddb
16 changed files with 3 additions and 3 deletions

803
src/adbclient.js Normal file
View File

@@ -0,0 +1,803 @@
/*
ADBClient: class to manage connection and commands to adb (via the Dex plugin) running on the local machine.
*/
const _JDWP = require('./jdwp')._JDWP;
const $ = require('./jq-promise');
const WebSocket = require('./minwebsocket').WebSocketClient;
const { atob,btoa,D } = require('./util');
function ADBClient(deviceid) {
this.deviceid = deviceid;
this.status = 'notinit';
this.reset();
this.JDWP = new _JDWP();
}
ADBClient.prototype = {
reset : function() {
this.ws = null;
this.activepromise={};
this.authdone=false;
this.fd=-1;
this.disconnect_reject_reason=null;
},
_parse_device_list:function(data, extended) {
var lines = atob(data).trim().split(/\r\n?|\n/);
lines.sort();
var devicelist = [];
var i=0;
if (extended) {
for (var i=0; i < lines.length; i++) {
try {
var m = JSON.parse(lines[i]);
if (!m) continue;
m.num = i;
} catch(e) {continue;}
devicelist.push(m);
}
} else {
for (var i=0; i < lines.length; i++) {
var m = lines[i].match(/([^\t]+)\t([^\t]+)/);
if (!m) continue;
devicelist.push({
serial: m[1],
status: m[2],
num:i,
});
}
}
return devicelist;
},
track_devices_extended : function(o) {
var x = {o:o||{},deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('track_devices', 'wa', this.fd, 'host:track-devices-extended');
})
.then(function(data) {
return this.dexcmd('ra', this.fd);
})
.then(function(data) {
function nextdeviceinfo(data) {
this.dexcmd('ra', this.fd, null, {notimeout:true})
.then(nextdeviceinfo);
var devicelist = this._parse_device_list(data, true);
x.o.ondevices(devicelist, this);
}
nextdeviceinfo.call(this, data);
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
finish_track_devices : function() {
return this.dexcmd('dc', this.fd)
.then(function() {
return this.proxy_disconnect();
});
},
list_devices : function(o) {
var x = {o:o||{},deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('list_devices', 'wa', this.fd, 'host:devices');
})
.then(function(data) {
return this.dexcmd('ra', this.fd);
})
.then(function(data) {
x.devicelist = this._parse_device_list(data);
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [x.devicelist, x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
jdwp_list : function(o) {
var x = {o:o||{},deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('set_transport', 'wa', this.fd, 'host:transport:'+this.deviceid);
})
.then(function(data) {
return this.dexcmd_read_status('jdwp', 'wa', this.fd, 'jdwp');
})
.then(function(data) {
return this.dexcmd_read_stdout(this.fd);
})
.then(function(data) {
this.stdout = data;
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [this.stdout.trim().split(/\r?\n|\r/g), x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
jdwp_forward : function(o) {
// localport:1234
// jdwp:1234
var x = {o:o,deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('forward', 'wa', this.fd, 'host:forward:tcp:'+x.o.localport+';jdwp:'+x.o.jdwp);
})
.then(function(data) {
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
forward_remove_all : function(o) {
var x = {o:o||{},deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('forward_remove_all', 'wa', this.fd, 'host:killforward-all');
})
.then(function(data) {
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
jdwp_connect : function(o) {
// {localport:1234, onreply:fn()}
// note that upon success, this method does not close the connection
var x = {o:o,deferred:$.Deferred()};
this.jdwpinfo = {
o: o,
localport: o.localport,
onreply: o.onreply,
received: [],
};
this.proxy_connect()
.then(function() {
return this.dexcmd('cp', o.localport);
})
.then(function(data) {
this.jdwpfd = data;
return this.dexcmd('wx', this.jdwpfd, 'JDWP-Handshake');
})
.then(function(data) {
return this.dexcmd_read_stdout(this.jdwpfd);
})
.then(function(data) {
if (data!=='JDWP-Handshake') {
// disconnect and fail
return this.dexcmd('dc', this.jdwpfd)
.then(function() {
return this.proxy_disconnect_with_fail({cat:'jdwp', msg:'Invalid handshake response'});
});
}
// start the monitor - we don't want it terminated on timeout
return this.logsend('rj', 'rj '+this.jdwpfd, {notimeout:true});
})
.then(function() {
// the first rj reply is a blank ok message indicating the monitor
// has started
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
jdwp_command : function(o) {
// cmd: JDWP.Command
// resolveonreply: true/false
// send the raw command over the socket - the reply
// is received via the JDWP monitor
var x = {o:o,deferred:$.Deferred()};
this.dexcmd('wx', this.jdwpfd, o.cmd.toRawString())
.fail(function(err) {
o.cmd.deferred.rejectWith(o.ths||this, [err]);
});
o.cmd.deferred
.then(function(decoded,reply,command) {
x.deferred.resolveWith(x.o.ths||this, [decoded,x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
jdwp_disconnect : function(o) {
var x = {o:o,deferred:$.Deferred()};
this.dexcmd('dc', this.jdwpfd)
.then(function() {
delete this.jdwpfd;
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
readwritesocket : function(o) {
var x = {o:o,deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd('qs', this.fd, ''+o.port+':'+o.readlen+':'+o.data);
})
.then(function(data) {
this.socket_reply = data;
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [this.socket_reply, x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
shell_cmd : function(o) {
// command='ls /'
// untilclosed=true
var x = {o:o,deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('set_transport', 'wa', this.fd, 'host:transport:'+this.deviceid);
})
.then(function(data) {
return this.dexcmd_read_status('shell_cmd', 'wa', this.fd, 'shell:'+x.o.command);
})
.then(function(data) {
return this.dexcmd_read_stdout(this.fd, !!x.o.untilclosed);
})
.then(function(data) {
this.stdout = data;
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [this.stdout, x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
logcat : function(o) {
// onlog:function(e)
// onclose:function(e)
// data:anything
var x = {o:o,deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('set_transport', 'wa', this.fd, 'host:transport:'+this.deviceid);
})
.then(function(data) {
return this.dexcmd_read_status('shell_cmd', 'wa', this.fd, 'shell:logcat -v time');
})
.then(function(data) {
// if there's no handler, just read the complete log and finish
if (!o.onlog) {
return this.dexcmd_read_stdout(this.fd)
.then(function(data) {
this.logcatbuffer = data;
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [this.logcatbuffer, x.o.extra]);
});
}
// start the logcat monitor
return this.dexcmd('so', this.fd)
.then(function() {
this.logcatinfo = {
deferred: x.deferred,
buffer: '',
onlog: o.onlog||$.noop,
onlogdata: o.data,
onclose: o.onclose||$.noop,
fd: this.fd,
waitfn:_waitfornextlogcat,
}
this.logcatinfo.waitfn.call(this);
function _waitfornextlogcat() {
// create a new promise for when the next message is received
this.activepromise.so = $.Deferred();
this.activepromise.so
.then(function(data) {
var decodeddata = atob(data);
if (decodeddata === 'eoso:d10d9798-1351-11e5-bdd9-5b316631f026') {
this.logcatinfo.fd=0;
this.proxy_disconnect().always(function() {
var e = {adbclient:this, data:this.logcatinfo.onlogdata};
this.logcatinfo.onclose.call(this, e);
if (this.logcatinfo.end) {
var x = this.logcatinfo.end;
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
}
});
return;
}
var s = this.logcatinfo.buffer + atob(data);
var sp = s.split(/\r\n?|\n/);
if (/[\r\n]$/.test(s)) {
this.logcatinfo.buffer = ''
} else {
this.logcatinfo.buffer = sp.pop();
}
var e = {adbclient:this, data:this.logcatinfo.onlogdata, logs:sp};
this.logcatinfo.onlog.call(this, e);
this.logcatinfo.waitfn.call(this);
});
}
// resolve the promise to indicate that logging has started
return x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
});
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
endlogcat : function(o) {
var x = {o:o||{},deferred:$.Deferred()};
var logcatfd = this.logcatinfo && this.logcatinfo.fd;
if (!logcatfd)
return x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
this.logcatinfo.fd = 0;
this.logcatinfo.end = x;
// close the connection - the monitor callback will resolve the promise
this.dexcmd('dc', logcatfd);
return x.deferred;
},
push_file : function(o) {
// filepathname='/data/local/tmp/fname'
// filedata:<arraybuffer>
// filemtime:12345678
this.push_file_info = o;
var x = {o:o,deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('set_transport', 'wa', this.fd, 'host:transport:'+this.deviceid);
})
.then(function(data) {
return this.dexcmd_read_status('sync', 'wa', this.fd, 'sync:');
})
.then(function() {
var perms = '33204';
var cmddata = this.push_file_info.filepathname+','+perms;
var cmd='SEND'+String.fromCharCode(cmddata.length)+'\0\0\0'+cmddata;
return this.dexcmd('wx', this.fd, cmd)
})
.then(function(data) {
return this.dexcmd_write_data(this.push_file_info.filedata);
})
.then(function(data) {
var cmd='DONE';
var mtime = this.push_file_info.filemtime;
for(var i=0;i < 4; i++)
cmd+= String.fromCharCode((mtime>>(i*8))&255);
return this.dexcmd_read_sync_response('done', 'wx', this.fd, cmd);
})
.then(function(data) {
this.progress = 'quit';
var cmd='QUIT\0\0\0\0';
return this.dexcmd('wx', this.fd, cmd);
})
.then(function(data) {
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
do_auth : function(msg) {
var m = msg.match(/^vscadb proxy version 1/);
if (m) {
this.authdone = true;
this.status='connected';
return this.activepromise.auth.resolveWith(this, []);
}
return this.proxy_disconnect_with_fail({cat:"Authentication", msg:"Proxy handshake failed"});
},
proxy_disconnect_with_fail : function(reason) {
this.disconnect_reject_reason = reason;
return this.proxy_disconnect();
},
proxy_disconnect : function() {
this.ws&&this.ws.close();
return this.activepromise.disconnect;
},
proxy_onopen : function() {
this.status='handshake';
this.logsend('auth','vscadb client version 1')
.then(function(){
this.activepromise.connected.resolveWith(this, []);
});
},
proxy_onerror : function() {
var reason;
if (this.status!=='connecting') {
reason= {cat:"Protocol", msg:"Connection fault"};
} else {
reason = {cat:"Connection", msg:"A connection to the Dex debugger could not be established.", nodbgr:true};
}
this.proxy_disconnect_with_fail(reason);
},
proxy_onmessage : function(e) {
if (!this.authdone)
return this.do_auth(e.data);
var cmd = e.data.substring(0, 2);
var msgresult = e.data.substring(3, 5);
if (cmd === 'rj' && this.jdwpinfo) {
// rj is the receive-jdwp reply - it is handled separately
if (this.jdwpinfo.started) {
this.jdwpinfo.received.push(e.data.substring(6));
if (this.jdwpinfo.received.length > 1) return;
process.nextTick(function() {
while (this.jdwpinfo.received.length) {
var nextdata = this.jdwpinfo.received.shift();
this.jdwpinfo.onreply.call(this.jdwpinfo.o.ths||this, atob(nextdata));
}
}.bind(this));
return;
}
if (e.data === 'rj ok')
this.jdwpinfo.started = new Date();
}
var err;
var ap = this.activepromise[cmd], p = ap;
if (Array.isArray(p))
p = p.shift();
if (msgresult === "ok") {
if (p) {
if (!ap.length)
this.activepromise[cmd] = null;
p.resolveWith(this, [e.data.substring(6)]);
return;
}
err = {cat:"Command", msg:'Missing response message: ' + cmd};
} else if (e.data==='cn error connection failed') {
// this is commonly expected, so remap the error to something nice
err = {cat:"Connection", msg:'ADB server is not running or cannot be contacted'};
} else {
err = {cat:"Command", msg:e.data};
}
this.proxy_disconnect_with_fail(err);
},
proxy_onclose : function(e) {
// when disconnecting, reject any pending promises first
var pending = [];
for (var cmd in this.activepromise) {
do {
var p = this.activepromise[cmd];
if (!p) break;
if (Array.isArray(p))
p = p.shift();
if (p !== this.activepromise.disconnect)
if (p.state()==='pending')
pending.push(p);
} while(this.activepromise[cmd].length);
}
if (pending.length) {
var reject_reason = this.disconnect_reject_reason || {cat:'Connection', msg:'Proxy disconnection'};
for (var i=0; i < pending.length; i++)
pending[i].rejectWith(this, [reject_reason]);
}
// reset the object so it can be reused
var dcinfo = {
client: this,
deferred: this.activepromise.disconnect,
reason: this.disconnect_reject_reason
};
this.status='closed';
this.reset();
// resolve the disconnect promise after all others
pending.unshift(dcinfo);
$.when.apply($, pending)
.then(function(dcinfo) {
if (dcinfo.reason)
dcinfo.deferred.rejectWith(dcinfo.client, [dcinfo.reason]);
else
dcinfo.deferred.resolveWith(dcinfo.client);
});
},
proxy_connect : function(o) {
var ws, port=(o&&o.port)||6037;
try {
ws = new WebSocket('ws://127.0.0.1:'+port);
} catch(e) {
ws=null;
return $.Deferred().rejectWith(this, [new Error('A connection to the ADB proxy could not be established.')]);
};
this.ws = ws;
this.ws.adbclient = this;
this.status='connecting';
// connected is resolved after auth has completed
this.activepromise.connected = $.Deferred();
// disconnect is resolved when the websocket is closed
this.activepromise.disconnect = $.Deferred();
ws.onopen = function(e) {
this.adbclient.proxy_onopen(e);
}
ws.onerror = function(e) {
clearTimeout(this.commandTimeout);
this.adbclient.proxy_onerror(e);
};
ws.onmessage = function(e) {
clearTimeout(this.commandTimeout);
this.adbclient.proxy_onmessage(e);
};
ws.onclose = function(e) {
clearTimeout(this.commandTimeout);
// safari doesn't call onerror for connection failures
if (this.adbclient.status==='connecting' && !this.adbclient.disconnect_reject_reason)
this.adbclient.proxy_onerror(e);
this.adbclient.proxy_onclose(e);
};
// the first promise is always connected, resolved after auth has completed
return this.activepromise.connected.promise();
},
logsend : function(cmd, msg, opts) {
var def = $.Deferred();
if (this.activepromise[cmd]) {
if (Array.isArray(this.activepromise[cmd])) {
// already a queue - just add it
this.activepromise[cmd].push(def);
} else {
// one pending - turn this into a queue
this.activepromise[cmd] = [this.activepromise[cmd], def];
}
} else {
// no active entry
this.activepromise[cmd] = def;
}
if (!this.ws) {
this.proxy_disconnect_with_fail({cat:'Connection', msg:'Proxy disconnected'});
return def;
}
clearTimeout(this.ws.commandTimeout);
try {
this.ws.send(msg);
} catch (e){
this.proxy_disconnect_with_fail({cat:'Connection', msg:e.toString()});
return def;
}
var docmdtimeout = 0;// !(opts&&opts.notimeout);
// if adb is not active, Windows takes at least 1 second to fail
// the socket connect...
this.ws.commandTimeout = docmdtimeout ?
setTimeout(function(adbclient) {
adbclient.proxy_disconnect_with_fail({cat:'Connection', msg:'Command timeout'});
}, 300*1000, this)
: -1;
return def;
},
dexcmd : function(cmd, fd, data, opts) {
var msg = cmd;
if (fd)
msg = msg + " " + fd;
if (data)
msg = msg + " " + btoa(data);
return this.logsend(cmd, msg, opts);
},
dexcmd_read_status : function(cmdname, cmd, fd, data) {
return this.dexcmd(cmd, fd, data)
.then(function() {
return this.dexcmd('rs', this.fd);
})
.then(function(data) {
if (data !== 'OKAY') {
return this.proxy_disconnect_with_fail({cat:"cmd", msg:"Command "+ cmdname +" failed"});
}
return data;
});
},
dexcmd_read_sync_response : function(cmdname, cmd, fd, data) {
return this.dexcmd(cmd, fd, data)
.then(function() {
return this.dexcmd('rs', this.fd, '4');
})
.then(function(data) {
if (data.slice(0,4) !== 'OKAY') {
return this.proxy_disconnect_with_fail({cat:"cmd", msg:"Command "+ cmdname +" failed"});
}
return data;
});
},
dexcmd_read_stdout : function(fd, untilclosed) {
this.stdoutinfo = {
fd: fd,
result:'',
untilclosed:untilclosed||false,
deferred: $.Deferred(),
}
function readchunk() {
this.dexcmd('rx', this.stdoutinfo.fd)
.then(function(data) {
var eod = data==='nomore';
if (data && data.length && !eod) {
this.stdoutinfo.result += atob(data);
}
if (this.stdoutinfo.untilclosed && !eod) {
readchunk.call(this);
return;
}
var info = this.stdoutinfo;
delete this.stdoutinfo;
info.deferred.resolveWith(this, [info.result]);
})
.fail(function(err) {
var info = this.stdoutinfo;
delete this.stdoutinfo;
info.deferred.rejectWith(this, [err]);
});
}
readchunk.call(this);
return this.stdoutinfo.deferred.promise();
},
dexcmd_write_data : function(data) {
this.dtinfo = {
transferred: 0,
transferring: 0,
data: data,
deferred: $.Deferred(),
}
function writechunk() {
this.dtinfo.transferred += this.dtinfo.transferring;
var remaining = this.dtinfo.data.byteLength-this.dtinfo.transferred;
if (remaining <= 0 || isNaN(remaining)) {
var info = this.dtinfo;
delete this.dtinfo;
info.deferred.resolveWith(this, [info.transferred]);
return;
}
var datalen=remaining;
if (datalen > 4000) datalen=4000;
var cmd='DATA';
for(var i=0;i < 4; i++)
cmd+= String.fromCharCode((datalen>>(i*8))&255);
var bytes = new Uint8Array(this.dtinfo.data.slice(this.dtinfo.transferred, this.dtinfo.transferred+datalen));
for(var i=0;i < bytes.length; i++)
cmd+= String.fromCharCode(bytes[i]);
bytes = null;
this.dtinfo.transferring = datalen;
this.dexcmd('wx', this.fd, cmd)
.then(function(data) {
writechunk.call(this);
})
.fail(function(err) {
var info = this.dtinfo;
delete this.dtinfo;
info.deferred.rejectWith(this, [err]);
});
}
writechunk.call(this);
return this.dtinfo.deferred.promise();
},
};
exports.ADBClient = ADBClient;

190
src/chrome-polyfill.js Normal file
View File

@@ -0,0 +1,190 @@
const net = require('net');
const D = require('./util').D;
var sockets_by_id = {};
var last_socket_id = 0;
const chrome = {
storage: {
local: {
q:{},
get(o, cb) {
for (var key in o) {
var x = this.q[key];
if (typeof(x) !== 'undefined') o[key] = x;
}
process.nextTick(cb, o);
},
set(obj, cb) {
for (var key in obj)
this.q[key] = obj[key];
process.nextTick(cb);
}
}
},
runtime: {
_lastError:[],
get lastError() { return this._lastError.pop() },
set lastError(e) { this._lastError.push(e) }
},
permissions: {
request(usbPermissions, cb) {
process.nextTick(cb, true);
}
},
socket: {
listen(socketId, host, port, max_connections, cb) {
var result = 0;
var s = sockets_by_id[socketId];
s._raw.listen(port, host, max_connections);
process.nextTick(cb, result);
},
connect(socketId, host, port, cb) {
var s = sockets_by_id[socketId];
s._raw.connect({port:port,host:host}, function(){
this.s.onerror = null;
this.cb.call(null,0);
}.bind({s:s,cb:cb}));
s.onerror = function(e) {
this.s.onerror = null;
chrome.runtime.lastError = e;
this.cb.call(null,-1);
}.bind({s:s,cb:cb});
},
disconnect(socketId) {
var s = sockets_by_id[socketId];
s._raw.end();
},
setNoDelay(socketId, state, cb) {
var s = sockets_by_id[socketId];
s._raw.setNoDelay(state);
process.nextTick(cb, 1);
},
read(socketId, bufferSize, onRead) {
if (!onRead && typeof(bufferSize) === 'function')
onRead = bufferSize, bufferSize=-1;
if (!onRead) return;
var s = sockets_by_id[socketId];
if (bufferSize === 0) {
process.nextTick(function(onRead) {
onRead.call(null, {resultCode:1,data:Buffer.alloc(0)});
}, onRead);
return;
}
s.read_requests.push({onRead:onRead, bufferSize:bufferSize});
if (s.read_requests.length > 1) {
return;
}
!s.ondata && s._raw.on('data', s.ondata = function(data) {
this.readbuffer = Buffer.concat([this.readbuffer, data]);
while(this.read_requests.length) {
var amount = this.read_requests[0].bufferSize;
if (amount <= 0) amount = this.readbuffer.length;
if (amount > this.readbuffer.length || this.readbuffer.length === 0)
return; // wait for more data
var readInfo = {
resultCode:1,
data:Buffer.from(this.readbuffer.slice(0,amount)),
};
this.readbuffer = this.readbuffer.slice(amount);
this.read_requests.shift().onRead.call(null,readInfo);
}
this.onerror = this.onclose = null;
}.bind(s));
var on_read_terminated = function(e) {
this.readbuffer = Buffer.alloc(0);
while(this.read_requests.length) {
var readInfo = {
resultCode:-1, // <=0 for error
};
this.read_requests.shift().onRead.call(null,readInfo);
}
this.onerror = this.onclose = null;
}.bind(s);
!s.onerror && (s.onerror = on_read_terminated);
!s.onclose && (s.onclose = on_read_terminated);
if (s.readbuffer.length || bufferSize < 0) {
process.nextTick(s.ondata, Buffer.alloc(0));
}
},
write(socketId, data, cb) {
var s = sockets_by_id[socketId];
if (!(data instanceof Buffer))
data = Buffer.from(data);
s._raw.write(data, function(e,f,g) {
if (this.s.write_cbs.length === 1)
this.s.onerror = null;
var writeInfo = {
bytesWritten: this.len,
};
this.s.write_cbs.shift().call(null, writeInfo);
}.bind({s:s,len:data.length,cb:cb}));
s.write_cbs.push(cb);
if (!s.onerror) {
s.onerror = function(e) {
this.s.onerror = null;
while (this.s.write_cbs.length) {
var writeInfo = {
bytesWritten: 0,
};
this.s.write_cbs.shift().call(null, writeInfo);
}
}.bind({s:s});
}
},
},
create_socket:function(id, type, cb) {
if (!cb && typeof(type) === 'function') {
cb = type, type = null;
}
var socket = type === 'server' ? new net.Server() : new net.Socket();
var socketInfo = {
id: id,
socketId: ++last_socket_id,
_raw: socket,
onerror:null,
onclose:null,
write_cbs:[],
read_requests:[],
readbuffer:Buffer.alloc(0),
};
socketInfo._raw.on('error', function(e) {
this.onerror && this.onerror(e);
}.bind(socketInfo));
socketInfo._raw.on('close', function(e) {
this.onclose && this.onclose(e);
}.bind(socketInfo));
sockets_by_id[socketInfo.socketId] = socketInfo;
process.nextTick(cb, socketInfo);
},
create_chrome_socket(id, type, cb) { return chrome.create_socket(id, type, cb) },
accept_socket:function(id, socketId, cb) {
var s = sockets_by_id[socketId];
if (s.onconnection) {
s.onconnection = cb;
} else {
s.onconnection = cb;
s._raw.on('connection', function(client_socket) {
var acceptInfo = {
socketId: ++last_socket_id,
_raw: client_socket,
}
sockets_by_id[acceptInfo.socketId] = acceptInfo;
this.onconnection(acceptInfo);
}.bind(s));
}
},
accept_chrome_socket(id, socketId, cb) { return chrome.accept_socket(id, socketId, cb) },
destroy_socket:function(socketId) {
var s = sockets_by_id[socketId];
if (!s) return;
s._raw.end();
sockets_by_id[socketId] = null;
},
destroy_chrome_socket(socketId) { return chrome.destroy_socket(socketId) },
}
exports.chrome = chrome;

1135
src/debugMain.js Normal file

File diff suppressed because it is too large Load Diff

1480
src/debugger.js Normal file

File diff suppressed because it is too large Load Diff

1056
src/jdwp.js Normal file

File diff suppressed because it is too large Load Diff

124
src/jq-promise.js Normal file
View File

@@ -0,0 +1,124 @@
// a very stripped down polyfill implementation of jQuery's promise methods
const util = require('util'); // for util.inspect
var $ = this;
// Deferred wraps a Promise into a jQuery-like object
var Deferred = exports.Deferred = function(p, parent) {
var o = {
_isdeferred:true,
_original:null,
_promise:null,
_fns:null,
_context:null,
_parent:null,
_root:null,
promise() {
return this;
},
then(fn) {
var thendef = $.Deferred(null, this);
var p = this._promise.then(function(a) {
var res = this.fn.apply(a._ctx, a._args);
if (res === undefined)
return a;
if (res && res._isdeferred)
return res._promise;
return {_ctx:a._ctx, _args:[res]}
}.bind({def:thendef,fn:fn}));
thendef._promise = thendef._original = p;
return thendef;
},
fail(fn) {
var faildef = $.Deferred(null, this);
var p = this._promise.catch(function(a) {
if (a.stack) {
console.error(a.stack);
a = [a];
}
if (this.def._context === null && this.def._parent)
this.def._context = this.def._parent._context;
if (this.def._context === null && this.def._root)
this.def._context = this.def._root._context;
var res = this.fn.apply(this.def._context,a);
if (res === undefined)
return a;
return res;
}.bind({def:faildef,fn:fn}));
faildef._promise = faildef._original = p;
return faildef;
},
state() {
var m = util.inspect(this._original).match(/^Promise\s*\{\s*<(\w+)>/); // urgh!
// anything that's not pending or rejected is resolved
return m ? m[1] : 'resolved';
},
resolve:function() {
return this.resolveWith(null, Array.prototype.map.call(arguments,x=>x));
},
resolveWith:function(ths, args) {
if (typeof(args) === 'undefined') args = [];
if (!Array.isArray(args))
throw new Error('resolveWith must be passed an array of arguments');
if (this._root) {
this._root.resolveWith(ths, args);
return this;
}
if (ths === null || ths === undefined) ths = this;
this._fns[0]({_ctx:ths,_args:args});
return this;
},
reject:function() {
return this.rejectWith(null, Array.prototype.map.call(arguments,x=>x));
},
rejectWith:function(ths,args) {
if (typeof(args) === 'undefined') args = [];
if (!Array.isArray(args))
throw new Error('rejectWith must be passed an array of arguments');
if (this._root) {
this._root.rejectWith(ths, args);
return this;
}
this._context = ths;
this._fns[1](args);
return this;
},
}
if (parent) {
o._original = o._promise = p;
o._parent = parent;
o._root = parent._root || parent;
} else {
o._original = o._promise = new Promise((res,rej) => {
o._fns = [res,rej];
});
}
return o;
}
// $.when() is jQuery's version of Promise.all()
// - this version just scans the array of arguments waiting on any Deferreds in turn before finally resolving the return Deferred
var when = exports.when = function() {
var x = {
def: $.Deferred(),
args: Array.prototype.map.call(arguments,x=>x),
idx:0,
next(x) {
if (x.idx >= x.args.length) {
return process.nextTick(x => {
x.def.resolveWith(null, x.args);
}, x);
}
if ((x.args[x.idx]||{})._isdeferred) {
x.args[x.idx].then(function() {
var x = this, result = Array.prototype.map.call(arguments,x=>x);
x.args[x.idx] = result;
x.idx++; x.next(x);
}.bind(x));
return;
}
x.idx++; x.next(x);
},
};
x.next(x);
return x.def;
}

12
src/jsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"lib": [
"es6"
]
},
"exclude": [
"node_modules"
]
}

122
src/minwebsocket.js Normal file
View File

@@ -0,0 +1,122 @@
/*
A dummy websocket implementation for passing messages internally using a WS-like protocol
*/
var Servers = {};
function isfn(x) { return typeof(x) === 'function' }
function WebSocketClient(url) {
// we only support localhost addresses in this implementation
var match = url.match(/^ws:\/\/127\.0\.0\.1:(\d+)$/);
var port = match && parseInt(match[1],10);
if (!port || port <= 0 || port >= 65536)
throw new Error('Invalid websocket url');
var server = Servers[port];
if (!server) throw new Error('Connection refused'); // 'port' already in use :)
server.addClient(this);
this._ws = {
port: port,
server: server,
outgoing:[],
};
}
WebSocketClient.prototype.send = function(message) {
this._ws.outgoing.push(message);
if (this._ws.outgoing.length > 1) return;
process.nextTick(function(client) {
if (!client || !client._ws || !client._ws.server)
return;
client._ws.server.receive(client, client._ws.outgoing);
client._ws.outgoing = [];
}, this);
}
WebSocketClient.prototype.receive = function(messages) {
if (isfn(this.onmessage))
messages.forEach(m => {
this.onmessage({
data:m
});
});
}
WebSocketClient.prototype.close = function() {
process.nextTick(() => {
this._ws.server.rmClient(this);
this._ws.server = null;
if (isfn(this.onclose))
this.onclose(this);
this._ws = null;
});
}
function WebSocketServer(port) {
if (typeof(port) !== 'number' || port <= 0 || port >= 65536)
throw new Error('Invalid websocket server port');
if (Servers[''+port])
throw new Error('Address in use');
this.port = port;
this.clients = [];
Servers[''+port] = this;
}
WebSocketServer.prototype.addClient = function(client) {
var status;
this.clients.push(status = {
server:this,
client: client,
onmessage:null,
onclose:null,
outgoing:[],
send: function(message) {
this.outgoing.push(message);
if (this.outgoing.length > 1) return;
process.nextTick(function(status) {
if (!status || !status.client)
return;
status.client.receive(status.outgoing);
status.outgoing = [];
}, this);
}
});
process.nextTick((status) => {
if (isfn(this.onconnection))
this.onconnection({
status: status,
accept:function() {
process.nextTick((status) => {
if (isfn(status.client.onopen))
status.client.onopen(status.client);
}, this.status);
return this.status;
}
});
}, status);
}
WebSocketServer.prototype.rmClient = function(client) {
for (var i = this.clients.length-1; i >= 0; --i) {
if (this.clients[i].client === client) {
if (isfn(this.clients[i].onclose))
this.clients[i].onclose();
this.clients.splice(i, 1);
}
}
}
WebSocketServer.prototype.receive = function(client, messages) {
var status = this.clients.filter(c => c.client === client)[0];
if (!status) return;
if (!isfn(status.onmessage)) return;
messages.forEach(m => {
status.onmessage({
data: m,
});
});
}
exports.WebSocketClient = WebSocketClient;
exports.WebSocketServer = WebSocketServer;

322
src/services.js Normal file
View File

@@ -0,0 +1,322 @@
const chrome = require('./chrome-polyfill').chrome;
const { new_socketfd } = require('./sockets');
const { create_chrome_socket, accept_chrome_socket, destroy_chrome_socket } = chrome;
var start_request = function(fd) {
if (fd.closeState) return;
// read service passed from client
D('waiting for adb request...');
readx_with_data(fd, function(err, data) {
if (err) {
D('SS: error %o', err);
return;
}
handle_request(fd, data.asString());
start_request(fd);
});
}
var handle_request = exports.handle_request = function(fd, service) {
if (!service){
D('SS: no service');
sendfailmsg(fd, 'No service received');
return false;
}
D('adb request: %s', service);
if (service.slice(0,4) === 'host') {
// trim 'host:'
return handle_host_request(service.slice(5), 'kTransportAny', null, fd);
}
if (!fd.transport) {
D('No transport configured - using any found');
var t = acquire_one_transport('CS_DEVICE', 'kTransportAny', null);
t = check_one_transport(t, '', fd);
if (!t) return false;
fd.transport = t;
}
// once we call open_device_service, the fd belongs to the transport
open_device_service(fd.transport, fd, service, function(err, serviceinfo) {
if (err) {
sendfailmsg(fd, 'Device connection failed');
return;
}
D('device service opened: %o', serviceinfo);
send_okay(fd);
});
return true;
}
var sendfailmsg = function(fd, reason) {
reason = reason.slice(0, 0xffff);
var msg = 'FAIL' + intToHex(reason.length,4) + reason;
writex(fd, msg);
}
var handle_host_request = function(service, ttype, serial, replyfd) {
var transport;
if (service === 'kill') {
cl('service kill request');
send_okay(replyfd);
killall_devices();
//window.close();
return false;
}
if (service.slice(0,9) === 'transport') {
var t,serialmatch;
switch(service.slice(9)) {
case '-any':
t = acquire_one_transport('CS_ANY','kTransportAny',null);
break;
case '-local':
t = acquire_one_transport('CS_ANY','kTransportLocal',null);
break;
case '-usb':
t = acquire_one_transport('CS_ANY','kTransportUsb',null);
break;
default:
if (serialmatch = service.slice(9).match(/^:(.+)/))
t = acquire_one_transport('CS_ANY','kTransportAny',serialmatch[1]);
break;
}
t = check_one_transport(t, serialmatch&&serialmatch[1], replyfd);
if (!t) return false;
// set the transport in the fd - the client can use it
// to send raw data directly to the device
D('transport configured: %o', t);
replyfd.transport = t;
adb_writebytes(replyfd, "OKAY");
return false;
}
if (service.slice(0,7) === 'devices') {
var use_long = service.slice(7)==='-l';
D('Getting device list');
var transports = list_transports(use_long);
D('Wrote device list');
send_msg_with_okay(replyfd, transports);
return false;
}
if (service === 'version') {
var version = intToHex(ADB_SERVER_VERSION, 4);
send_msg_with_okay(replyfd, version);
return false;
}
if (service.slice(0,9) === 'emulator:') {
var port = service.slice(9);
port = port&&parseInt(port, 10)||0;
if (!port || port <= 0 || port >= 65536) {
D('Invalid emulator port: %s', service);
return false;
}
local_connect(port, function(err) {
});
// no reply needed
return false;
}
if (service.slice(0,9) === 'get-state') {
transport = acquire_one_transport('CS_ANY', ttype, serial, null);
transport = check_one_transport(transport, serial, replyfd);
if (!transport) return false;
var state = connection_state_name(transport);
send_msg_with_okay(replyfd, state);
return false;
}
if (service === 'killforward-all') {
remove_all_forward_listeners();
writex(replyfd, 'OKAY');
return false;
}
var fwdmatch = service.match(/^forward:(tcp:\d+);(jdwp:\d+)/);
if (fwdmatch) {
transport = acquire_one_transport('CS_ANY', ttype, serial, null);
transport = check_one_transport(transport, serial, replyfd);
if (!transport) return false;
install_forward_listener(fwdmatch[1], fwdmatch[2], transport, function(err) {
if (err) return sendfailmsg(replyfd, err.msg);
// on the host, 1st OKAY is connect, 2nd OKAY is status
writex(replyfd, 'OKAY');
writex(replyfd, 'OKAY');
});
return false;
}
if (service === 'track-devices') {
writex(replyfd, 'OKAY');
add_device_tracker(replyfd);
// fd now belongs to the tracker
return true;
}
if (service === 'track-devices-extended') {
writex(replyfd, 'OKAY');
add_device_tracker(replyfd, true);
// fd now belongs to the tracker
return true;
}
cl('Ignoring host service request: %s', service);
return false;
}
var check_one_transport = function(t, serial, replyfd) {
var which = serial||'(null)';
switch((t||[]).length) {
case 0:
sendfailmsg(replyfd, "device '"+which+"' not found");
return null;
case 1: t = t[0];
break;
default:
sendfailmsg(replyfd, 'more than one device/emulator');
return null;
}
switch(t.connection_state) {
case 'CS_DEVICE': break;
case 'CS_UNAUTHORIZED':
sendfailmsg(replyfd, 'device unauthorized.\r\nCheck for a confirmation dialog on your device or reconnect the device.');
return null;
default:
sendfailmsg(replyfd, 'Device not ready');
return null;
}
return t;
}
var forward_listeners = {};
var install_forward_listener = function(local, remote, t, cb) {
var localport = parseInt(local.split(':').pop(), 10);
var socket = chrome.socket;
create_chrome_socket('forward listener:'+localport, function(socketInfo) {
if (chrome.runtime.lastError) {
return cb({msg:chrome.runtime.lastError.message||'socket creation failed'});
}
socket.listen(socketInfo.socketId, '127.0.0.1', localport, 5,
function(result) {
if (chrome.runtime.lastError) {
var err = {msg:chrome.runtime.lastError.message||'socket listen failed'};
destroy_setup(socketInfo);
return cb(err);
}
if (result < 0) {
destroy_setup(socketInfo);
return cb({msg:'Cannot bind to socket'});
}
forward_listeners[localport] = {
port:localport,
socketId: socketInfo.socketId,
connectors_fd: null,
connect_cb:function(){},
};
accept_chrome_socket('forward server:'+localport, socketInfo.socketId, function(acceptInfo) {
accept_forward_connection(socketInfo.socketId, acceptInfo, localport, local, remote, t);
});
// listener is ready
D('started forward listener on port %d: %d', localport, socketInfo.socketId);
cb();
}
);
});
function destroy_setup(socketInfo) {
destroy_chrome_socket(socketInfo.socketId);
}
}
var connect_forward_listener = exports.connect_forward_listener = function(port, opts, cb) {
// if we're implementing the adb service, this will already be created
// if we're connecting via the adb executable, we need to create a dummy entry
if (!forward_listeners[port]) {
if (opts && opts.create) {
forward_listeners[port] = {
is_external_adb: true,
port:port,
socketId: null,
connectors_fd: null,
connect_cb:function(){},
}
} else {
D('Refusing forward connection request - forwarder for port %d does not exist', port);
return cb();
}
}
create_chrome_socket('forward client:'+port, function(createInfo) {
// save the receiver info
forward_listeners[port].connectors_fd = new_socketfd(createInfo.socketId);
forward_listeners[port].connect_cb = cb;
// do the connect - everything from here on is handled in the accept routine
chrome.socket.connect(createInfo.socketId, '127.0.0.1', port, function(result) {
chrome.socket.setNoDelay(createInfo.socketId, true, function(result) {
var x = forward_listeners[port];
if (x.is_external_adb) {
delete forward_listeners[port];
x.connect_cb(x.connectors_fd);
}
});
});
});
}
var accept_forward_connection = exports.accept_forward_connection = function(listenerSocketId, acceptInfo, port, local, remote, t) {
if (chrome.runtime.lastError) {
D('Forward port socket accept failed: '+port);
var listener = remove_forward_listener(listenerSocketId);
return listener.connect_cb();
}
// on accept - create the remote connection to the device
D('Binding forward port connection to remote port %s', remote);
var sfd = new_socketfd(acceptInfo.socketId);
// remove the listener
var listener = remove_forward_listener(listenerSocketId);
chrome.socket.setNoDelay(acceptInfo.socketId, true, function(result) {
// start the connection as a service
open_device_service(t, sfd, remote, function(err) {
listener.connect_cb(listener.connectors_fd);
});
});
}
var remove_forward_listener = exports.remove_forward_listener = function(socketId) {
for (var port in forward_listeners) {
if (forward_listeners[port].socketId === socketId) {
var x = forward_listeners[port];
delete forward_listeners[port];
destroy_chrome_socket(x.socketId);
D('removed forward listener: %d', x.socketId);
return x;
}
}
}
var remove_all_forward_listeners = exports.remove_all_forward_listeners = function() {
var ports = Object.keys(forward_listeners);
while (ports.length) {
remove_forward_listener(forward_listeners[ports.pop()].socketId);
}
}

290
src/sockets.js Normal file
View File

@@ -0,0 +1,290 @@
const chrome = require('./chrome-polyfill').chrome;
const { create_chrome_socket, destroy_chrome_socket } = chrome;
const { D, remove_from_list } = require('./util');
// array of local_sockets
var _local_sockets = [];
var _new_local_socket_id = 1000;
var new_local_socket = function(t, fd, close_fd_on_local_socket_close) {
var x = {
id:++_new_local_socket_id,
fd:fd,
close_fd_on_local_socket_close: !!close_fd_on_local_socket_close,
transport:t,
enqueue: local_socket_enqueue,
ready: local_socket_ready_notify,
close: local_socket_close,
peer:null,
//socketbuffer: [],
}
_local_sockets.push(x);
return x;
}
var find_local_socket = function(local_socket_id, peer_socket_id) {
for (var i=0; i < _local_sockets.length; i++) {
var ls = _local_sockets[i];
if (ls.id === local_socket_id) {
if (!peer_socket_id) return ls;
if (!ls.peer) continue;
if (ls.peer.id === peer_socket_id) return ls;
}
}
return null;
}
var local_socket_ready = function(s) {
D("LS(%d): ready()\n", s.id);
}
var local_socket_ready_notify = function(s) {
s.ready = local_socket_ready;
send_okay(s.fd);
s.ready(s);
}
var local_socket_enqueue = function(s, p) {
D("LS(%d): enqueue()\n", s.id, p.len);
if (s.fd.closed) return false;
D("LS: enqueue() - writing %d bytes to fd:%d %o\n", p.len, s.fd.n, s.fd);
adb_writebytes(s.fd, p.data, p.len);
//s.socketbuffer.push({data:p.data, len:p.len});
return true;
}
var local_socket_close = function(s) {
// flush the data to the output socket
/*var totallen = s.socketbuffer.reduce(function(n, x) { return n+x.len },0);
adb_writebytes(s.fd, intToHex(totallen,4));
s.socketbuffer.forEach(function(x) {
adb_writebytes(s.fd, x.data, x.len);
});*/
if (s.peer) {
s.peer.peer = null;
s.peer.close(s.peer);
s.peer = null;
}
if (s.fd && s.close_fd_on_local_socket_close) {
s.fd.close();
}
var id = s.id;
var idx = _local_sockets.indexOf(s);
if (idx >= 0) _local_sockets.splice(idx, 1);
D("LS(%d): closed()\n", id);
}
var local_socket_force_close_all = function(t) {
// called when a transport disconnects without a clean finish
var lsarr = _local_sockets.reduce(function(res, ls) {
if (ls && ls.transport === t) res.push(ls);
return res;
}, []);
lsarr.forEach(function(ls) {
D('force closing socket: %o', ls);
local_socket_close(ls);
});
}
var remote_socket_ready = function(s, cb) {
D("entered remote_socket_ready RS(%d) OKAY fd=%d peer.fd=%d\n",
s.id, s.fd, s.peer.fd);
p = get_apacket();
p.msg.command = A_OKAY;
p.msg.arg0 = s.peer.id;
p.msg.arg1 = s.id;
send_packet(p, s.transport, cb);
}
var remote_socket_close = function(s) {
if (s.peer) {
s.peer.peer = null;
s.peer.close(s.peer);
}
D("RS(%d): closed\n", s.id);
}
var create_remote_socket = function(id, t) {
var s = {
id: id,
transport: t,
peer:null,
ready: remote_socket_ready,
close: remote_socket_close,
// a remote socket is a normal socket with an extra disconnect function
disconnect:null,
}
D("RS(%d): created\n", s.id);
// when a
return s;
}
var loopback_clients = [];
var get_socket_fd_from_fdn = exports.get_socket_fd_from_fdn = function(n) {
for (var i=0; i < loopback_clients.length; i++) {
if (loopback_clients[i].n === n)
return loopback_clients[i];
}
return null;
}
var socket_loopback_client = exports.socket_loopback_client = function(port, cb) {
create_chrome_socket('socket_loopback_client', function(createInfo) {
chrome.socket.connect(createInfo.socketId, '127.0.0.1', port, function(result) {
if (result < 0) {
destroy_chrome_socket(createInfo.socketId);
return cb();
}
chrome.socket.setNoDelay(createInfo.socketId, true, function(result) {
var x = new_socketfd(createInfo.socketId);
return cb(x);
});
});
});
}
var new_socketfd = exports.new_socketfd = function(socketId) {
var x = {
n: socketId,
isSocket:true,
connected:true,
closed:false,
// readbytes and writebytes are used by readx and writex
readbytes:function(len, cb) {
slc_read(this, len, function(err, data){
cb(err, data);
});
},
writebytes:function(data, cb) {
slc_write(this, data, cb||function(){});
},
close:function() {
slc_close(this, function(){});
}
};
loopback_clients.push(x);
return x;
}
var slc_readwithkick = function(sfd, cb) {
/*if (sfd.reader_cb_stack.length) {
return cb(null, new Uint8Array(0));
}*/
//var readinfo = {cb:cb, expired:false};
//sfd.reader_cb_stack.push(readinfo);
var kicker = setTimeout(function() {
if (!kicker) return;
kicker = null;
D('reader kick expired - retuning nothing');
//readinfo.expired = true;
cb(null, new Uint8Array(0));
}, 100);
slc_read_stacked_(sfd, function(err, data) {
if (!kicker) {
D('Discarding data recevied after kick expired');
return;
}
clearTimeout(kicker);
kicker = null;
cb(err, data);
});
};
var slc_read = function(sfd, minlen, cb) {
//sfd.reader_cb_stack.push({cb:cb, expired:false});
slc_read_stacked_(sfd, minlen, cb);
}
var slc_read_stacked_ = function(sfd, minlen, cb) {
var params = [sfd.n];
switch(typeof(minlen)) {
case 'number': params.push(minlen); break;
case 'function': cb = minlen; // fall through
default: minlen = 'any';
};
var buffer = new Uint8Array(minlen==='any'?65536:minlen);
var buffer_offset = 0;
var onread = function(readInfo) {
if (chrome.runtime.lastError) {
slc_close(sfd, function() {
cb({msg: 'socket read error. Terminating socket'});
});
return;
}
if (readInfo.resultCode < 0) return cb(readInfo);
buffer.set(new Uint8Array(readInfo.data), buffer_offset);
buffer_offset += readInfo.data.byteLength;
if (typeof(minlen)==='number' &&buffer_offset < minlen) {
// read more
params[1] = minlen - buffer_offset;
chrome.socket.read.apply(chrome.socket, params);
return;
}
buffer = buffer.subarray(0, buffer_offset);
buffer.asString = function() { return arrayBufferToString(this); }
return cb(null, buffer);
};
params.push(onread);
chrome.socket.read.apply(chrome.socket, params);
}
var slc_write = function(sfd, data, cb) {
var buf = data.buffer;
if (buf.byteLength !== data.byteLength) {
buf = buf.slice(0, data.byteLength);
}
chrome.socket.write(sfd.n, buf, function(writeInfo) {
if (chrome.runtime.lastError) {
slc_close(sfd, function() {
cb({msg: 'socket write error. Terminating socket'});
});
return;
}
if (writeInfo.bytesWritten !== data.byteLength)
return cb({msg: 'socket write mismatch. wanted:'+data.byteLength+', sent:'+writeInfo.bytesWritten});
cb();
});
}
var slc_shutdown = function(sfd, cb) {
if (sfd.connected) {
sfd.connected = false;
chrome.socket.disconnect(sfd.n);
}
cb();
}
var slc_close = function(sfd, cb) {
if (sfd.connected) {
sfd.connected = false;
chrome.socket.disconnect(sfd.n);
}
sfd.closed = true;
destroy_chrome_socket(sfd.n);
remove_from_list(loopback_clients, sfd);
cb();
}
var fd_loopback_client = function() {
var s = [];
adb_socketpair(s, 'fd_loopback_client', true);
D('fd_loopback_client created. server fd:%d, client fd:%d', s[1].n, s[0].n);
// return one side and pass the other side to the request handler
start_request(s[1]);
return s[0];
}

424
src/transport.js Normal file
View File

@@ -0,0 +1,424 @@
const D = function(){};// require('./util').D;
var transport_list = [];
var next_connect_device_service_id = 1;
var open_device_service = exports.open_device_service = function(t, fd, service, cb) {
D('open_device_service %s on device %s', service, t.serial);
var p = get_apacket();
p.msg.command = A_OPEN;
p.msg.arg0 = ++next_connect_device_service_id;
p.msg.data_length = service.length+1;
p.data.set(str2u8arr(service));
var serviceinfo = {
service: service,
transport: t,
localid: p.msg.arg0,
remoteid: 0,
state: 'init',
nextokay:null,
nextwrte:null,
nextclse:on_device_close_reply,
clientfd: fd,
isjdwp: /^jdwp\:\d+/.test(service),
islogcat: /^(shell:)?logcat/.test(service),
};
t.open_services.push(serviceinfo);
serviceinfo.nextokay = on_device_open_okay;
serviceinfo.state = 'talking';
send_packet(p, t, function(err) {
if (err) {
serviceinfo.state = 'init-error';
remove_device_service(serviceinfo);
return cb(err);
}
});
function ignore_response(err, p, serviceinfo, receivecb) {
D('ignore_response, p=%o', p);
receivecb();
}
function on_device_open_okay(err, p, serviceinfo, receivecb) {
D('on_device_open_okay: %s, err:%o', serviceinfo.service, err);
if (err) {
receivecb();
cb(err);
return;
}
serviceinfo.state = 'ready';
serviceinfo.nextokay = ignore_response;
serviceinfo.nextwrte = on_device_write_reply;
// ack the packet receive callback
receivecb();
// ack the open_device_service callback
cb(null, serviceinfo);
// start reading from the client
read_from_client(serviceinfo);
}
function read_from_client(serviceinfo) {
D('Waiting for client data');
serviceinfo.clientfd.readbytes(function(err, data) {
if (err) {
// read error - the client probably closed the connection
send_close_device_service(serviceinfo, function(err) {
remove_device_service(serviceinfo);
});
return;
}
D('client WRTE %d bytes to device', data.byteLength);
// send the data to the device
var p = get_apacket();
p.msg.command = A_WRTE;
p.msg.arg0 = serviceinfo.localid;
p.msg.arg1 = serviceinfo.remoteid;
p.msg.data_length = data.byteLength;
p.data.set(data);
if (serviceinfo.isjdwp)
print_jdwp_data('out',data);
serviceinfo.nextokay = function(err, p, serviceinfo, receivecb) {
if (err) {
// if we fail to write, just abort
remove_device_service(serviceinfo);
receivecb();
return;
}
D('client WRTE - got OKAY');
serviceinfo.nextokay = ignore_response;
receivecb();
// read and send more
read_from_client(serviceinfo);
}
send_packet(p, t, function(err) {
if (err) {
// if we fail to write, just abort
remove_device_service(serviceinfo);
return;
}
// we must wait until the next OKAY until we can write more
D('client WRTE - waiting for OKAY');
});
});
}
function on_device_write_reply(err, p, serviceinfo, receivecb) {
D('device WRTE received');
if (err) {
serviceinfo.state = 'write reply error';
remove_device_service(serviceinfo);
receivecb();
return;
};
// when we receive a WRTE, we must reply with an OKAY as the very next packet.
// - we can't wait for the data to be forwarded because the reader might post
// something in between
D('sending OKAY');
send_ready(serviceinfo.localid, serviceinfo.remoteid, serviceinfo.transport, function(err){
if (err) {
serviceinfo.state = 'write okay error';
remove_device_service(serviceinfo);
return;
}
D('sent OKAY');
});
if (serviceinfo.isjdwp)
print_jdwp_data('dev', p.data);
// write the data to the client
serviceinfo.clientfd.writebytes(new Uint8Array(p.data.buffer.slice(0, p.msg.data_length)), function(err) {
// ack the packet receive callback
receivecb();
});
}
function on_device_close_reply(err, p, serviceinfo, receivecb) {
var t = serviceinfo.transport;
D('on_device_close_reply %s (by device) on device %s', serviceinfo.service, t.serial);
serviceinfo.state = 'closed (by device)';
remove_device_service(serviceinfo);
// ack the packet receive callback
receivecb();
}
}
var find_open_device_service = exports.find_open_device_service = function(t, localid, remoteid) {
for (var i=0; i < t.open_services.length; i++) {
var s = t.open_services[i];
if (s.localid === localid && (!remoteid ||(s.remoteid === remoteid))) {
return s;
}
}
return null;
}
var send_close_device_service = exports.send_close_device_service = function(serviceinfo, cb) {
D('send_close_device_service: %s, device:%s', serviceinfo.service, serviceinfo.transport.serial);
var p = get_apacket();
p.msg.command = A_CLSE;
p.msg.arg0 = serviceinfo.localid;
p.msg.arg1 = serviceinfo.remoteid;
serviceinfo.nextreply = on_close_request_reply;
serviceinfo.state = 'talking';
send_packet(p, serviceinfo.transport, function(err) {
if (err) {
serviceinfo.state = 'error';
} else {
serviceinfo.state = 'closed';
}
// ack the close_device_service request as soon as we
// send the packet - don't wait for the reply
return cb(err);
});
function on_close_request_reply(which, serviceinfo, receivecb) {
// ack the packet receive callback
receivecb();
}
}
var remove_device_service = exports.remove_device_service = function(serviceinfo) {
var fd;
if (fd=serviceinfo.clientfd) {
serviceinfo.clientfd=null;
fd.close();
}
remove_from_list(serviceinfo.transport.open_services, serviceinfo);
}
var register_transport = exports.register_transport = function(t, cb) {
t.terminated = false;
t.open_services = [];
transport_list.push(t);
// start the reader
function read_next_packet_from_transport(t, packetcount) {
var p = new_apacket();
t.read_from_remote(p, t, function(err, p) {
if (t.terminated) {
return;
}
if (err) {
D('Error reading next packet from transport:%s - terminating.', t.serial);
kick_transport(t);
unregister_transport(t);
return;
}
p.which = intToCharString(p.msg.command);
D('Read packet:%d (%s) from transport:%s', packetcount, p.which, t.serial);
var pc = packetcount++;
handle_packet(p, t, function(err) {
D('packet:%d handled, err:%o', pc, err);
read_next_packet_from_transport(t, packetcount);
});
});
}
read_next_packet_from_transport(t, 0);
D("transport: %s registered\n", t.serial);
D('new transport list: %o', transport_list.slice());
update_transports();
ui.update_device_property(t.deviceinfo, 'status', 'Connecting...');
send_connect(t, cb);
}
var unregister_transport = exports.unregister_transport = function(t) {
if (t.fd)
t.fd.close();
// kill any connected services
while (t.open_services.length) {
remove_device_service(t.open_services.pop());
}
remove_from_list(transport_list, t);
D("transport: %s unregistered\n", t.serial);
D('remaining transports: %o', transport_list.slice());
t.serial = 'REMOVED:' + t.serial;
t.terminated = true;
update_transports();
ui.update_device_property(t.deviceinfo, 'status', 'Disconnected', '#8B0E0E');
ui.remove_disconnected_device(t.deviceinfo);
}
var kick_transport = exports.kick_transport = function(t) {
if (t && !t.kicked) {
t.kicked = true;
t.kick(t);
}
}
var write_packet_to_transport = exports.write_packet_to_transport = function(t, p, cb) {
if (t.terminated) {
D('Refusing to write packet to terminated transport: %s', t.serial);
return cb({msg:'device not found'});
}
t.write_to_remote(p, t, function(err) {
cb(err);
});
}
var send_packet = exports.send_packet = function(p, t, cb) {
p.msg.magic = p.msg.command ^ 0xffffffff;
var count = p.msg.data_length;
var x = new Uint8Array(p.data);
var sum = 0, i=0;
while(count-- > 0){
sum += x[i++];
}
p.msg.data_check = sum;
write_packet_to_transport(t, p, cb);
}
var acquire_one_transport = exports.acquire_one_transport = function(connection_state, transport_type, serial) {
var candidates = [];
for (var i=0, tl=transport_list; i < tl.length; i++) {
if (connection_state !== 'CS_ANY' && tl[i].connection_state !== connection_state)
continue;
if (transport_type !== 'kTransportAny' && tl[i].transport_type !== transport_type)
continue;
if (serial && tl[i].serial !== serial)
continue;
candidates.push(tl[i]);
}
return candidates;
}
var statename = exports.statename = function(t) {
if (/^CS_.+/.test(t.connection_state))
return t.connection_state.slice(3).toLowerCase();
return 'unknown state: ' + t.connection_state;
}
var typename = exports.typename = function(t) {
if (/^kTransport.+/.test(t.type))
return t.type.slice(10).toLowerCase();
return 'unknown type: ' + t.type;
}
var format_transport = exports.format_transport = function(t, format) {
var serial = t.serial || '???????????';
if (!format) {
return serial+'\t'+statename(t);
} else if (format === 'extended') {
return '{'+[
'"device":'+JSON.stringify(t.device),
'"model":'+JSON.stringify(t.model||t.deviceinfo.productName),
'"product":'+JSON.stringify(t.product),
'"serial":'+JSON.stringify(serial),
'"status":'+JSON.stringify(statename(t)),
'"type":'+JSON.stringify(typename(t)),
].join(',') + '}';
} else {
return [
serial+'\t'+statename(t),
t.devpath||'',
t.product?'product:'+t.product.replace(/\s+/,'_'):'',
t.model?'model:'+t.model.replace(/\s+/,'_'):'',
t.device?'device:'+t.device.replace(/\s+/,'_'):''
].join(' ');
}
}
var list_transports = exports.list_transports = function(format) {
return transport_list.map(function(t) {
return format_transport(t, format);
}).join('\n')+'\n';
}
var update_transports = exports.update_transports = function() {
write_transports_to_trackers(_device_trackers.normal);
write_transports_to_trackers(_device_trackers.extended, null, true);
}
var readx_with_data = exports.readx_with_data = function(fd, cb) {
readx(fd, 4, function(err, buf) {
if (err) return cb(err);
var dlen = buf.intFromHex();
if (dlen < 0 || dlen > 0xffff)
return cb({msg:'Invalid data len: ' + dlen});
readx(fd, dlen, function(err, buf) {
if (err) return cb(err);
return cb(null, buf);
});
});
}
var readx = exports.readx = function(fd, len, cb) {
D('readx: fd:%o wanted=%d', fd, len);
fd.readbytes(len, function(err, buf) {
if (err) return cb(err);
cb(err, buf);
});
}
var writex = exports.writex = function(fd, bytes, len) {
if (typeof(bytes) === 'string') {
var buf = new Uint8Array(bytes.length);
for (var i=0; i < bytes.length; i++)
buf[i] = bytes.charCodeAt(i);
bytes = buf;
}
if (typeof(len) !== 'number')
len = bytes.byteLength;
D('writex: fd:%o writing=%d', fd, len);
fd.writebytes(bytes.subarray(0,len));
}
var writex_with_data = exports.writex_with_data = function(fd, data, len) {
if (typeof(len) === 'undefined');
len = data.byteLength||data.length||0;
writex(fd, intToHex(len, 4));
writex(fd, data, len);
}
var _device_trackers = {
normal:[],
extended:[],
}
var add_device_tracker = exports.add_device_tracker = function(fd, extended) {
_device_trackers[extended?'extended':'normal'].push(fd);
write_transports_to_trackers([fd], null, extended);
readtracker(fd, extended);
D('Device tracker added. Trackers: %o', _device_trackers);
function readtracker(fd, extended) {
chrome.socket.read(fd.n, function(readInfo) {
if (chrome.runtime.lastError || readInfo.resultCode < 0) {
remove_from_list(_device_trackers[extended?'extended':'normal'], fd);
D('Device tracker socket read failed - closing. Trackers: %o', _device_trackers);
fd.close();
return;
}
D('Ignoring data read from device tracker socket');
readtracker(fd, extended);
});
}
}
var write_transports_to_trackers = exports.write_transports_to_trackers = function(fds, transports, extended) {
if (!fds || !fds.length)
return;
if (!transports) {
return write_transports_to_trackers(fds, list_transports(extended?'extended':''), extended);
}
D('Writing transports: %s', transports);
fds.slice().forEach(function(fd) {
writex_with_data(fd, str2u8arr(transports));
});
}

631
src/util.js Normal file
View File

@@ -0,0 +1,631 @@
const crypto = require('crypto');
var nofn=function(){};
var D=exports.D=console.log.bind(console);
var E=exports.E=console.error.bind(console);
var W=exports.W=console.warn.bind(console);
var DD=nofn,cl=D,printf=D;
var print_jdwp_data = nofn;// _print_jdwp_data;
var print_packet = nofn;//_print_packet;
Array.first = function(arr, fn, defaultvalue) {
var idx = Array.indexOfFirst(arr, fn);
return idx < 0 ? defaultvalue : arr[idx];
}
Array.indexOfFirst = function(arr, fn) {
if (!Array.isArray(arr)) return -1;
for (var i=0; i < arr.length; i++)
if (fn(arr[i], i, arr))
return i;
return -1;
}
var isEmptyObject = exports.isEmptyObject = function(o) {
return typeof(o)==='object' && !Object.keys(o).length;
}
var leftpad = exports.leftpad = function(char, len, s) {
while (s.length < len)
s = char + s;
return s;
}
var intToHex = exports.intToHex = function(i, minlen) {
var s = i.toString(16);
if (minlen) s = leftpad('0', minlen, s);
return s;
}
var intFromHex = exports.intFromHex = function(s, maxlen, defaultvalue) {
s = s.slice(0, maxlen);
if (!/^[0-9a-fA-F]+$/.test(s)) return defaultvalue;
return parseInt(s, 16);
}
var fdcache = [];
var index_of_file_fdn = function(n) {
if (n <= 0) return -1;
for (var i=0; i < fdcache.length; i++) {
if (fdcache[i] && fdcache[i].n === n)
return i;
}
return -1;
}
var get_file_fd_from_fdn = function(n) {
var idx = index_of_file_fdn(n);
if (idx < 0) return null;
return fdcache[idx];
}
var remove_fd_from_cache = function(fd) {
if (!fd) return;
var idx = index_of_file_fdn(fd.n);
if (idx>=0) fdcache.splice(idx, 1);
}
// add an offset so we don't conflict with tcp socketIds
var min_fd_num = 100000;
var _new_fd_count = 0;
var new_fd = this.new_fd = function(name, raw) {
var rwpipe = raw ? new Uint8Array(0) : [];
var fd = {
name: name,
n: min_fd_num + (++_new_fd_count),
raw: !!raw,
readpipe:rwpipe,
writepipe:rwpipe,
reader:null,
readerlen:0,
kickingreader:false,
total:{read:0,written:0},
duplex: null,
closed:'',
read:function(cb) {
if (this.raw)
throw 'Cannot read from raw fd';
if (this.reader && this.reader !== cb)
throw 'multiple readers?';
this.reader = cb;
this._kickreader();
},
write:function(data) {
if (this.closed) {
D('Ignoring attempt to write to closed file: %o', this);
return;
}
if (this.raw) {
D('Ignoring attempt to write object to raw file: %o', this);
return;
}
this.writepipe.push(data);
if (this.duplex) {
this.duplex._kickreader();
}
},
readbytes:function(len, cb) {
if (!this.raw)
throw 'Cannot readbytes from non-raw fd';
if (this.reader)
throw 'multiple readers?';
this.reader = cb;
this.readerlen = len;
this._kickreader();
},
writebytes:function(buffer) {
if (this.closed) {
D('Ignoring attempt to write to closed file: %o', this);
return;
}
if (!this.raw) {
D('Ignoring attempt to write bytes to non-raw file: %o', this);
return;
}
if (!buffer || !buffer.byteLength) {
// kick the reader when writing 0 bytes
this._kickreaders();
return;
}
this.total.written += buffer.byteLength;
var newbuf = new Uint8Array(this.writepipe.byteLength + buffer.byteLength);
newbuf.set(this.writepipe);
newbuf.set(buffer, this.writepipe.byteLength);
this.writepipe = newbuf;
if (this.duplex)
this.duplex.readpipe = newbuf;
else
this.readpipe = newbuf;
D('new buffer size: %d (fd:%d)',this.writepipe.byteLength, this.n);
this._kickreaders();
},
cancelread:function(flushfirst) {
if (flushfirst)
this.flush();
this.reader = null;
this.readerlen = 0;
},
write_eof:function() {
this.flush();
// eof is only relevant for read-until-close readers
if (this.raw && this.reader && this.readerlen === -1) {
this.reader({err:'eof'});
}
},
flush:function() {
this._doread();
},
close:function() {
if (this.closed)
return;
console.trace('Closing file %d: %o', this.n, this);
this.closed = 'closed';
if (this.duplex)
this.duplex.close();
// last kick to finish off any read-until-close readers
this._kickreaders();
// remove this entry from the cache
remove_fd_from_cache(this);
},
_kickreaders:function() {
if (this.duplex)
this.duplex._kickreader();
else
this._kickreader();
},
_kickreader:function() {
if (!this.reader) return;
if (this.kickingreader) return;
var t = this;
t.kickingreader = setTimeout(function() {
t.kickingreader = false;
t._doreadcheckclose();
}, 0);
},
_doreadcheckclose:function() {
var cs = this.closed;
this._doread();
if (cs) {
// they've had one last read - no more
var rucreader = this.readerlen === -1;
var rucreadercb = this.reader;
this.reader = null;
this.readerlen = 0;
if (rucreader && rucreadercb) {
// terminate the read-until-close reader
D('terminating ruc reader. fd: %o',this);
rucreadercb({err:'File closed'});
}
}
},
_doread:function() {
if (this.raw) {
if (!this.reader) return;
if (this.readerlen > this.readpipe.byteLength) return;
if (this.readerlen && !this.readpipe.byteLength) return;
var cb = this.reader, len = this.readerlen;
this.reader = null, this.readerlen = 0;
var data;
if (len) {
var readlen = len>0?len:this.readpipe.byteLength;
data = this.readpipe.subarray(0, readlen);
this.readpipe = this.readpipe.subarray(readlen);
if (this.duplex)
this.duplex.writepipe = this.readpipe;
else
this.writepipe = this.readpipe;
this.total.read += readlen;
} else {
data = new Uint8Array(0);
}
data.asString = function() {
return uint8ArrayToString(this);
};
data.intFromHex = function(len) {
len = len||this.byteLength;
var x = this.asString().slice(0,len);
if (!/^[0-9a-fA-F]+/.test(x)) return -1;
return parseInt(x, 16);
}
cb(null, data);
if (len < 0) {
// reset the reader
this.readbytes(len, cb);
}
return;
}
if (this.reader && this.readpipe.length) {
var cb = this.reader;
this.reader = null;
cb(this.readpipe.shift());
}
}
}
fdcache.push(fd);
return fd;
}
var intToCharString = function(n) {
return String.fromCharCode(
(n>>0)&255,
(n>>8)&255,
(n>>16)&255,
(n>>24)&255
);
}
var stringToUint8Array = function(s) {
var x = new Uint8Array(s.length);
for (var i=0; i < s.length; i++)
x[i] = s.charCodeAt(i);
return x;
}
var uint8ArrayToString = function(a) {
var s = new Array(a.byteLength);
for (var i=0; i < a.byteLength; i++)
s[i] = a[i];
return String.fromCharCode.apply(String, s);
}
// asynchronous array iterater
var iterate = function(arr, o) {
var isrange = typeof(arr)==='number';
if (isrange)
arr = { length: arr<0?0:arr };
var x = {
value:arr,
isrange:isrange,
first:o.first||nofn,
each:o.each||(function() { this.next(); }),
last:o.last||nofn,
success:o.success||nofn,
error:o.error||nofn,
complete:o.complete||nofn,
_idx:0,
_donefirst:false,
_donelast:false,
abort:function(err) {
this.error(err);
this.complete();
return;
},
finish:function(res) {
// finish early
if (typeof(res)!=='undefined') this.result = res;
this.success(res||this.result);
this.complete();
return;
},
iteratefirst:function() {
if (!this.value.length) {
this.finish();
return;
}
this.first(this.value[this._idx],this._idx,this);
this.each(this.value[this._idx],this._idx,this);
},
iteratenext:function() {
if (++this._idx >= this.value.length) {
this.last(this.value[this._idx],this._idx,this);
this.finish();
return;
}
this.each(this.value[this._idx],this._idx,this);
},
next:function() {
var t = this;
setTimeout(function() {
t.iteratenext();
},0);
},
nextorabort:function(err) {
if (err) this.abort(err);
else this.next();
},
};
setTimeout(function() { x.iteratefirst(); }, 0);
return x;
};
var iterate_repeat = function(arr, count, o, j) {
iterate(arr, {
each: function(value, i, it) {
o.each(value, i, j||0, it);
},
success: function() {
if (!--count) {
o.success && o.success();
o.complete && o.complete();
return;
}
iterate_repeat(arr, count, o, (j||0)+1);
},
error:function(err) {
o.error && o.error();
o.complete && o.complete();
}
});
}
/**
* Convert from an ArrayBuffer to a string.
* @param {ArrayBuffer} buffer The array buffer to convert.
* @return {string} The textual representation of the array.
*/
var arrayBufferToString = exports.arrayBufferToString = function(buffer) {
var array = new Uint8Array(buffer);
var str = '';
for (var i = 0; i < array.length; ++i) {
str += String.fromCharCode(array[i]);
}
return str;
};
/**
* Convert from an UTF-8 array to UTF-8 string.
* @param {array} UTF-8 array
* @return {string} UTF-8 string
*/
var ary2utf8 = (function() {
var patterns = [
{pattern: '0xxxxxxx', bytes: 1},
{pattern: '110xxxxx', bytes: 2},
{pattern: '1110xxxx', bytes: 3},
{pattern: '11110xxx', bytes: 4},
{pattern: '111110xx', bytes: 5},
{pattern: '1111110x', bytes: 6}
];
patterns.forEach(function(item) {
item.header = item.pattern.replace(/[^10]/g, '');
item.pattern01 = item.pattern.replace(/[^10]/g, '0');
item.pattern01 = parseInt(item.pattern01, 2);
item.mask_length = item.header.length;
item.data_length = 8 - item.header.length;
var mask = '';
for (var i = 0, len = item.mask_length; i < len; i++) {
mask += '1';
}
for (var i = 0, len = item.data_length; i < len; i++) {
mask += '0';
}
item.mask = mask;
item.mask = parseInt(item.mask, 2);
});
return function(ary) {
var codes = [];
var cur = 0;
while(cur < ary.length) {
var first = ary[cur];
var pattern = null;
for (var i = 0, len = patterns.length; i < len; i++) {
if ((first & patterns[i].mask) == patterns[i].pattern01) {
pattern = patterns[i];
break;
}
}
if (pattern == null) {
throw 'utf-8 decode error';
}
var rest = ary.slice(cur + 1, cur + pattern.bytes);
cur += pattern.bytes;
var code = '';
code += ('00000000' + (first & (255 ^ pattern.mask)).toString(2)).slice(-pattern.data_length);
for (var i = 0, len = rest.length; i < len; i++) {
code += ('00000000' + (rest[i] & parseInt('111111', 2)).toString(2)).slice(-6);
}
codes.push(parseInt(code, 2));
}
return String.fromCharCode.apply(null, codes);
};
})();
/**
* Convert from an UTF-8 string to UTF-8 array.
* @param {string} UTF-8 string
* @return {array} UTF-8 array
*/
var utf82ary = (function() {
var patterns = [
{pattern: '0xxxxxxx', bytes: 1},
{pattern: '110xxxxx', bytes: 2},
{pattern: '1110xxxx', bytes: 3},
{pattern: '11110xxx', bytes: 4},
{pattern: '111110xx', bytes: 5},
{pattern: '1111110x', bytes: 6}
];
patterns.forEach(function(item) {
item.header = item.pattern.replace(/[^10]/g, '');
item.mask_length = item.header.length;
item.data_length = 8 - item.header.length;
item.max_bit_length = (item.bytes - 1) * 6 + item.data_length;
});
var code2utf8array = function(code) {
var pattern = null;
var code01 = code.toString(2);
for (var i = 0, len = patterns.length; i < len; i++) {
if (code01.length <= patterns[i].max_bit_length) {
pattern = patterns[i];
break;
}
}
if (pattern == null) {
throw 'utf-8 encode error';
}
var ary = [];
for (var i = 0, len = pattern.bytes - 1; i < len; i++) {
ary.unshift(parseInt('10' + ('000000' + code01.slice(-6)).slice(-6), 2));
code01 = code01.slice(0, -6);
}
ary.unshift(parseInt(pattern.header + ('00000000' + code01).slice(-pattern.data_length), 2));
return ary;
};
return function(str) {
var codes = [];
for (var i = 0, len = str.length; i < len; i++) {
var code = str.charCodeAt(i);
Array.prototype.push.apply(codes, code2utf8array(code));
}
return codes;
};
})();
/**
* Convert a string to an ArrayBuffer.
* @param {string} string The string to convert.
* @return {ArrayBuffer} An array buffer whose bytes correspond to the string.
*/
var stringToArrayBuffer = exports.stringToArrayBuffer = function(string) {
var buffer = new ArrayBuffer(string.length);
var bufferView = new Uint8Array(buffer);
for (var i = 0; i < string.length; i++) {
bufferView[i] = string.charCodeAt(i);
}
return buffer;
};
var str2ab = exports.str2ab = stringToArrayBuffer;
var ab2str = exports.ab2str = arrayBufferToString;
var str2u8arr = exports.str2u8arr = function(s) {
return new Uint8Array(str2ab(s));
}
exports.getutf8bytes = function(str) {
var utf8 = [];
for (var i=0; i < str.length; i++) {
var charcode = str.charCodeAt(i);
if (charcode < 0x80) utf8.push(charcode);
else if (charcode < 0x800) {
utf8.push(0xc0 | (charcode >> 6),
0x80 | (charcode & 0x3f));
}
else if (charcode < 0xd800 || charcode >= 0xe000) {
utf8.push(0xe0 | (charcode >> 12),
0x80 | ((charcode>>6) & 0x3f),
0x80 | (charcode & 0x3f));
}
// surrogate pair
else {
i++;
// UTF-16 encodes 0x10000-0x10FFFF by
// subtracting 0x10000 and splitting the
// 20 bits of 0x0-0xFFFFF into two halves
charcode = 0x10000 + (((charcode & 0x3ff)<<10)
| (str.charCodeAt(i) & 0x3ff));
utf8.push(0xf0 | (charcode >>18),
0x80 | ((charcode>>12) & 0x3f),
0x80 | ((charcode>>6) & 0x3f),
0x80 | (charcode & 0x3f));
}
}
return utf8;
}
exports.fromutf8bytes = function(array) {
var out, i, len, c;
var char2, char3;
out = "";
len = array.length;
i = 0;
while(i < len) {
c = array[i++];
switch(c >> 4)
{
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12: case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
}
}
return out;
}
exports.arraybuffer_concat = function() {
var bufs=[], total=0;
for (var i=0; i < arguments.length; i++) {
var a = arguments[i];
if (!a || !a.byteLength) continue;
bufs.push(a);
total += a.byteLength;
}
switch (bufs.length) {
case 0: return new Uint8Array(0);
case 1: return new Uint8Array(bufs[0]);
}
var res = new Uint8Array(total);
for (var i=0, j=0; i < bufs.length; i++) {
res.set(bufs[i], j);
j += bufs[i].byteLength;
}
return res;
}
exports.remove_from_list = function(arr, item, searchfn) {
if (!searchfn) searchfn = function(a,b) { return a===b; };
for (var i=0; i < arr.length; i++) {
var found = searchfn(arr[i], item);
if (found) {
return {
item: arr.splice(i, 1)[0],
index: i,
}
}
}
D('Object %o not removed from list %o', item, arr);
}
exports.dumparr = function(arr, offset, count) {
offset=offset||0;
count = count||(count===0?0:arr.length);
if (count > arr.length-offset)
count = arr.length-offset;
var s = '';
while (count--) {
s += ' '+('00'+arr[offset++].toString(16)).slice(-2);
}
return s.slice(1);
}
exports.btoa = function(arr) {
return new Buffer(arr,'binary').toString('base64');
}
exports.atob = function(base64) {
return new Buffer(base64, 'base64').toString('binary');
}

440
src/wsproxy.js Normal file
View File

@@ -0,0 +1,440 @@
const WebSocketServer = require('./minwebsocket').WebSocketServer;
const { atob, btoa, ab2str, str2u8arr, arrayBufferToString, intFromHex, intToHex, D,E,W, get_file_fd_from_fdn } = require('./util');
const { connect_forward_listener } = require('./services');
const { get_socket_fd_from_fdn, socket_loopback_client } = require('./sockets');
const { readx, writex } = require('./transport');
var dprintfln = ()=>{};//D;
WebSocketServer.DEFAULT_ADB_PORT = 5037;
var proxy = {
Server: function(port, adbport) {
// Listen for websocket connections.
var wsServer = new WebSocketServer(port);
wsServer.adbport = adbport;
wsServer.setADBPort = function(port) {
if (typeof(port) === 'undefined')
return this.adbport = WebSocketServer.DEFAULT_ADB_PORT;
return this.adbport = port;
}
// A list of connected websockets.
var connectedSockets = [];
function indexof_connected_socket(socketinfo) {
if (!socketinfo) return -1;
for (var i=0; i < connectedSockets.length; i++)
if (connectedSockets[i] === socketinfo)
return i;
return -1;
}
wsServer.onconnection = function(req) {
var ws = req.accept();
var si = {
wsServer: wsServer,
ws: ws,
fn: check_client_version,
fdarr: [],
};
connectedSockets.push(si);
ws.onmessage = function(e) {
si.fn(si, e);
};
// When a socket is closed, remove it from the list of connected sockets.
ws.onclose = function() {
while (si.fdarr.length) {
si.fdarr.pop().close();
}
var idx = indexof_connected_socket(si);
if (idx>=0) connectedSockets.splice(idx, 1);
else D('Cannot find disconnected socket in connectedSockets');
};
return true;
};
D('WebSocketServer started. Listening on port: %d', port);
return wsServer;
}
}
var check_client_version = function(si, e) {
if (e.data !== 'vscadb client version 1') {
D('Wrong client version: ', e.data);
return end_of_connection(si);
}
si.fn = handle_proxy_command;
si.ws.send('vscadb proxy version 1');
}
var end_of_connection = function(si) {
if (!si || !si.ws) return;
si.ws.close();
}
var handle_proxy_command = function(si, e) {
if (!e || !e.data || e.data.length<2) return end_of_connection(si);
var cmd = e.data.slice(0,2);
var fn = proxy_command_fns[cmd];
if (!fn) {
E('Unknown command: %s', e.data);
return end_of_connection(si);
}
fn(si, e);
}
function end_of_command(si, respfmt) {
if (!si || !si.ws || !respfmt) return;
// format the response - we allow %s, %d and %xX
var response = respfmt;
var fmtidx = 0;
for (var i=2; i < arguments.length; i++) {
var fmt = response.slice(fmtidx).match(/%([sdxX])/);
if (!fmt) break;
response = [response.slice(0,fmt.index),arguments[i],response.slice(fmt.index+2)];
switch(fmt[1]) {
case 'x': response[1] = response[1].toString(16).toLowerCase(); break;
case 'X': response[1] = response[1].toString(16).toUpperCase(); break;
}
response = response.join('');
fmtidx = fmt.index + arguments[i].length;
}
si.ws.send(response);
}
function readsckt(fd, n, cb) {
readx(fd, n, cb);
}
function write_adb_command(fd, cmd) {
dprintfln('write_adb_command: %s',cmd);
// write length in hex first
writex(fd, intToHex(cmd.length, 4));
// then the command
writex(fd, cmd);
}
function read_adb_status(adbfd, extra, cb) {
// read back the status
readsckt(adbfd, 4+extra, function(err, data) {
if (err) return cb();
var status = ab2str(data);
dprintfln("adb status: %s", status);
cb(status);
});
}
function read_adb_reply(adbfd, b64encode, cb) {
// read reply length
readsckt(adbfd, 4, function(err, data) {
if (err) return cb();
var n = intFromHex(ab2str(data));
dprintfln("adb expected reply: %d bytes", n);
// read reply
readsckt(adbfd, n, function(err, data) {
if (err) return cb();
var n = data.byteLength;
dprintfln("adb reply: %d bytes", n);
var response = ab2str(data);
if (n === 0) response = '\n'; // always send something
dprintfln("%s",response);
if (b64encode) response = btoa(response);
return cb(response);
});
});
}
const min_fd_num = 1000;
var fdn_to_fd = function(n) {
var fd;
if (n >= min_fd_num) fd = get_file_fd_from_fdn(n);
else fd = get_socket_fd_from_fdn(n);
if (!fd) throw new Error('Invalid file descriptor number: '+n);
return fd;
}
var retryread = function(fd, len, cb) {
fd.readbytes(len, cb);
}
var retryreadfill = function(fd, len, cb) {
var buf = new Uint8Array(len);
var totalread = 0;
var readmore = function(amount) {
fd.readbytes(amount, function(err, data) {
if (err) return cb(err);
buf.set(data, totalread);
totalread += data.byteLength;
var diff = len - totalread;
if (diff > 0) return readmore(diff);
cb(err, buf);
});
};
readmore(len);
}
var be2le = function(buf) {
var x = new Uint8Array(buf);
var a = x[0];
a = (a<<8)+x[1];
a = (a<<8)+x[2];
a = (a<<8)+x[3];
return a;
}
var jdwpReplyMonitor = function(fd, si, packets) {
if (!packets) {
packets = 0;
dprintfln("jdwpReplyMonitor thread started. jdwpfd:%d.", fd.n);
}
//dprintfln("WAITING FOR JDWP DATA....");
//int* pjdwpdatalen = (int*)&buffer[0];
//*pjdwpdatalen=0;
retryread(fd, 4, function(err, data) {
if (err) return terminate();
var m = data.byteLength;
if (m != 4) {
dprintfln("rj %d len read", m);
return terminate();
}
m = be2le(data.buffer.slice(0,4));
//dprintfln("STARTING JDWP DATA: %.8x....", m);
var lenstr = arrayBufferToString(data.buffer);
retryreadfill(fd, m-4, function(err, data) {
if (err) return terminate();
var n = data.byteLength + 4;
if (n != m) {
dprintfln("rj read incomplete %d/%d", (n+4),m);
return terminate();
}
//dprintfln("GOT JDWP DATA....");
dprintfln("rj encoding %d bytes", n);
var response = "rj ok ";
response += btoa(lenstr + arrayBufferToString(data.buffer));
si.ws.send(response);
//dprintfln("SENT JDWP REPLY....");
packets++;
jdwpReplyMonitor(fd, si, packets);
});
});
function terminate() {
// try and send a final event reply indicating the VM has disconnected
var vmdisconnect = [
0,0,0,17, // len
100,100,100,100, // id
0, //flags
0x40,0x64, // errcode = composite event
0, //suspend
0,0,0,1, // eventcount
100, // eventkind=VM_DISCONNECTED
];
var response = "rj ok ";
response += btoa(ab2str(new Uint8Array(vmdisconnect)));
si.ws.send(response);
dprintfln("jdwpReplyMonitor thread finished. Sent:%d packets.", packets);
}
}
var stdoutMonitor = function(fd, si, packets) {
if (!packets) {
packets = 0;
dprintfln("stdoutMonitor thread started. jdwpfd:%d, wsfd:%o.", fd.n, si);
}
retryread(fd, function(err, data) {
if (err) return terminate();
var response = 'so ok '+btoa(ab2str(new Uint8Array(data)));
si.ws.send(response);
packets++;
stdoutMonitor(fd, si, packets);
});
function terminate() {
// send a unique terminating string to indicate the stdout monitor has finished
var eoso = "eoso:d10d9798-1351-11e5-bdd9-5b316631f026";
var response = "so ok " + btoa(eoso);
si.ws.send(response);
dprintfln("stdoutMonitor thread finished. Sent:%d packets.", packets);
}
}
// commands are:
// cn - create adb socket
// cp <port> - create custom-port socket
// wa <fd> <base64cmd> - write_adb_command
// rs <fd> [extra] - read_adb_status
// ra <fd> - read_adb_reply
// rj <fd> - read jdwp-formatted reply
// rx <fd> <len> - read raw data from adb socket
// wx <fd> <base64data> - write raw data to adb socket
// dc <fd|all> - disconnect adb sockets
var proxy_command_fns = {
cn:function(si, e) {
// create adb socket
socket_loopback_client(si.wsServer.adbport, function(fd) {
if (!fd) {
return end_of_command(si, 'cn error connection failed');
}
si.fdarr.push(fd);
return end_of_command(si, 'cn ok %d', fd.n);
});
},
cp:function(si, e) {
var x = e.data.split(' '), port;
port = parseInt(x[1], 10);
connect_forward_listener(port, {create:true}, function(sfd) {
return end_of_command(si, 'cp ok %d', sfd.n);
});
},
wa:function(si, e) {
var x = e.data.split(' '), fd, buffer;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
buffer = atob(x[2]);
} catch(err) {
return end_of_command(si, 'wa error wrong parameters');
}
write_adb_command(fd, buffer);
return end_of_command(si, 'wa ok');
},
// rs fd [extra]
rs:function(si, e) {
var x = e.data.split(' '), fd, extra;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
// optional additional bytes - used for sync-responses which
// send status+length as 8 bytes
extra = parseInt(atob(x[2]||'MA=='));
} catch(err) {
return end_of_command(si, 'rs error wrong parameters');
}
read_adb_status(fd, extra, function(status) {
return end_of_command(si, 'rs ok %s', status||'');
})
},
ra:function(si, e) {
var x = e.data.split(' '), fd;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
} catch(err) {
return end_of_command(si, 'ra error wrong parameters');
}
read_adb_reply(fd, true, function(b64adbreply) {
if (!b64adbreply) {
return end_of_command('ra error read failed');
}
return end_of_command(si, 'ra ok %s', b64adbreply);
});
},
rj:function(si, e) {
var x = e.data.split(' '), fd;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
} catch(err) {
return end_of_command(si, 'rj error wrong parameters');
}
jdwpReplyMonitor(fd, si);
return end_of_command(si, 'rj ok');
},
rx:function(si, e) {
var x = e.data.split(' '), fd;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
} catch(err) {
return end_of_command(si, 'rx error wrong parameters');
}
if (fd.isSocket) {
fd.readbytes(doneread);
} else {
fd.readbytes(fd.readpipe.byteLength, doneread);
}
function doneread(err, data) {
if (err) {
return end_of_command(si, 'rx ok nomore');
}
end_of_command(si, 'rx ok ' + btoa(ab2str(data)));
}
},
so:function(si, e) {
var x = e.data.split(' '), fd;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
} catch(err) {
return end_of_command(si, 'so error wrong parameters');
}
stdoutMonitor(fd, si);
return end_of_command(si, 'so ok');
},
wx:function(si, e) {
var x = e.data.split(' '), fd, buffer;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
buffer = atob(x[2]);
} catch(err) {
return end_of_command(si, 'wx error wrong parameters');
}
fd.writebytes(str2u8arr(buffer), function(err) {
if (err)
return end_of_command(si, 'wx error device write failed');
end_of_command(si, 'wx ok');
});
},
dc:function(si, e) {
var x = e.data.split(' ');
if (x[1] === 'all') {
while (si.fdarr.length) {
si.fdarr.pop().close();
}
return end_of_command(si, 'dc ok');
}
var n = parseInt(x[1],10);
for (var i=0; i < si.fdarr.length; i++) {
if (si.fdarr[i].n === n) {
var fd = si.fdarr.splice(i,1)[0];
fd.close();
break;
}
}
return end_of_command(si, 'dc ok');
}
}
exports.proxy = proxy;