Permalink
Browse files

Merge branch 'master' into patch-1

  • Loading branch information...
legastero committed Jan 22, 2019
2 parents 1c9a6a3 + 15a0535 commit e1f7934c5c252da1004064a6eef1a86a4ac68a87
@@ -10,6 +10,7 @@
- Moved `xmpp-jid` implementation back into `stanza.io`, obsoleting `xmpp-jid`.
- Use `ws` module instead of `faye-websocket`.
- Dropped support of old, pre-RFC XMPP-over-WebSocket.
- Moved `jingle` implementation back into `stanza.io`.

## 9.1.0 -> 9.2.0

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -94,5 +94,6 @@
- [XEP-0339: Source-Specific Media Attributes in Jingle](http://xmpp.org/extensions/xep-0339.html)
- [XEP-0343: Use of DTLS/SCTP in Jingle ICE-UDP](http://xmpp.org/extensions/xep-0343.html)
- [XEP-0352: Client State Indication](http://xmpp.org/extensions/xep-0352.html)
- [XEP-0357: Push Notifications](https://xmpp.org/extensions/xep-0357.html)
- [XEP-0372: References](http://xmpp.org/extensions/xep-0372.html)

@@ -1,7 +1,7 @@
{
"name": "stanza.io",
"description": "Modern XMPP in the browser, with a JSON API",
"version": "9.3.0",
"version": "10.0.3",
"author": "Lance Stout <lancestout@gmail.com>",
"browser": {
"node-stringprep": false,
@@ -15,10 +15,9 @@
"dependencies": {
"async": "^2.5.0",
"cross-fetch": "^3.0.0",
"iana-hashes": "^1.0.2",
"jingle": "^3.0.0",
"iana-hashes": "^1.1.0",
"jxt": "^4.0.0",
"randombytes": "^2.0.6",
"sdp": "^2.9.0",
"tslib": "^1.9.3",
"uuid": "^3.0.1",
"wildemitter": "^1.0.1",
@@ -29,7 +28,7 @@
"prettier": "^1.14.3",
"pretty-quick": "^1.8.0",
"rimraf": "^2.6.2",
"rollup": "^0.68.0",
"rollup": "^1.0.0",
"rollup-plugin-node-resolve": "^4.0.0",
"string-replace-loader": "^2.1.1",
"tap-spec": "^5.0.0",
@@ -78,7 +77,7 @@
"compile:rollup": "rollup -c rollup.config.js",
"compile:webpack": "webpack --mode production",
"lint": "tslint -p .",
"test": "ts-node test/index.js | tap-spec",
"test": "ts-node --files test/index.js | tap-spec",
"validate": "npm ls"
}
}
@@ -10,10 +10,11 @@ export default {
external: [
'async',
'cross-fetch',
'events',
'iana-hashes',
'jingle',
'jxt',
'randombytes',
'sdp',
'tslib',
'uuid',
'wildemitter',
@@ -0,0 +1,299 @@
import { EventEmitter } from 'events';
import * as Hashes from 'iana-hashes';

import ICESession from './ICESession';
import { importFromSDP, exportToSDP } from './lib/Intermediate';
import { convertIntermediateToRequest, convertRequestToIntermediate } from './lib/Protocol';

export class Sender extends EventEmitter {
constructor(opts = {}) {
super();

this.config = {
chunkSize: 16384,
hash: 'sha-1',
pacing: 0,
...opts
};

this.file = null;
this.channel = null;
this.hash = Hashes.createHash(this.config.hash);
}

send(file, channel) {
if (this.file && this.channel) {
return;
}

this.file = file;
this.channel = channel;
this.channel.binaryType = 'arraybuffer';

const usePoll = typeof channel.bufferedAmountLowThreshold !== 'number';

const sliceFile = (offset = 0) => {
const reader = new FileReader();
reader.onload = () => {
const data = new Uint8Array(reader.result);
this.channel.send(data);
this.hash.update(data);

this.emit('progress', offset, file.size, data);

if (file.size > offset + this.config.chunkSize) {
if (usePoll) {
setTimeout(sliceFile, this.config.pacing, offset + this.config.chunkSize);
} else if (channel.bufferedAmount <= channel.bufferedAmountLowThreshold) {
setTimeout(sliceFile, 0, offset + this.config.chunkSize);
} else {
// wait for bufferedAmountLow to fire
}
} else {
this.emit('progress', file.size, file.size, null);
this.emit('sentFile', {
algo: this.config.hash,
hash: this.hash.digest('hex')
});
}
};

const slice = file.slice(offset, offset + this.config.chunkSize);
reader.readAsArrayBuffer(slice);
};

if (!usePoll) {
channel.bufferedAmountLowThreshold = 8 * this.config.chunkSize;
channel.addEventListener('bufferedamountlow', sliceFile);
}
setTimeout(sliceFile, 0, 0);
}
}

export class Receiver extends EventEmitter {
constructor(opts = {}) {
super();

this.config = {
hash: 'sha-1',
...opts
};

this.receiveBuffer = [];
this.received = 0;
this.metadata = {};
this.channel = null;
this.hash = Hashes.createHash(this.config.hash);
}

receive(metadata, channel) {
if (metadata) {
this.metadata = metadata;
}

this.channel = channel;
this.channel.binaryType = 'arraybuffer';

this.channel.onmessage = e => {
const len = e.data.byteLength;
this.received += len;
this.receiveBuffer.push(e.data);
if (e.data) {
this.hash.update(new Uint8Array(e.data));
}

this.emit('progress', this.received, this.metadata.size, e.data);
if (this.received === this.metadata.size) {
this.metadata.actualhash = this.hash.digest('hex');

this.emit('receivedFile', new Blob(this.receiveBuffer), this.metadata);
this.receiveBuffer = [];
} else if (this.received > this.metadata.size) {
// FIXME
console.error('received more than expected, discarding...');
this.receiveBuffer = []; // just discard...
}
};
}
}

export default class FileTransferSession extends ICESession {
constructor(opts) {
super(opts);

this.sender = null;
this.receiver = null;
this.file = null;
}

start(file, next) {
next = next || (() => undefined);

this.state = 'pending';
this.role = 'initiator';

this.file = file;

this.sender = new Sender();
this.sender.on('progress', (sent, size) => {
this._log('info', 'Send progress ' + sent + '/' + size);
});
this.sender.on('sentFile', meta => {
this._log('info', 'Sent file', meta.name);

this.send('description-info', {
contents: [
{
application: {
applicationType: 'filetransfer',
offer: {
hash: {
algo: meta.algo,
value: meta.hash
}
}
},
creator: 'initiator',
name: this.contentName
}
]
});

this.emit('sentFile', this, meta);
});

this.channel = this.pc.createDataChannel('filetransfer', {
ordered: true
});
this.channel.onopen = () => {
this.sender.send(this.file, this.channel);
};

this.pc
.createOffer({
offerToReceiveAudio: false,
offerToReceiveVideo: false
})
.then(offer => {
const json = importFromSDP(offer.sdp);
const jingle = convertIntermediateToRequest(json, this.role);

this.contentName = jingle.contents[0].name;

jingle.sessionId = this.sid;
jingle.action = 'session-initate';
jingle.contents[0].application = {
applicationType: 'filetransfer',
offer: {
date: file.lastModifiedDate,
hash: {
algo: 'sha-1',
value: ''
},
name: file.name,
size: file.size
}
};

this.send('session-initiate', jingle);

return this.pc.setLocalDescription(offer).then(() => next());
})
.catch(err => {
console.error(err);
this._log('error', 'Could not create WebRTC offer', err);
return this.end('failed-application', true);
});
}

accept(next) {
this._log('info', 'Accepted incoming session');

this.role = 'responder';
this.state = 'active';

next = next || (() => undefined);

this.pc
.createAnswer()
.then(answer => {
const json = importFromSDP(answer.sdp);
const jingle = convertIntermediateToRequest(json, this.role);
jingle.sessionId = this.sid;
jingle.action = 'session-accept';
jingle.contents.forEach(content => {
content.creator = 'initiator';
});
this.contentName = jingle.contents[0].name;
this.send('session-accept', jingle);
return this.pc.setLocalDescription(answer).then(() => next());
})
.catch(err => {
console.error(err);
this._log('error', 'Could not create WebRTC answer', err);
this.end('failed-application');
});
}

onSessionInitiate(changes, cb) {
this._log('info', 'Initiating incoming session');

this.role = 'responder';
this.state = 'pending';

const json = convertRequestToIntermediate(changes, this.peerRole);
const sdp = exportToSDP(json);
const desc = changes.contents[0].application;

this.receiver = new Receiver({ hash: desc.offer.hash.algo });
this.receiver.on('progress', (received, size) => {
this._log('info', 'Receive progress ' + received + '/' + size);
});
this.receiver.on('receivedFile', file => {
this.receivedFile = file;
this._maybeReceivedFile();
});
this.receiver.metadata = desc.offer;
this.pc.addEventListener('datachannel', e => {
this.channel = e.channel;
this.receiver.receive(null, e.channel);
});

this.pc
.setRemoteDescription({ type: 'offer', sdp })
.then(() => {
if (cb) {
return cb();
}
})
.catch(err => {
console.error(err);
this._log('error', 'Could not create WebRTC answer', err);
if (cb) {
return cb({ condition: 'general-error' });
}
});
}

onDescriptionInfo(info, cb) {
const hash = info.contents[0].application.offer.hash;
this.receiver.metadata.hash = hash;
if (this.receiver.metadata.actualhash) {
this._maybeReceivedFile();
}
cb();
}

_maybeReceivedFile() {
if (!this.receiver.metadata.hash.value) {
// unknown hash, file transfer not completed
} else if (this.receiver.metadata.hash.value === this.receiver.metadata.actualhash) {
this._log('info', 'File hash matches');
this.emit('receivedFile', this, this.receivedFile, this.receiver.metadata);
this.end('success');
} else {
this._log('error', 'File hash does not match');
this.end('media-error');
}
}
}
Oops, something went wrong.

0 comments on commit e1f7934

Please sign in to comment.