2020-10-16 08:06:03 +00:00
|
|
|
"use strict";
|
2020-10-02 12:01:51 +00:00
|
|
|
|
2020-10-09 14:42:13 +00:00
|
|
|
// Public modules from npm
|
2020-10-31 15:00:26 +00:00
|
|
|
const axios = require("axios").default;
|
2020-11-01 08:54:59 +00:00
|
|
|
const cheerio = require("cheerio");
|
2020-11-01 13:56:07 +00:00
|
|
|
const axiosCookieJarSupport = require("axios-cookiejar-support").default;
|
|
|
|
const tough = require("tough-cookie");
|
2020-10-09 14:42:13 +00:00
|
|
|
|
2020-10-01 19:13:23 +00:00
|
|
|
// Modules from file
|
2020-10-31 15:00:26 +00:00
|
|
|
const shared = require("./shared.js");
|
2020-11-01 08:54:59 +00:00
|
|
|
const f95url = require("./constants/url.js");
|
2020-11-01 20:56:12 +00:00
|
|
|
const f95selector = require("./constants/css-selector.js");
|
|
|
|
const LoginResult = require("./classes/login-result.js");
|
2020-12-15 13:16:18 +00:00
|
|
|
const fetchPlatformData = require("./platform-data.js").fetchPlatformData;
|
2020-11-01 08:54:59 +00:00
|
|
|
|
|
|
|
// Global variables
|
2020-11-01 20:56:12 +00:00
|
|
|
const userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) " +
|
|
|
|
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Safari/605.1.15";
|
2020-11-01 13:56:07 +00:00
|
|
|
axiosCookieJarSupport(axios);
|
2020-11-02 14:06:09 +00:00
|
|
|
|
2020-11-01 20:56:12 +00:00
|
|
|
const commonConfig = {
|
|
|
|
headers: {
|
|
|
|
"User-Agent": userAgent,
|
|
|
|
"Connection": "keep-alive"
|
|
|
|
},
|
|
|
|
withCredentials: true,
|
2020-11-07 17:55:20 +00:00
|
|
|
jar: new tough.CookieJar() // Used to store the token in the PC
|
2020-11-01 20:56:12 +00:00
|
|
|
};
|
|
|
|
|
2020-10-31 15:00:26 +00:00
|
|
|
/**
|
|
|
|
* @protected
|
|
|
|
* Gets the HTML code of a page.
|
|
|
|
* @param {String} url URL to fetch
|
|
|
|
* @returns {Promise<String>} HTML code or `null` if an error arise
|
|
|
|
*/
|
|
|
|
module.exports.fetchHTML = async function (url) {
|
2020-12-12 12:41:39 +00:00
|
|
|
// Local variables
|
|
|
|
let returnValue = null;
|
|
|
|
|
2020-11-01 20:56:12 +00:00
|
|
|
// Fetch the response of the platform
|
|
|
|
const response = await exports.fetchGETResponse(url);
|
2020-12-12 12:41:39 +00:00
|
|
|
|
|
|
|
// Manage response
|
2020-11-01 20:56:12 +00:00
|
|
|
if (!response) {
|
|
|
|
shared.logger.warn(`Unable to fetch HTML for ${url}`);
|
2020-11-01 08:54:59 +00:00
|
|
|
}
|
2020-11-30 09:39:59 +00:00
|
|
|
else if (!response.headers["content-type"].includes("text/html")) {
|
|
|
|
// The response is not a HTML page
|
|
|
|
shared.logger.warn(`The ${url} returned a ${response.headers["content-type"]} response`);
|
|
|
|
}
|
2020-12-12 12:41:39 +00:00
|
|
|
|
|
|
|
returnValue = response.data;
|
|
|
|
return returnValue;
|
2020-11-01 08:54:59 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @protected
|
2020-11-01 13:56:07 +00:00
|
|
|
* It authenticates to the platform using the credentials
|
|
|
|
* and token obtained previously. Save cookies on your
|
|
|
|
* device after authentication.
|
2020-11-01 08:54:59 +00:00
|
|
|
* @param {Credentials} credentials Platform access credentials
|
2020-12-03 09:12:56 +00:00
|
|
|
* @param {Boolean} force Specifies whether the request should be forced, ignoring any saved cookies
|
2020-11-02 14:06:09 +00:00
|
|
|
* @returns {Promise<LoginResult>} Result of the operation
|
2020-11-01 08:54:59 +00:00
|
|
|
*/
|
2020-11-02 14:06:09 +00:00
|
|
|
module.exports.authenticate = async function (credentials, force) {
|
2020-11-01 13:56:07 +00:00
|
|
|
shared.logger.info(`Authenticating with user ${credentials.username}`);
|
|
|
|
if (!credentials.token) throw new Error(`Invalid token for auth: ${credentials.token}`);
|
2020-11-01 08:54:59 +00:00
|
|
|
|
2020-11-01 20:56:12 +00:00
|
|
|
// Secure the URL
|
|
|
|
const secureURL = exports.enforceHttpsUrl(f95url.F95_LOGIN_URL);
|
|
|
|
|
2020-11-01 13:56:07 +00:00
|
|
|
// Prepare the parameters to send to the platform to authenticate
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
params.append("login", credentials.username);
|
|
|
|
params.append("url", "");
|
|
|
|
params.append("password", credentials.password);
|
|
|
|
params.append("password_confirm", "");
|
|
|
|
params.append("additional_security", "");
|
|
|
|
params.append("remember", "1");
|
|
|
|
params.append("_xfRedirect", "https://f95zone.to/");
|
|
|
|
params.append("website_code", "");
|
|
|
|
params.append("_xfToken", credentials.token);
|
2020-11-01 08:54:59 +00:00
|
|
|
|
|
|
|
try {
|
2020-11-01 20:56:12 +00:00
|
|
|
// Try to log-in
|
2020-11-02 14:06:09 +00:00
|
|
|
let config = Object.assign({}, commonConfig);
|
|
|
|
if (force) delete config.jar;
|
|
|
|
const response = await axios.post(secureURL, params, config);
|
2020-11-01 20:56:12 +00:00
|
|
|
|
|
|
|
// Parse the response HTML
|
|
|
|
const $ = cheerio.load(response.data);
|
|
|
|
|
|
|
|
// Get the error message (if any) and remove the new line chars
|
|
|
|
const errorMessage = $("body").find(f95selector.LOGIN_MESSAGE_ERROR).text().replace(/\n/g, "");
|
|
|
|
|
|
|
|
// Return the result of the authentication
|
2020-12-03 10:44:15 +00:00
|
|
|
if (errorMessage === "") {
|
|
|
|
// Fetch data
|
2020-12-15 13:16:18 +00:00
|
|
|
await fetchPlatformData();
|
2020-12-03 10:44:15 +00:00
|
|
|
return new LoginResult(true, "Authentication successful");
|
|
|
|
}
|
2020-11-01 20:56:12 +00:00
|
|
|
else return new LoginResult(false, errorMessage);
|
2020-10-31 15:00:26 +00:00
|
|
|
} catch (e) {
|
2020-11-01 20:56:12 +00:00
|
|
|
shared.logger.error(`Error ${e.message} occurred while authenticating to ${secureURL}`);
|
|
|
|
return new LoginResult(false, `Error ${e.message} while authenticating`);
|
2020-10-31 15:00:26 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-11-01 08:54:59 +00:00
|
|
|
/**
|
|
|
|
* Obtain the token used to authenticate the user to the platform.
|
|
|
|
* @returns {Promise<String>} Token or `null` if an error arise
|
|
|
|
*/
|
|
|
|
module.exports.getF95Token = async function() {
|
2020-11-01 20:56:12 +00:00
|
|
|
// Fetch the response of the platform
|
|
|
|
const response = await exports.fetchGETResponse(f95url.F95_LOGIN_URL);
|
|
|
|
if (!response) {
|
|
|
|
shared.logger.warn("Unable to get the token for the session");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The response is a HTML page, we need to find the <input> with name "_xfToken"
|
|
|
|
const $ = cheerio.load(response.data);
|
|
|
|
const token = $("body").find(f95selector.GET_REQUEST_TOKEN).attr("value");
|
|
|
|
return token;
|
|
|
|
};
|
|
|
|
|
|
|
|
//#region Utility methods
|
2020-11-02 14:06:09 +00:00
|
|
|
/**
|
|
|
|
* @protected
|
|
|
|
* Performs a GET request to a specific URL and returns the response.
|
|
|
|
* If the request generates an error (for example 400) `null` is returned.
|
|
|
|
* @param {String} url
|
|
|
|
*/
|
2020-11-01 20:56:12 +00:00
|
|
|
module.exports.fetchGETResponse = async function(url) {
|
|
|
|
// Secure the URL
|
|
|
|
const secureURL = exports.enforceHttpsUrl(url);
|
|
|
|
|
2020-11-01 08:54:59 +00:00
|
|
|
try {
|
2020-11-01 20:56:12 +00:00
|
|
|
// Fetch and return the response
|
|
|
|
return await axios.get(secureURL, commonConfig);
|
2020-11-01 08:54:59 +00:00
|
|
|
} catch (e) {
|
2020-11-01 20:56:12 +00:00
|
|
|
shared.logger.error(`Error ${e.message} occurred while trying to fetch ${secureURL}`);
|
2020-11-01 08:54:59 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-10-31 15:00:26 +00:00
|
|
|
/**
|
|
|
|
* @protected
|
|
|
|
* Enforces the scheme of the URL is https and returns the new URL.
|
|
|
|
* @param {String} url
|
2020-11-02 14:06:09 +00:00
|
|
|
* @returns {String} Secure URL or `null` if the argument is not a string
|
2020-10-31 15:00:26 +00:00
|
|
|
*/
|
|
|
|
module.exports.enforceHttpsUrl = function (url) {
|
2020-11-02 14:06:09 +00:00
|
|
|
return exports.isStringAValidURL(url) ? url.replace(/^(https?:)?\/\//, "https://") : null;
|
2020-10-31 15:00:26 +00:00
|
|
|
};
|
2020-10-01 19:13:23 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @protected
|
|
|
|
* Check if the url belongs to the domain of the F95 platform.
|
2020-10-08 21:08:20 +00:00
|
|
|
* @param {String} url URL to check
|
2020-10-01 19:13:23 +00:00
|
|
|
* @returns {Boolean} true if the url belongs to the domain, false otherwise
|
|
|
|
*/
|
2020-10-08 21:09:05 +00:00
|
|
|
module.exports.isF95URL = function (url) {
|
2020-11-01 08:54:59 +00:00
|
|
|
if (url.toString().startsWith(f95url.F95_BASE_URL)) return true;
|
2020-10-29 21:14:40 +00:00
|
|
|
else return false;
|
2020-10-08 21:09:05 +00:00
|
|
|
};
|
2020-10-01 19:13:23 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @protected
|
2020-11-02 14:06:09 +00:00
|
|
|
* Checks if the string passed by parameter has a
|
|
|
|
* properly formatted and valid path to a URL (HTTP/HTTPS).
|
2020-10-01 19:13:23 +00:00
|
|
|
* @param {String} url String to check for correctness
|
|
|
|
* @returns {Boolean} true if the string is a valid URL, false otherwise
|
|
|
|
*/
|
2020-10-09 14:45:17 +00:00
|
|
|
module.exports.isStringAValidURL = function (url) {
|
2020-11-02 14:06:09 +00:00
|
|
|
// Many thanks to Daveo at StackOverflow (https://preview.tinyurl.com/y2f2e2pc)
|
|
|
|
const expression = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
|
|
|
|
const regex = new RegExp(expression);
|
|
|
|
if (url.match(regex)) return true;
|
|
|
|
else return false;
|
2020-10-09 14:45:17 +00:00
|
|
|
};
|
2020-10-09 14:42:13 +00:00
|
|
|
|
|
|
|
/**
|
2020-10-20 20:47:36 +00:00
|
|
|
* @protected
|
2020-10-09 14:42:13 +00:00
|
|
|
* Check if a particular URL is valid and reachable on the web.
|
|
|
|
* @param {String} url URL to check
|
2020-12-15 08:26:29 +00:00
|
|
|
* @param {Boolean} [checkRedirect]
|
|
|
|
* If true, the function will consider redirects a violation and return false.
|
|
|
|
* Default: false
|
2020-10-09 14:42:13 +00:00
|
|
|
* @returns {Promise<Boolean>} true if the URL exists, false otherwise
|
|
|
|
*/
|
2020-12-15 08:26:29 +00:00
|
|
|
module.exports.urlExists = async function (url, checkRedirect = false) {
|
|
|
|
// Local variables
|
|
|
|
let valid = false;
|
2020-10-09 14:45:17 +00:00
|
|
|
|
2020-12-15 08:26:29 +00:00
|
|
|
if (exports.isStringAValidURL(url)) {
|
2020-12-15 08:47:40 +00:00
|
|
|
valid = await _axiosUrlExists(url);
|
2020-10-09 14:45:17 +00:00
|
|
|
|
2020-12-15 08:26:29 +00:00
|
|
|
if (valid && checkRedirect) {
|
|
|
|
const redirectUrl = await exports.getUrlRedirect(url);
|
|
|
|
valid = redirectUrl === url;
|
|
|
|
}
|
2020-10-29 21:14:40 +00:00
|
|
|
}
|
2020-10-09 14:45:17 +00:00
|
|
|
|
2020-10-29 21:14:40 +00:00
|
|
|
return valid;
|
2020-10-09 14:45:17 +00:00
|
|
|
};
|
2020-10-20 20:47:36 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @protected
|
|
|
|
* Check if the URL has a redirect to another page.
|
|
|
|
* @param {String} url URL to check for redirect
|
|
|
|
* @returns {Promise<String>} Redirect URL or the passed URL
|
|
|
|
*/
|
2020-10-20 20:48:42 +00:00
|
|
|
module.exports.getUrlRedirect = async function (url) {
|
2020-12-15 08:26:29 +00:00
|
|
|
const response = await axios.head(url);
|
|
|
|
return response.config.url;
|
2020-11-01 20:56:12 +00:00
|
|
|
};
|
2020-12-15 13:00:05 +00:00
|
|
|
//#endregion Utility methods
|
|
|
|
|
|
|
|
//#region Private methods
|
|
|
|
/**
|
|
|
|
* @private
|
|
|
|
* Check with Axios if a URL exists.
|
|
|
|
* @param {String} url
|
|
|
|
*/
|
|
|
|
async function _axiosUrlExists(url) {
|
|
|
|
// Local variables
|
|
|
|
let valid = false;
|
|
|
|
try {
|
|
|
|
const response = await axios.head(url);
|
|
|
|
valid = response && !/4\d\d/.test(response.status);
|
|
|
|
} catch (error) {
|
|
|
|
if (error.code === "ENOTFOUND") valid = false;
|
|
|
|
else throw error;
|
|
|
|
}
|
|
|
|
return valid;
|
|
|
|
}
|
|
|
|
//#endregion
|