Implement cookie storage on disk
parent
52131a143b
commit
b26c5de21b
|
@ -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}`;
|
const hashValid = md5(value) === this._hash;
|
||||||
valid = 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue