UNPKG

@hyperlink/plex-api-credentials

Version:
185 lines (156 loc) 6.23 kB
'use strict'; var EventEmitter = require('events').EventEmitter; var Promise = require('bluebird'); var util = require('util'); var request = require('request-promise'); var headers = require('plex-api-headers'); var parseString = Promise.promisify(require('xml2js').parseString); var errors = require('request-promise/errors'); var rxAuthToken = /authenticationToken="([^"]+)"/; function CredentialsAuthenticator(options) { EventEmitter.call(this); this.username = options.username; this.password = options.password; this.homeUser = options.managedUser; } util.inherits(CredentialsAuthenticator, EventEmitter); CredentialsAuthenticator.prototype.authenticate = function authenticate(plexApi, callback) { var self = this; var options; if (typeof plexApi !== 'object') { throw new TypeError('First argument should be the plex-api object to perform authentication for'); } if (typeof callback !== 'function') { throw new TypeError('Second argument should be a callback function to be called when authentication has finished'); } options = { url: 'https://plex.tv/users/sign_in.xml', headers: headers(plexApi, { Authorization: authHeaderVal(this.username, this.password) }) }; return request.post(options) .then(xmlBody => { let token = extractAuthToken(xmlBody); if (!token) { throw new Error('Couldnt not find authentication token in response from Plex.tv :('); } self.emit('authToken', token); return token; }) .catch(errors.StatusCodeError, err => { throw new Error('Invalid status code in authentication response from Plex.tv, expected 201 but got ' + err.statusCode); }) .catch(errors.RequestError, err => { throw new Error('Error while requesting https://plex.tv for authentication: ' + String(err)); }) .then(token => { if (self.homeUser && self.homeUser.name) { let plexContext = { plexApi : plexApi, token : token }; return getHomeUser(plexContext) .then(xml => findUserByName(xml, self.homeUser.name)) .then(user => switchUser(plexContext, user, self.homeUser.pin)) .then(extractAuthToken) .then(token => getAccessToken({token : token, plexApi : plexApi})) .then(accessToken => { self.emit('accessToken', accessToken); self.emit('token', accessToken); return token; }); } self.emit('token', token); return token; }) .asCallback(callback); }; function getHomeUser(plexContext) { return request.get(createRequestOpts(plexContext, 'https://plex.tv/api/home/users')); } function createRequestOpts(plexContext, url) { return { url : url, headers : headers(plexContext.plexApi, { 'X-Plex-Token': plexContext.token }) }; } function getAccessToken(plexContext, callback) { return request.get(createRequestOpts(plexContext, 'https://plex.tv/api/resources?includeHttps=1')) .then(parseString) .then( result => extractAccessToken(result, plexContext) ); } function extractAccessToken(result, {plexApi}) { let token = null; const checkServerName = !!plexApi.serverName; let hasDevices = result.MediaContainer.Device && result.MediaContainer.Device.some(rawDevice => { const {$: device} = rawDevice; if (device.presence !== '1') { return false; } const provides = device.provides.split(','); if (!provides.includes('server')) { return false; } if (checkServerName) { if (plexApi.serverName === device.name) { token = device.accessToken; return true; } return false; } token = device.accessToken; return true; }); if (hasDevices) { return token; } throw new Error('Couldn\'t find any devices this managed user has access to'); } function switchUser(plexContext, user, pin, callback) { let url = 'https://plex.tv/api/home/users/' + user.id + '/switch?' + (user.protected && pin ? ('pin=' + pin) : ''); return request.post(createRequestOpts(plexContext, url)); } function findUserByName(xml, homeUser) { return parseString(xml) .then(result => { let foundUser = { protected : false, name : null, id : null }; let found = result.MediaContainer.User.some((user) => { if (user.$.title.toLocaleLowerCase() == homeUser.toLocaleLowerCase()) { foundUser.id = user.$.id; foundUser.name = user.$.title; foundUser.protected = user.$.protected === '1'; return true; } return false; }); if (found) { return foundUser; } throw new Error('Home user ' + homeUser + ' not found'); }); } function extractAuthToken(xmlBody) { return xmlBody.match(rxAuthToken)[1]; } function authHeaderVal(username, password) { var authString = username + ':' + password; var buffer = new Buffer(authString.toString(), 'binary'); return 'Basic ' + buffer.toString('base64'); } module.exports = function (options) { if (typeof (options) !== 'object') { throw new TypeError('An options object containing .username and .password is required'); } if (typeof (options.username) !== 'string') { throw new TypeError('Options object requires a .username property as a string'); } if (typeof (options.password) !== 'string') { throw new TypeError('Options object requires a .password property as a string'); } return new CredentialsAuthenticator(options); };