From b6c7578dddad063f2cdd657c79850990c51df247 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Thu, 20 Aug 2015 21:32:52 +1200 Subject: [PATCH 1/6] Disabled SNI on websocket connections to fix #111 --- lib/spotify.js | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/lib/spotify.js b/lib/spotify.js index 792cab7..e13c8bb 100644 --- a/lib/spotify.js +++ b/lib/spotify.js @@ -5,6 +5,8 @@ var vm = require('vm'); var util = require('./util'); var http = require('http'); +var https = require('https'); +var tls = require('tls'); var WebSocket = require('ws'); var cheerio = require('cheerio'); var schemas = require('./schemas'); @@ -67,6 +69,35 @@ Spotify.login = function (un, pw, fn) { return spotify; }; +/** + * Patched version of `https.Agent.createConnection` that disables SNI on websocket connections. + */ + +function createHttpsConnection(port, host, options) { + if (typeof port === 'object') { + options = port; + } else if (typeof host === 'object') { + options = host; + } else if (typeof options === 'object') { + options = options; + } else { + options = {}; + } + + if (typeof port === 'number') { + options.port = port; + } + + if (typeof host === 'string') { + options.host = host; + } + + // Disable SNI + options.servername = null; + + return tls.connect(options); +} + /** * Spotify Web base class. * @@ -105,6 +136,10 @@ function Spotify () { this.sourceUrls.LARGE = this.sourceUrls.large; this.sourceUrls.XLARGE = this.sourceUrls.avatar; + // WebSocket agent + this.wsAgent = new https.Agent(); + this.wsAgent.createConnection = createHttpsConnection; + // WebSocket callbacks this._onopen = this._onopen.bind(this); this._onclose = this._onclose.bind(this); @@ -333,7 +368,11 @@ Spotify.prototype._openWebsocket = function (err, res) { var url = 'wss://' + ap_list[0] + '/'; debug('WS %j', url); - this.ws = new WebSocket(url, null, {"origin": "https://play.spotify.com", "headers":{"User-Agent": this.userAgent}}); + this.ws = new WebSocket(url, null, { + "agent": this.wsAgent, + "origin": "https://play.spotify.com", + "headers":{"User-Agent": this.userAgent} + }); this.ws.on('open', this._onopen); this.ws.on('close', this._onclose); this.ws.on('message', this._onmessage); From 127eadd5eb8e585ea8b20638374c6f148952341a Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 21 Aug 2015 16:29:48 +1200 Subject: [PATCH 2/6] Updated metadata protocol buffer and request version #111 --- lib/spotify.js | 4 ++-- proto/metadata.desc | Bin 3624 -> 3529 bytes proto/metadata.proto | 7 +------ 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/spotify.js b/lib/spotify.js index e13c8bb..3381780 100644 --- a/lib/spotify.js +++ b/lib/spotify.js @@ -752,14 +752,14 @@ Spotify.prototype.metadata = function (uris, fn) { mtype = type; requests.push({ method: 'GET', - uri: 'hm://metadata/' + type + '/' + id + uri: 'hm://metadata/3/' + type + '/' + id }); }); var header = { method: 'GET', - uri: 'hm://metadata/' + mtype + 's' + uri: 'hm://metadata/3/' + mtype + 's' }; var multiGet = true; if (requests.length == 1) { diff --git a/proto/metadata.desc b/proto/metadata.desc index df61c1e6caca4c76e9a01482c75ef1312a016bc3..fc0c6a30fe9fca5256dcf5b2b42a09596dc71bca 100644 GIT binary patch delta 61 zcmZ1>b5fd_>zMRL=1^|_)r?%+L8-+hMVZMZnfZB>S8^AK3yE^^CMT98=H#cBrp5#1 QBv=)gH8?l>^4w+w08;4^;{X5v delta 130 zcmX>py+VeWYpKje=1^{xE+#JSpw!}$qRixy%=|nd2QJR!#FE6E{PfaP2}T8G4L&2? z$qQK}CfhM{bLlz5q$a0x7nmp+adE;$gxI*499;w$gao*Ff<5 Vx!8j}LfzbaTm_gnzvI5m2mnpkBJcnJ diff --git a/proto/metadata.proto b/proto/metadata.proto index 2b0d9f9..ce29b25 100644 --- a/proto/metadata.proto +++ b/proto/metadata.proto @@ -116,18 +116,13 @@ message Copyright { optional string text = 2; } message Restriction { - enum Catalogue { - AD = 0; - SUBSCRIPTION = 1; - SHUFFLE = 3; - } enum Type { STREAMING = 0; } - repeated Catalogue catalogue = 1; optional string countries_allowed = 2; optional string countries_forbidden = 3; optional Type type = 4; + repeated string catalogue_str = 5; } message SalePeriod { From 8b3cecfb4a4f19f68bb23bc6869376e5c13b3289 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 21 Aug 2015 16:31:12 +1200 Subject: [PATCH 3/6] Changed client `userAgent` --- lib/spotify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/spotify.js b/lib/spotify.js index 3381780..20728f4 100644 --- a/lib/spotify.js +++ b/lib/spotify.js @@ -117,7 +117,7 @@ function Spotify () { this.authServer = 'play.spotify.com'; this.authUrl = '/xhr/json/auth.php'; this.landingUrl = '/'; - this.userAgent = 'Mozilla/5.0 (Chrome/13.37 compatible-ish) spotify-web/' + pkg.version; + this.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.46 Safari/537.36'; // base URLs for Image files like album artwork, artist prfiles, etc. // these values taken from "spotify.web.client.js" From 0ac4a8510d226c537278c13c3fb29908bc49c422 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 21 Aug 2015 16:37:15 +1200 Subject: [PATCH 4/6] Updated track restriction parsing #111 --- lib/spotify.js | 131 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 94 insertions(+), 37 deletions(-) diff --git a/lib/spotify.js b/lib/spotify.js index 20728f4..ee5cfcd 100644 --- a/lib/spotify.js +++ b/lib/spotify.js @@ -968,50 +968,107 @@ Spotify.prototype.isTrackAvailable = function (track, country) { if (!country) country = this.country; debug('isTrackAvailable()'); - var allowed = []; - var forbidden = []; - var available = false; - var restriction; - - if (Array.isArray(track.restriction)) { - for (var i = 0; i < track.restriction.length; i++) { - restriction = track.restriction[i]; - allowed.push.apply(allowed, restriction.allowed); - forbidden.push.apply(forbidden, restriction.forbidden); - - var isAllowed = !restriction.hasOwnProperty('countriesAllowed') || has(allowed, country); - var isForbidden = has(forbidden, country) && forbidden.length > 0; - - // guessing at names here, corrections welcome... - var accountTypeMap = { - premium: 'SUBSCRIPTION', - unlimited: 'SUBSCRIPTION', - free: 'AD' - }; - - if (has(allowed, country) && has(forbidden, country)) { - isAllowed = true; - isForbidden = false; + var account = { + catalogue: this.accountType, + country: country + }; + + return this.isPlayable( + this.parseRestrictions(track.restriction, account), + account + ); +}; + +/** + * @param {String} availability Track availability + * @param {Object} account Account details {catalogue, country} + * @api public + */ + +Spotify.prototype.isPlayable = function(availability, account) { + if(availability === "premium" && account.catalogue === "premium") { + return true; + } + + return availability === "available"; +}; + +/** + * @param {Array} restrictions Track restrictions + * @param {Object} account Account details {catalogue, country} + * @api public + */ + +Spotify.prototype.parseRestrictions = function(restrictions, account) { + debug('parseRestrictions() account: %j', account); + + var catalogues = {}, + available = false; + + if ("undefined" === typeof restrictions || 0 === restrictions.length) { + // Track has no restrictions + return "available"; + } + + for (var ri = 0; ri < restrictions.length; ++ri) { + var restriction = restrictions[ri], + valid = true, + allowed; + + if(restriction.countriesAllowed != void 0) { + // Check if account region is allowed + valid = restriction.countriesAllowed.length !== 0; + allowed = has(restriction.countriesAllowed, account.country); + } else { + // Check if account region is forbidden + if(restriction.countriesForbidden != void 0) { + allowed = !has(restriction.countriesForbidden, account.country); + } else { + allowed = true; } + } + + if (allowed && restriction.catalogueStr != void 0) { + // Update track catalogues + for (var ci = 0; ci < restriction.catalogueStr.length; ++ci) { + var key = restriction.catalogueStr[ci]; + + catalogues[key] = true; + } + } - var type = accountTypeMap[this.accountType] || 'AD'; - var applicable = has(restriction.catalogue, type); + if (restriction.type == void 0 || "streaming" == restriction.type.toLowerCase()) { + available |= valid; + } + } - available = isAllowed && !isForbidden && applicable; + debug('parseRestrictions() catalogues: %j', catalogues); - //debug('restriction: %j', restriction); - debug('type: %j', type); - debug('allowed: %j', allowed); - debug('forbidden: %j', forbidden); - debug('isAllowed: %j', isAllowed); - debug('isForbidden: %j', isForbidden); - debug('applicable: %j', applicable); - debug('available: %j', available); + if(available && account.catalogue === "all") { + // Account can stream anything + return "available"; + } - if (available) break; + if(catalogues[account.catalogue]) { + // Track can be streamed by this account + if(account.catalogue === "premium") { + return "premium"; + } else { + return "available"; } } - return available; + + if(catalogues.premium) { + // Premium account required + return "premium"; + } + + if(available) { + // Track not available in the account region + return "regional"; + } + + return "unavailable"; }; /** From 35c115075893931bdbb75f78692686a95ab85859 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 21 Aug 2015 16:55:08 +1200 Subject: [PATCH 5/6] The `Track` get/metadata method didn't work correctly with protobuf.js #111 --- lib/track.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/track.js b/lib/track.js index 11fa9ce..3d5d355 100644 --- a/lib/track.js +++ b/lib/track.js @@ -62,7 +62,7 @@ Track.prototype.metadata = function (fn) { if (err) return fn(err); // extend this Track instance with the new one's properties Object.keys(track).forEach(function (key) { - if (!self.hasOwnProperty(key)) { + if (track.hasOwnProperty(key)) { self[key] = track[key]; } }); From 75fdd0cb914bfd5d2401e10602b0f0ea4a58498e Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 21 Aug 2015 22:51:14 +1200 Subject: [PATCH 6/6] Fixed bug with restriction country checks #111 --- lib/spotify.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/spotify.js b/lib/spotify.js index ee5cfcd..3a48eb1 100644 --- a/lib/spotify.js +++ b/lib/spotify.js @@ -1018,11 +1018,11 @@ Spotify.prototype.parseRestrictions = function(restrictions, account) { if(restriction.countriesAllowed != void 0) { // Check if account region is allowed valid = restriction.countriesAllowed.length !== 0; - allowed = has(restriction.countriesAllowed, account.country); + allowed = has(restriction.allowed, account.country); } else { // Check if account region is forbidden if(restriction.countriesForbidden != void 0) { - allowed = !has(restriction.countriesForbidden, account.country); + allowed = !has(restriction.forbidden, account.country); } else { allowed = true; }