'use strict'; import cheerio = require('cheerio'); import request = require('request'); import rp = require('request-promise'); import Promise = require('bluebird'); import log = require('./log'); import { RequestPromise } from 'request-promise'; import { Response } from 'request'; // tslint:disable-next-line:no-var-requires const cloudscraper = require('cloudscraper'); let isAuthenticated = false; let isPremium = false; const defaultHeaders: request.Headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; WOW64; x64; rv:58.0) Gecko/20100101 Firefox/58.0', 'Connection': 'keep-alive', 'Referer': 'https://www.crunchyroll.com/login', }; function generateDeviceId(): string { let id = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < 32; i++) { id += possible.charAt(Math.floor(Math.random() * possible.length)); } return id; } function startSession(): Promise { return rp({ method: 'GET', url: 'CR_SESSION_URL', qs: { device_id: generateDeviceId(), device_type: 'CR_DEVICE_TYPE', access_token: 'CR_SESSION_KEY', version: 'CR_API_VERSION', locale: 'CR_LOCALE', }, json: true, }) .then((response: any) => { return response.data.session_id; }); } function login(sessionId: string, user: string, pass: string): Promise { return rp({ method: 'POST', url: 'CR_LOGIN_URL', form: { account: user, password: pass, session_id: sessionId, version: 'CR_API_VERSION', }, json: true, }) .then((response) => { if (response.error) throw new Error('Login failed: ' + response.message); return response.data; }); } // TODO: logout /** * Performs a GET request for the resource. */ export function get(config: IConfig, options: string|request.Options, done: (err: Error, result?: string) => void) { authenticate(config, (err) => { if (err) { return done(err); } cloudscraper.request(modify(options, 'GET'), (error: Error, response: any, body: any) => { if (error) return done(error); done(null, typeof body === 'string' ? body : String(body)); }); }); } /** * Performs a POST request for the resource. */ export function post(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void) { authenticate(config, (err) => { if (err) { return done(err); } cloudscraper.request(modify(options, 'POST'), (error: Error, response: any, body: any) => { if (error) return done(error); done(null, typeof body === 'string' ? body : String(body)); }); }); } /** * Authenticates using the configured pass and user. */ function authenticate(config: IConfig, done: (err: Error) => void) { if (isAuthenticated || !config.pass || !config.user) { return done(null); } startSession() .then((sessionId: string) => { defaultHeaders.Cookie = `sess_id=${sessionId}; c_locale=enUS`; return login(sessionId, config.user, config.pass); }) .then((userData) => { /** * The page return with a meta based redirection, as we wan't to check that everything is fine, reload * the main page. A bit convoluted, but more sure. */ const options = { headers: defaultHeaders, jar: true, url: 'http://www.crunchyroll.com/', method: 'GET', }; cloudscraper.request(options, (err: Error, rep: string, body: string) => { if (err) return done(err); const $ = cheerio.load(body); /* Check if auth worked */ const regexps = /ga\('set', 'dimension[5-8]', '([^']*)'\);/g; const dims = regexps.exec($('script').text()); for (let i = 1; i < 5; i++) { if ((dims[i] !== undefined) && (dims[i] !== '') && (dims[i] !== 'not-registered')) { isAuthenticated = true; } if ((dims[i] === 'premium') || (dims[i] === 'premiumplus')) { isPremium = true; } } if (isAuthenticated === false) { const error = $('ul.message, li.error').text(); return done(new Error('Authentication failed: ' + error)); } if (isPremium === false) { log.warn('Do not use this app without a premium account.'); } else { log.info('You have a premium account! Good!'); } done(null); }); }) .catch(done); } /** * Modifies the options to use the authenticated cookie jar. */ function modify(options: string|request.Options, reqMethod: string): request.Options { if (typeof options !== 'string') { options.jar = true; options.headers = defaultHeaders; options.method = reqMethod; return options; } return { jar: true, headers: defaultHeaders, url: options.toString(), method: reqMethod }; }