Files
crunchy/src/my_request.ts
2020-04-27 22:34:13 +01:00

424 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
import cheerio = require('cheerio');
import request = require('request');
import rp = require('request-promise');
import Promise = require('bluebird');
import uuid = require('uuid');
import path = require('path');
import fs = require('fs-extra');
import languages = require('./languages');
import log = require('./log');
import { RequestPromise } from 'request-promise';
import { Response } from 'request';
// tslint:disable-next-line:no-var-requires
const cookieStore = require('tough-cookie-file-store');
const CR_COOKIE_DOMAIN = 'http://crunchyroll.com';
let isAuthenticated = false;
let isPremium = false;
let j: request.CookieJar;
// tslint:disable-next-line:no-var-requires
import cloudscraper = require('cloudscraper');
let currentOptions: any;
let optionsSet = false;
function AuthError(msg: string): IAuthError
{
return { name: 'AuthError', message: msg, authError: true };
}
function startSession(config: IConfig): Promise<any>
{
return rp(
{
method: 'GET',
url: config.crSessionUrl,
qs:
{
device_id: config.crDeviceId,
device_type: config.crDeviceType,
access_token: config.crSessionKey,
version: config.crAPIVersion,
locale: config.crLocale,
},
json: true,
})
.then((response: any) =>
{
if ((response.data === undefined) || (response.data.session_id === undefined))
{
throw new Error('Getting session failed: ' + JSON.stringify(response));
}
return response.data.session_id;
});
}
function APIlogin(config: IConfig, sessionId: string, user: string, pass: string): Promise<any>
{
return rp(
{
method: 'POST',
url: config.crLoginUrl,
form:
{
account: user,
password: pass,
session_id: sessionId,
version: config.crAPIVersion,
},
json: true,
jar: j,
})
.then((response) =>
{
if (response.error) throw new Error('Login failed: ' + response.message);
return response.data;
});
}
function checkIfUserIsAuth(config: IConfig, done: (err: any) => void): void
{
/**
* The main page give us some information about the user
*/
const url = 'http://www.crunchyroll.com/';
cloudscraper.get(url, getOptions(config, null), (err: any, rep: Response, body: string) =>
{
if (err)
{
return done(err);
}
const $ = cheerio.load(body);
/* As we are here, try to detect which locale CR tell us */
const localeRE = /LOCALE = "([a-zA-Z]+)",/g;
const locale = localeRE.exec($('script').text())[1];
const countryCode = languages.localeToCC(locale);
if (config.crlang === undefined)
{
log.info('No locale set. Setting to the one reported by CR: "' + countryCode + '"');
config.crlang = countryCode;
}
else if (config.crlang !== countryCode)
{
log.warn('Crunchy is configured for locale "' + config.crlang + '" but CR report "' + countryCode + '" (LOCALE = ' + locale + ')');
log.warn('Check if it is correct or rerun (once) with "-l ' + countryCode + '" to correct.');
}
/* 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();
log.warn('Authentication failed: ' + error);
log.dumpToDebug('not auth rep', rep);
log.dumpToDebug('not auth body', body);
return done(AuthError('Authentication failed: ' + error));
}
else
{
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);
});
}
function loadCookies(config: IConfig)
{
const cookiePath = path.join(config.output || process.cwd(), '.cookies.json');
if (!fs.existsSync(cookiePath))
{
fs.closeSync(fs.openSync(cookiePath, 'w'));
}
j = request.jar(new cookieStore(cookiePath));
}
export function eatCookies(config: IConfig)
{
const cookiePath = path.join(config.output || process.cwd(), '.cookies.json');
if (fs.existsSync(cookiePath))
{
fs.removeSync(cookiePath);
}
j = undefined;
}
export function getUserAgent(): string
{
return currentOptions.headers['User-Agent'];
}
/**
* Performs a GET request for the resource.
*/
export function get(config: IConfig, url: string, done: (err: any, result?: string) => void)
{
authenticate(config, (err) =>
{
if (err)
{
return done(err);
}
cloudscraper.get(url, getOptions(config, null), (error: any, 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, url: string, form: any, done: (err: any, result?: string) => void)
{
authenticate(config, (err) =>
{
if (err)
{
return done(err);
}
cloudscraper.post(url, getOptions(config, form), (error: Error, response: any, body: any) =>
{
if (error)
{
return done(error);
}
done(null, typeof body === 'string' ? body : String(body));
});
});
}
function authUsingCookies(config: IConfig, done: (err: any) => void)
{
j.setCookie(request.cookie('session_id=' + config.crSessionId + '; Domain=crunchyroll.com; HttpOnly; hostOnly=false;'),
CR_COOKIE_DOMAIN);
checkIfUserIsAuth(config, (errCheckAuth2) =>
{
if (isAuthenticated)
{
return done(null);
}
else
{
return done(errCheckAuth2);
}
});
}
function authUsingApi(config: IConfig, done: (err: any) => void)
{
if (!config.pass || !config.user)
{
log.error('You need to give login/password to use Crunchy');
process.exit(-1);
}
if (config.crDeviceId === undefined)
{
config.crDeviceId = uuid.v4();
}
if (!config.crSessionUrl || !config.crDeviceType || !config.crAPIVersion ||
!config.crLocale || !config.crLoginUrl)
{
return done(AuthError('Invalid API configuration, please check your config file.'));
}
startSession(config)
.then((sessionId: string) =>
{
// defaultHeaders['Cookie'] = `sess_id=${sessionId}; c_locale=enUS`;
return APIlogin(config, sessionId, config.user, config.pass);
})
.then((userData) =>
{
checkIfUserIsAuth(config, (errCheckAuth2) =>
{
if (isAuthenticated)
{
return done(null);
}
else
{
return done(errCheckAuth2);
}
});
})
.catch((errInChk) =>
{
return done(AuthError(errInChk.message));
});
}
function authUsingForm(config: IConfig, done: (err: any) => void)
{
/* So if we are here now, that mean we are not authenticated so do as usual */
if (!config.pass || !config.user)
{
log.error('You need to give login/password to use Crunchy');
process.exit(-1);
}
/* First get https://www.crunchyroll.com/login to get the login token */
cloudscraper.get('https://www.crunchyroll.com/login', getOptions(config, null), (err: any, rep: Response, body: string) =>
{
if (err) return done(err);
const $ = cheerio.load(body);
/* Get the token from the login page */
const token = $('input[name="login_form[_token]"]').attr('value');
if (token === '')
{
return done(AuthError('Can\'t find token!'));
}
/* Now call the page again with the token and credentials */
const paramForm =
{
'login_form[name]': config.user,
'login_form[password]': config.pass,
'login_form[redirect_url]': '/',
'login_form[_token]': token
};
cloudscraper.post('https://www.crunchyroll.com/login', getOptions(config, paramForm), (err: any, rep: Response, body: string) =>
{
if (err)
{
return done(err);
}
/* Now let's check if we are authentificated */
checkIfUserIsAuth(config, (errCheckAuth2) =>
{
if (isAuthenticated)
{
return done(null);
}
else
{
return done(errCheckAuth2);
}
});
});
});
}
/**
* Authenticates using the configured pass and user.
*/
function authenticate(config: IConfig, done: (err: any) => void)
{
if (isAuthenticated)
{
return done(null);
}
/* First of all, check if the user is not already logged via the cookies */
checkIfUserIsAuth(config, (errCheckAuth) =>
{
if (isAuthenticated)
{
return done(null);
}
log.info('Seems we are not currently logged. Let\'s login!');
if (config.logUsingApi)
{
return authUsingApi(config, done);
}
else if (config.logUsingCookie)
{
return authUsingCookies(config, done);
}
else
{
return authUsingForm(config, done);
}
});
}
function getOptions(config: IConfig, form: any)
{
if (!optionsSet)
{
currentOptions = {};
currentOptions.headers = {};
currentOptions.headers['Cache-Control'] = 'private';
currentOptions.headers.Accept = 'application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5';
if (config.userAgent)
{
currentOptions.headers['User-Agent'] = config.userAgent;
}
else
{
currentOptions.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0';
}
if (j === undefined)
{
loadCookies(config);
}
currentOptions.decodeEmails = true;
currentOptions.jar = j;
optionsSet = true;
}
currentOptions.form = {};
if (form !== null)
{
currentOptions.form = form;
}
return currentOptions;
}