Implement cookie storage on disk

pull/73/head
MillenniumEarl 2021-03-03 15:13:00 +01:00
parent 52131a143b
commit b26c5de21b
2 changed files with 61 additions and 18 deletions

View File

@ -3,9 +3,11 @@
// Core modules // Core modules
import * as fs from "fs"; import * as fs from "fs";
import { promisify } from "util"; import { promisify } from "util";
import path from "path";
// Public modules from npm // Public modules from npm
import md5 from "md5"; import md5 from "md5";
import tough, { CookieJar } from "tough-cookie";
// Promisifed functions // Promisifed functions
const areadfile = promisify(fs.readFile); const areadfile = promisify(fs.readFile);
@ -19,12 +21,15 @@ export default class Session {
/** /**
* Max number of days the session is valid. * Max number of days the session is valid.
*/ */
private readonly SESSION_TIME: number = 1; private readonly SESSION_TIME: number = 3;
private readonly COOKIEJAR_FILENAME: string = "f95cookiejar.json";
private _path: string; private _path: string;
private _isMapped: boolean; private _isMapped: boolean;
private _created: Date; private _created: Date;
private _hash: string; private _hash: string;
private _token: string; private _token: string;
private _cookieJar: CookieJar;
private _cookieJarPath: string;
//#endregion Fields //#endregion Fields
@ -50,18 +55,27 @@ export default class Session {
* Token used to login to F95Zone. * Token used to login to F95Zone.
*/ */
public get token() { return this._token; } public get token() { return this._token; }
/**
* Cookie holder.
*/
public get cookieJar() { return this._cookieJar; }
//#endregion Getters //#endregion Getters
/** /**
* Initializes the session by setting the path for saving information to disk. * Initializes the session by setting the path for saving information to disk.
*/ */
constructor(path: string) { constructor(p: string) {
this._path = path; this._path = p;
this._isMapped = fs.existsSync(this.path); this._isMapped = fs.existsSync(this.path);
this._created = new Date(Date.now()); this._created = new Date(Date.now());
this._hash = null; this._hash = null;
this._token = null; this._token = null;
this._cookieJar = new tough.CookieJar();
// Define the path for the cookiejar
const basedir = path.dirname(p);
this._cookieJarPath = path.join(basedir, this.COOKIEJAR_FILENAME);
} }
//#region Private Methods //#region Private Methods
@ -122,6 +136,10 @@ export default class Session {
// Write data // Write data
await awritefile(this.path, data); await awritefile(this.path, data);
// Write cookiejar
const serializedJar = await this._cookieJar.serialize();
await awritefile(this._cookieJarPath, JSON.stringify(serializedJar));
} }
/** /**
@ -134,9 +152,13 @@ export default class Session {
const json = JSON.parse(data); const json = JSON.parse(data);
// Assign values // Assign values
this._created = json._created; this._created = new Date(json._created);
this._hash = json._hash; this._hash = json._hash;
this._token = json._token; this._token = json._token;
// Load cookiejar
const serializedJar = await areadfile(this._cookieJarPath, { encoding: 'utf-8', flag: 'r' });
this._cookieJar = await CookieJar.deserialize(JSON.parse(serializedJar));
} }
} }
@ -145,7 +167,11 @@ export default class Session {
*/ */
async delete(): Promise<void> { async delete(): Promise<void> {
if (this.isMapped) { if (this.isMapped) {
// Delete the session data
await aunlinkfile(this.path); await aunlinkfile(this.path);
// Delete the cookiejar
await aunlinkfile(this._cookieJarPath);
} }
} }
@ -154,18 +180,22 @@ export default class Session {
*/ */
isValid(username: string, password: string): boolean { isValid(username: string, password: string): boolean {
// Get the number of days from the file creation // Get the number of days from the file creation
const diff = this.dateDiffInDays(new Date(Date.now()), this._created); const diff = this.dateDiffInDays(new Date(Date.now()), this.created);
// The session is valid if the number of days is minor than SESSION_TIME // The session is valid if the number of days is minor than SESSION_TIME
let valid = diff < this.SESSION_TIME; const dateValid = diff < this.SESSION_TIME;
if(valid) { // Check the hash
// Check the _hash
const value = `${username}%%%${password}`; const value = `${username}%%%${password}`;
valid = md5(value) === this._hash; const hashValid = md5(value) === this._hash;
}
return valid; // Search for expired cookies
const jarValid = this._cookieJar
.getCookiesSync("https://f95zone.to")
.filter(el => el.TTL() === 0)
.length === 0;
return dateValid && hashValid && jarValid;
} }
//#endregion Public Methods //#endregion Public Methods

View File

@ -4,7 +4,6 @@
import axios, { AxiosResponse } from "axios"; import axios, { AxiosResponse } from "axios";
import cheerio from "cheerio"; import cheerio from "cheerio";
import axiosCookieJarSupport from "axios-cookiejar-support"; import axiosCookieJarSupport from "axios-cookiejar-support";
import tough from "tough-cookie";
// Modules from file // Modules from file
import shared from "./shared.js"; import shared from "./shared.js";
@ -21,13 +20,25 @@ const userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) " +
// @ts-ignore // @ts-ignore
axiosCookieJarSupport.default(axios); axiosCookieJarSupport.default(axios);
/**
* Common configuration used to send request via Axios.
*/
const commonConfig = { const commonConfig = {
/**
* Headers to add to the request.
*/
headers: { headers: {
"User-Agent": userAgent, "User-Agent": userAgent,
"Connection": "keep-alive" "Connection": "keep-alive"
}, },
/**
* Specify if send credentials along the request.
*/
withCredentials: true, withCredentials: true,
jar: new tough.CookieJar() // Used to store the token in the PC /**
* Jar of cookies to send along the request.
*/
jar: shared.session.cookieJar,
}; };
/** /**
@ -38,6 +49,7 @@ export async function fetchHTML(url: string): Promise<Result<GenericAxiosError |
const response = await fetchGETResponse(url); const response = await fetchGETResponse(url);
if (response.isSuccess()) { if (response.isSuccess()) {
// Check if the response is a HTML source code
const isHTML = response.value.headers["content-type"].includes("text/html"); const isHTML = response.value.headers["content-type"].includes("text/html");
const unexpectedResponseError = new UnexpectedResponseContentType({ const unexpectedResponseError = new UnexpectedResponseContentType({
@ -93,7 +105,7 @@ export async function authenticate(credentials: credentials, force: boolean = fa
// Return the result of the authentication // Return the result of the authentication
const result = errorMessage.trim() === ""; const result = errorMessage.trim() === "";
const message = errorMessage.trim() === "" ? "Authentication successful" : errorMessage; const message = result ? "Authentication successful" : errorMessage;
return new LoginResult(result, message); return new LoginResult(result, message);
} }
else throw response.value; else throw response.value;
@ -127,6 +139,7 @@ export async function fetchGETResponse(url: string): Promise<Result<GenericAxios
try { try {
// Fetch and return the response // Fetch and return the response
commonConfig.jar = shared.session.cookieJar;
const response = await axios.get(secureURL, commonConfig); const response = await axios.get(secureURL, commonConfig);
return success(response); return success(response);
} catch (e) { } catch (e) {
@ -142,10 +155,10 @@ export async function fetchGETResponse(url: string): Promise<Result<GenericAxios
/** /**
* Enforces the scheme of the URL is https and returns the new URL. * Enforces the scheme of the URL is https and returns the new URL.
* @returns {String} Secure URL or `null` if the argument is not a string
*/ */
export function enforceHttpsUrl(url: string): string { export function enforceHttpsUrl(url: string): string {
return isStringAValidURL(url) ? url.replace(/^(https?:)?\/\//, "https://") : null; if (isStringAValidURL(url)) return url.replace(/^(https?:)?\/\//, "https://");
else throw new Error(`${url} is not a valid URL`);
}; };
/** /**
@ -159,7 +172,6 @@ export function isF95URL(url: string): boolean {
* Checks if the string passed by parameter has a * Checks if the string passed by parameter has a
* properly formatted and valid path to a URL (HTTP/HTTPS). * properly formatted and valid path to a URL (HTTP/HTTPS).
* @param {String} url String to check for correctness * @param {String} url String to check for correctness
* @returns {Boolean} true if the string is a valid URL, false otherwise
*/ */
export function isStringAValidURL(url: string): boolean { export function isStringAValidURL(url: string): boolean {
// Many thanks to Daveo at StackOverflow (https://preview.tinyurl.com/y2f2e2pc) // Many thanks to Daveo at StackOverflow (https://preview.tinyurl.com/y2f2e2pc)
@ -217,6 +229,7 @@ export async function fetchPOSTResponse(url: string, params: { [s: string]: stri
for (const [key, value] of Object.entries(params)) urlParams.append(key, value); for (const [key, value] of Object.entries(params)) urlParams.append(key, value);
// Shallow copy of the common configuration object // Shallow copy of the common configuration object
commonConfig.jar = shared.session.cookieJar;
const config = Object.assign({}, commonConfig); const config = Object.assign({}, commonConfig);
// Remove the cookies if forced // Remove the cookies if forced