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