forked from dhleong/ps4-waker
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
executable file
·353 lines (314 loc) · 11.8 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
var util = require('util')
, events = require('events')
, fs = require('fs')
, _ = require('underscore')
, Detector = require('./lib/detector')
, Dummy = require('./lib/dummy')
, ps4lib = require('./lib/ps4lib')
, newSocket = require('./lib/ps4socket')
, DEFAULT_TIMEOUT = 5000
, WAIT_FOR_WAKE = 30000
, MAX_RETRIES = 5
, CRED_KEYS = ['client-type', 'auth-type', 'user-credential'];
/**
* Construct a new Waker instance, which may be
* used to wake a PS4 and login to it. If desired,
* you may also retain the ps4socket connection
* used to login for other purposes (such as
* launching apps, or putting the system in standby).
*
* @param credentials Either a string path to a credentials
* file, or an object containing the
* credentials (formatted the same way that
* they'd be stored in the credentials.json)
* @param config A config object/map. Valid keys:
* - autoLogin: (default: true) If true, will open a socket
* connection and cause the PS4 to login with
* the credentials provided for waking
* - errorIfAwake: (default: true) If true, returns an Error
* to the callback from wake() if the PS4 was
* not in standby mode. If you're using Waker
* to get a ps4socket, you may want to
* specify `false` here so you can get a socket
* regardless of whether the PS4 is in standby
* - keepSocket: (default: false) If true, the callback from
* wake will have a single, extra parameter
* on successful connection, which will be
* a reference to a valid ps4socket if it is
* non-null. autoLogin must also be true for
* this to work. If false, the callback will
* only have the usual error parameter, and any
* socket opened (IE: if autoLogin is true)
* will be closed after logging in.
*
* @see lib/ps4socket for the things you can do with the socket
*/
function Waker(credentials, config) {
this.credentials = credentials;
this.config = _.extend({
autoLogin: true
, errorIfAwake: true
, keepSocket: false
, debug: false
}, config);
}
util.inherits(Waker, events.EventEmitter);
/**
* Attempt to wake a specific PS4, or any PS4.
* @param detectOpts @see Detector#detect(); If you provide a
* device, any `timeout` set in this is ignored
* @param device (optional) A device object. If not provided,
* we will attempt to locate *any* PS4 and wake
* the first one found within `timeout`. Should
* look like:
*
* {
* status: "Standby",
* address: "192.168.4.2",
* host-name: "My PS4",
* port: 9001
* }
*
* @param callback Standard node.js-style callback. Will be
* called with 1 or 2 parameters, depending
* on the configuration (see the config param
* on the Waker constructor)
*/
Waker.prototype.wake = function(detectOpts, device, callback) {
// fix up/validate input
if (!(typeof(detectOpts) === 'number'
|| typeof(callback) === 'function'
|| (typeof(detectOpts) === 'object'
&& device
&& (device['host-name'] || typeof(device) === 'function')))) {
// thanks to backwards compat, this is a bit gross. In other words,
// detectOpts was definitely provided IFF it's a number OR it's
// an object and: callback is a function OR
// device is a function (IE: `device` was actually
// omitted and we were called as `wake(opts, callback)`) OR
// device has the `host-name` field, which should be returned by
// the PS4 in a `device` object.
// If neither of these cases were true, no opts were provided,
// we have to shift the args, and detectOpts can just be `undefined`
callback = device;
device = detectOpts;
detectOpts = undefined;
}
if (typeof(device) === 'function') {
callback = device;
device = undefined;
}
if (!callback) {
throw new Error("callback parameter is required");
}
// already got your device? just wake it
if (device) {
return this._doWake(device, detectOpts, callback);
}
// get the first device we can find
var self = this;
Detector.findAny(detectOpts, function(err, device, rinfo) {
if (err) return callback(err);
device.address = rinfo.address;
device.port = device['host-request-port']
self._doWake(device, detectOpts, callback);
});
};
Waker.prototype._doWake = function(device, detectOpts, callback) {
var self = this;
this.readCredentials(function(err, creds) {
if (err && self.listeners('need-credentials')) {
self.emit('need-credentials', device);
return;
} else if (err) {
// no listeners? just hop to it
self.requestCredentials(self._doWake.bind(self, device, detectOpts, callback));
return;
}
// we have credentials!
if (device.status && device.status != 'Standby' && self.config.errorIfAwake) {
return callback(new Error(device['host-name']
+ ' is already awake! ('
+ device.status
+ ')'
));
}
self.sendWake(device, detectOpts, creds, callback);
});
};
/**
* Read credentials from the provided constructor
* argument. Transparently handles the case that a
* credentials object was passed instead of a path
* to a file, so you can use this regardless of
* how the Waker was constructed. Most users will
* likely not need to use this function directly,
* as we will use it internally to get the credentials.
*
* @param callback Standard node.js callback, will be
* fired as (err, creds), where `err`
* will be non-null if something went
* wrong reading the credentials, and
* `creds` will be the credentials object
* on success.
*/
Waker.prototype.readCredentials = function(callback) {
if (this.credentials !== null && typeof(this.credentials) === 'object') {
callback(null, this.credentials);
} else {
fs.readFile(this.credentials, function(err, buf) {
if (err) return callback(err);
callback(null, JSON.parse(buf.toString()));
});
}
};
/**
* Constructs a "dummy" PS4 on the network for the purpose
* of acquiring the appropriate credentials. It may be
* preferrable to just install ps4-waker globally and
* use the ps4-waker executable to acquire credentials,
* instead of calling this directly.
*
* While the acquired credentials (if any) will be passed
* to the callback always, if a file path was provided
* for the `credentials` param in the Waker constructor,
* they will also be written to that file. If an object
* was provided, however the credentials will ONLY be
* passed to the callback.
*
* @param callback Standard node.js callback function,
* called as (err, creds), where `err`
* will be non-null if something went wrong,
* and `creds` will be a credentials object
* if it worked.
*/
Waker.prototype.requestCredentials = function(callback) {
var self = this;
var dummy = new Dummy();
dummy.setStandby();
dummy.once('wakeup', function(packet) {
var creds = CRED_KEYS.reduce(function(data, key) {
data[key] = packet[key];
return data;
}, {});
if (typeof(self.credentials) == 'object') {
callback(null, creds);
} else {
fs.writeFile(self.credentials, JSON.stringify(creds), function(err) {
if (err) return callback(err);
callback(null, creds);
});
}
dummy.close();
});
dummy.once('error', function(err) {
callback(err);
});
dummy.listen();
}
/**
* Send a WAKEUP request directly to the given device,
* using the provided credentials. Must users should
* probably prefer wake()
*
* @param device Device object (@see wake())
* @param detectOpts @see Detector#detect()
* @param creds Credentials object, as in the Waker constructor.
* It MUST be the inflated object, however; a string
* file path will not work here.
* @param callback @see wake()
*/
Waker.prototype.sendWake = function(device, detectOpts, creds, callback) {
if (!detectOpts || typeof(detectOpts) === 'number') {
detectOpts = {
timeout: detectOpts
};
} else if (detectOpts && typeof(detectOpts) !== 'object') {
// not undefined, not a number, and not an object.
// barf all over the input
throw new Error("Illegal value for detectOpts: " + detectOpts);
}
// override any provided timeout
detectOpts.timeout = WAIT_FOR_WAKE;
// make sure to use standard port
device.port = ps4lib.DDP_PORT;
// send the wake command
var self = this;
this.udp = ps4lib.udpSocket();
this.udp.bind(undefined, detectOpts.bindAddress, function() {
self.udp.setBroadcast(true); // maybe?
self.udp.discover("WAKEUP", creds, device);
self._whenAwake(device, detectOpts,
self._login.bind(self, device, creds, callback));
});
}
Waker.prototype._whenAwake = function(device, detectOpts, callback) {
this.emit('device-notified', device);
var statusCheckDelay = 1000;
var start = new Date().getTime();
var self = this;
var loop = function(err, d) {
d = d || {};
if (d.statusLine != ps4lib.STATUS_AWAKE) {
var now = new Date().getTime();
var delta = now - start;
var newTimeout = detectOpts.timeout - delta - statusCheckDelay;
if (newTimeout > 0) {
detectOpts.timeout = newTimeout;
setTimeout(function() {
Detector.find(device.address, detectOpts, loop);
}, statusCheckDelay);
} else {
self.udp.close();
callback(new Error("Device didn't wake in time"));
}
return;
}
self.udp.close();
callback(null);
}
// begin the loop
loop(null);
}
// NB: weird arg order due to binding
Waker.prototype._login = function(device, creds, callback, err) {
if (err) return callback(err);
if (!this.config.autoLogin) {
callback();
return;
}
var self = this;
this.emit('logging-in', device);
var socket = newSocket({
accountId: creds['user-credential']
, host: device.address
, pinCode: '' // assume we're registered...?
, debug: self.config.debug
});
socket.retries = 0;
socket.on('login_result', function(packet) {
if (packet.result !== 0) {
console.error("Login error:", packet.error);
}
if (self.config.keepSocket) {
callback(null, socket);
} else {
this.close();
callback(null);
}
}).on('error', function(err) {
if (socket.retries++ < MAX_RETRIES && err.code == 'ECONNREFUSED') {
console.warn("Login connect refused; retrying soon");
setTimeout(function() {
// try again; system may just not be up yet
socket.connect(device.address);
}, 1000);
return;
}
console.error("Error logging in:", err);
callback(null); // technically, wake was successful
});
}
module.exports = Waker;
module.exports.Detector = Detector;
module.exports.Socket = newSocket;