0

I'm writing a session library for Express.js that stores the sessions in encrypted files. The basic interfaces hierarchy is:

export interface Current<T = any> {
    load(): Promise<T>;
    save(value: T): Promise<void>;
}

export interface Manager {
    current<T = any>(): Current<T>;
    create(): Promise<void>;
    delete(): Promise<void>;
    rewind(): void;
}

declare global {
  namespace Express {
    export interface Request {
      session: Manager;
    }
  }
}

Every session is stored in a JSON file, which name is a unique hash id. That hash is returned to the client as a cookie. The implementation it's like that:

import { sessionCrossover } from '.';
import express from 'express';

const app = express();
app.use(sessionCrossover({
    path: './data',
    expires: 1000 * 10,
    hashLength: 126
}));

For create a new session:

// The data structure of every session in this example
interface Data {
    id: number;
    value: string;
}

// Create an endpoint for create a new session
app.get('/create', async (req, res) => {
    try {
        // Get the current session instance
        const current = req.session.current<Data>();    
        if (!current) {
            // Create a new session instance
            await req.session.create();

            // Save in the current session the data. Just in this
            // case, if the session file doesn't exist, it will
            // be created. This method throws the error...
            await req.session
                .current<Data>()
                .save({
                    id: ++id,
                    value: new Date().toJSON()
                });
    
            res.json('Session created sucessfully');
        } else {
            req.session.rewind();
            res.json('Session rewinded...');
        }
    } catch (err) {
        console.error(err);
        res.json(err);
    }
});

Inside of the method that throws the error:

import { File } from '../tool/fsys';
import { Current } from './interfaces';

export class CurrentSession<T = any> implements Current<T> {
    private _file: File;

    // The related method
    save(value: T): Promise<void> {
        if (!this._killed) {
            const text = JSON.stringify(value, null, '    ');
            const byte = Buffer.from(text, 'utf8');

            // Throws the error
            return this._file.write(byte);
        } else {
            return Promise.resolve();
        }
    }
}

And the File class:

import * as fs from 'fs';
import * as fsPromises from 'fs/promises';

export class File extends FSys {
    // The related method
    public write(byte: Buffer): Promise<void> {
        // this._path it's a  protected property of FSys
        return fsPromises.writeFile(this._path, byte);
    }
}

You can set the current hash length in the middleware shown before. The problem is this:

  • When you set a hash of a 126 bytes or more, node.js throws an "ENOENT" error (path not found).
  • When the hash is 125 bytes or less the file with the session it's created normally.

My questions are:

  • Why node.js thows an "ENOENT" (path not found error) when i try to create a file with a large filename?
  • Exists a method to detect a "too large filesize" exception in windows?

Observations:

I tried with different hash byte length. The paths in those cases, are:

  • Hash length = 8; OK
C:\Projects\Node.JS\modules\session-crossover\data\be49c866b0181718.json
  • Hash length = 64; OK
C:\Projects\Node.JS\modules\session-crossover\data\419410c8db26d74563e31b3c0a12e9fb12d31951abe6b280869af47db088c9acaf251de12cd6b6fc51bf3182fa07597add2b48825498d869b99e914c64d42efa.json
  • Hash length = 125; OK
C:\Projects\Node.JS\modules\session-crossover\data\b6f4026e893fb053e626ca3318771a70e0802ca10bfc4ea018e18f35b04aa7f9e365a9883a35eea381d9cb9ad2ca11c8961e0096aacd2802e9e0b4cd96920c073800f40a1224d99a093f7fa0b4eca8799bc84c4fa84db2b8b62df211824271c4d908d3d62defa6f1890e613e04af86bcd04379b57ab3728e0366ed42c9.json
  • Hash length = 126; "ENOENT"
C:\Projects\Node.JS\modules\session-crossover\data\7784ae9a697eb7e5a2ccdf7a9b27c4d182e1e637da4efc47d21a1d48f208f1058bf40f6026dccb79702ea61ea3f4ca307fdeb960a38c89187b0c1b66395934a802ee62769810bd191eb85636d6a86c900299b68fcc1ad6ccfbd83aba863fc181a522cd22d0671148b56d6e4c8051b4366439d6855597caad0eb6a4bba043.json
SleepWritten
  • 334
  • 2
  • 5
  • Does [prefixing the path with `\\?\ `](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#maxpath) (`"\\\\?\\"`) help? – CherryDT Aug 02 '21 at 15:13
  • Do you mean change, for example this path: `c:\\data\\000000000.json` to this other one `c:\\data\\?\\000000000.json` ? – SleepWritten Aug 02 '21 at 15:15
  • @CherryDT no, adding that before the filename in the path doesn't solve the problem. I updated the post with more information about the real paths generated. – SleepWritten Aug 02 '21 at 15:36
  • No I meant `\\?\C:\data\000000000.json` (`"\\\\?\\C:\\data\\000000000.json"` in JavaScript) – CherryDT Aug 02 '21 at 15:45
  • But: The length of the filename you showed isn't 126, it's 252 (257 including `.json`) - so that means it's not an issue with `MAX_PATH` as I thought but you are simply exceeding the [filesystem-specific maximum filename length of 255](https://stackoverflow.com/a/265782/1871033) in NTFS. Other filesystems may have different limits, including shorter ones, so what you are doing isn't a good idea anyway. I suggest truncating the hash to something like 64 characters that almost all filesystems will support. (One can still get issues burning your directory to a CD though as ISO9660's max is 30.) – CherryDT Aug 02 '21 at 15:53
  • Oh, that's makes sense. Therefore do you recommend to establish a max length limit compatible with all OS (for example 64 bytes of length) in the `hashLength` middleware property? – SleepWritten Aug 02 '21 at 15:56
  • Many other frameworks use a session ID that is a UUID, which has 128 bits (16 bytes) of length, which is equal to the address space of IPv6 and more than sufficient. If you are building a system where a 1-in-~340,282,366,920,938,000,000,000,000,000,000,000,000 collision chance is still too high, you should probably not be using random IDs anyway ;-) [for nitpickers: yes this isn't the right way to describe a collision chance because it depends on how many sessions there are etc, but since there will never be that many sessions and one number is so much larger than the other, it doesn't matter] – CherryDT Aug 02 '21 at 16:00
  • I see, in my case i generate the Ids with that process: Get the current server timestamp in milliseconds. Hash the timestamp using Argon2. Search if this hash exists. And while this hash exists, create another. But as you said, the collision is too high, that means my library will generate a lot of useless discarded hashes only to get a hash that doesn't exists in memory. I'll check the UUID for sessions indentification. A lot of thanks for your help. – SleepWritten Aug 02 '21 at 16:09

0 Answers0