Implement cookie storage on disk
							parent
							
								
									52131a143b
								
							
						
					
					
						commit
						b26c5de21b
					
				| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue