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
import * as fs from "fs";
import { promisify } from "util";
import path from "path";
// Public modules from npm
import md5 from "md5";
import tough, { CookieJar } from "tough-cookie";
// Promisifed functions
const areadfile = promisify(fs.readFile);
@ -19,12 +21,15 @@ export default class Session {
/**
* 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 _isMapped: boolean;
private _created: Date;
private _hash: string;
private _token: string;
private _cookieJar: CookieJar;
private _cookieJarPath: string;
//#endregion Fields
@ -50,18 +55,27 @@ export default class Session {
* Token used to login to F95Zone.
*/
public get token() { return this._token; }
/**
* Cookie holder.
*/
public get cookieJar() { return this._cookieJar; }
//#endregion Getters
/**
* Initializes the session by setting the path for saving information to disk.
*/
constructor(path: string) {
this._path = path;
constructor(p: string) {
this._path = p;
this._isMapped = fs.existsSync(this.path);
this._created = new Date(Date.now());
this._hash = 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
@ -122,6 +136,10 @@ export default class Session {
// Write 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);
// Assign values
this._created = json._created;
this._created = new Date(json._created);
this._hash = json._hash;
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> {
if (this.isMapped) {
// Delete the session data
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 {
// 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
let valid = diff < this.SESSION_TIME;
const dateValid = diff < this.SESSION_TIME;
if(valid) {
// Check the _hash
// Check the hash
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

View File

@ -4,7 +4,6 @@
import axios, { AxiosResponse } from "axios";
import cheerio from "cheerio";
import axiosCookieJarSupport from "axios-cookiejar-support";
import tough from "tough-cookie";
// Modules from file
import shared from "./shared.js";
@ -21,13 +20,25 @@ const userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) " +
// @ts-ignore
axiosCookieJarSupport.default(axios);
/**
* Common configuration used to send request via Axios.
*/
const commonConfig = {
/**
* Headers to add to the request.
*/
headers: {
"User-Agent": userAgent,
"Connection": "keep-alive"
},
/**
* Specify if send credentials along the request.
*/
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);
if (response.isSuccess()) {
// Check if the response is a HTML source code
const isHTML = response.value.headers["content-type"].includes("text/html");
const unexpectedResponseError = new UnexpectedResponseContentType({
@ -93,7 +105,7 @@ export async function authenticate(credentials: credentials, force: boolean = fa
// Return the result of the authentication
const result = errorMessage.trim() === "";
const message = errorMessage.trim() === "" ? "Authentication successful" : errorMessage;
const message = result ? "Authentication successful" : errorMessage;
return new LoginResult(result, message);
}
else throw response.value;
@ -127,6 +139,7 @@ export async function fetchGETResponse(url: string): Promise<Result<GenericAxios
try {
// Fetch and return the response
commonConfig.jar = shared.session.cookieJar;
const response = await axios.get(secureURL, commonConfig);
return success(response);
} 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.
* @returns {String} Secure URL or `null` if the argument is not a 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
* properly formatted and valid path to a URL (HTTP/HTTPS).
* @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 {
// 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);
// Shallow copy of the common configuration object
commonConfig.jar = shared.session.cookieJar;
const config = Object.assign({}, commonConfig);
// Remove the cookies if forced