OpenMarshal/npm-WebDAV-Server

View on GitHub
src/server/v2/RequestContext.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import { XML, XMLElement } from 'xml-js-builder'
import { parseIfHeader } from '../../helper/v2/IfParser'
import { WebDAVServer } from './webDAVServer/WebDAVServer'
import { HTTPCodes } from '../HTTPCodes'
import { FileSystem } from '../../manager/v2/fileSystem/FileSystem'
import { ResourceType, ReturnCallback } from '../../manager/v2/fileSystem/CommonTypes'
import { Resource } from '../../manager/v2/fileSystem/Resource'
import { Path } from '../../manager/v2/Path'
import { Errors } from '../../Errors'
import { IUser } from '../../user/v2/IUser'
import * as http from 'http'
import * as url from 'url'
import { promisifyCall } from '../../helper/v2/promise'

export class RequestContextHeaders
{
    contentLength : number
    isSource : boolean
    depth : number
    host : string

    constructor(protected headers : { [name : string] : string | string[] })
    {
        this.isSource = this.find('source', 'F').toUpperCase() === 'T' || this.find('translate', 'T').toUpperCase() === 'F';
        this.host = this.find('Host', 'localhost');

        const depth = this.find('Depth');
        try
        {
            if(depth.toLowerCase() === 'infinity')
                this.depth = -1;
            else
                this.depth = Math.max(-1, parseInt(depth, 10));
        }
        catch(_)
        {
            this.depth = undefined;
        }
        
        try
        {
            this.contentLength = Math.max(0, parseInt(this.find('Content-length', '0'), 10));
        }
        catch(_)
        {
            this.contentLength = 0;
        }
    }

    find(name : string, defaultValue : string = null) : string
    {
        name = name.replace(/(-| )/g, '').toLowerCase();

        for(const k in this.headers)
            if(k.replace(/(-| )/g, '').toLowerCase() === name)
            {
                const value = this.headers[k].toString().trim();
                if(value.length !== 0)
                    return value;
            }
        
        return defaultValue;
    }

    findBestAccept(defaultType : string = 'xml') : string
    {
        const accepts = this.find('Accept', 'text/xml').split(',');
        const regex = {
            'xml': /[^a-z0-9A-Z]xml$/,
            'json': /[^a-z0-9A-Z]json$/
        };

        for(const value of accepts)
        {
            for(const name in regex)
                if(regex[name].test(value))
                    return name;
        }

        return defaultType;
    }
}

export interface RequestedResource
{
    path : Path
    uri : string
}

export interface RequestContextExternalOptions
{
    rootPath ?: string
    headers ?: { [name : string] : string }
    url ?: string
    user ?: IUser
}
export class DefaultRequestContextExternalOptions implements RequestContextExternalOptions
{
    headers : { [name : string] : string } = {
        host: 'localhost'
    }
    url : string = '/'
    user : IUser = {
        isAdministrator: true,
        isDefaultUser: false,
        password: null,
        uid: '-1',
        username: '_default_super_admin_'
    }
}

export class RequestContext
{
    overridePrivileges : boolean
    requested : RequestedResource
    rootPath : string
    headers : RequestContextHeaders
    server : WebDAVServer
    user : IUser
    
    protected constructor(server : WebDAVServer, uri : string, headers : { [name : string] : string | string[] }, rootPath ?: string)
    {
        this.overridePrivileges = false;
        this.rootPath = rootPath;
        this.headers = new RequestContextHeaders(headers);
        this.server = server;
        
        uri = url.parse(uri).pathname;
        uri = uri ? uri : '';
        this.requested = {
            uri,
            path: new Path(uri)
        };
        this.requested.path.decode();

        if(this.rootPath)
        {
            this.rootPath = new Path(this.rootPath).toString(false);
            if(this.rootPath === '/')
                this.rootPath = undefined;
        }
    }
    
    getResourceAsync() : Promise<Resource>
    getResourceAsync(path : Path | string) : Promise<Resource>
    getResourceAsync(path ?: Path | string) : Promise<Resource>
    {
        return promisifyCall((cb) => this.getResource(path, cb));
    }

    getResource(callback : ReturnCallback<Resource>) : void
    getResource(path : Path | string, callback : ReturnCallback<Resource>) : void
    getResource(_path : Path | string | ReturnCallback<Resource>, _callback ?: ReturnCallback<Resource>) : void
    {
        const path = Path.isPath(_path) ? new Path(_path as Path | string) : this.requested.path;
        const callback = _callback ? _callback : _path as ReturnCallback<Resource>;

        this.server.getResource(this, path, callback);
    }

    getResourceSync(path ?: Path | string) : Resource
    {
        path = path ? path : this.requested.path;
        return this.server.getResourceSync(this, path);
    }

    fullUri(uri : string = null) : string
    {
        if(!uri)
            uri = this.requested.uri;

        if(this.server.options.respondWithPaths)
            return this.rootPath ? this.rootPath + uri : uri;
        else
            return (this.prefixUri() + uri).replace(/([^:])\/\//g, '$1/');
    }

    prefixUri() : string
    {
        return 'http://' + this.headers.host.replace('/', '') + (this.rootPath ? this.rootPath : '');
    }
}

export class ExternalRequestContext extends RequestContext
{
    static create(server : WebDAVServer) : ExternalRequestContext
    static create(server : WebDAVServer, callback : (error : Error, ctx : ExternalRequestContext) => void) : ExternalRequestContext
    static create(server : WebDAVServer, options : RequestContextExternalOptions) : ExternalRequestContext
    static create(server : WebDAVServer, options : RequestContextExternalOptions, callback : (error : Error, ctx : ExternalRequestContext) => void) : ExternalRequestContext
    static create(server : WebDAVServer, _options ?: RequestContextExternalOptions | ((error : Error, ctx : ExternalRequestContext) => void), _callback ?: (error : Error, ctx : ExternalRequestContext) => void) : ExternalRequestContext
    {
        const defaultValues = new DefaultRequestContextExternalOptions();

        const options = _options && _options.constructor !== Function ? _options as RequestContextExternalOptions : defaultValues;
        const callback = _callback ? _callback : _options && _options.constructor === Function ? _options as ((error : Error, ctx : ExternalRequestContext) => void) : () => {};

        if(defaultValues !== options)
        {
            for(const name in defaultValues)
                if(options[name] === undefined)
                    options[name] = defaultValues[name];
        }

        const ctx = new ExternalRequestContext(server, options.url, options.headers);

        if(options.user)
        {
            ctx.user = options.user;
            process.nextTick(() => callback(null, ctx));
        }

        return ctx;
    }
}

export class HTTPRequestContext extends RequestContext
{
    responseBody : string
    request : http.IncomingMessage
    response : http.ServerResponse
    exit : () => void

    protected constructor(
        server : WebDAVServer,
        request : http.IncomingMessage,
        response : http.ServerResponse,
        exit : () => void,
        rootPath ?: string
    ) {
        super(server, request.url, request.headers, rootPath);

        this.responseBody = undefined;
        this.response = response;
        this.request = request;
        this.exit = exit;
        
        if(this.response)
        {
            this.response.on('error', (e) => {
                console.error(e);
            });
        }
    }

    static create(server : WebDAVServer, request : http.IncomingMessage, response : http.ServerResponse, callback : (error : Error, ctx : HTTPRequestContext) => void) : void
    static create(server : WebDAVServer, request : http.IncomingMessage, response : http.ServerResponse, rootPath : string, callback : (error : Error, ctx : HTTPRequestContext) => void) : void
    static create(server : WebDAVServer, request : http.IncomingMessage, response : http.ServerResponse, _rootPath : string | ((error : Error, ctx : HTTPRequestContext) => void), _callback ?: (error : Error, ctx : HTTPRequestContext) => void) : void
    {
        const rootPath = _callback ? _rootPath as string : undefined;
        const callback = _callback ? _callback : _rootPath as ((error : Error, ctx : HTTPRequestContext) => void);

        const ctx = new HTTPRequestContext(server, request, response, null, rootPath);
        response.setHeader('DAV', '1,2');
        response.setHeader('Access-Control-Allow-Origin', '*');
        response.setHeader('Access-Control-Allow-Credentials', 'true');
        response.setHeader('Access-Control-Expose-Headers', 'DAV, content-length, Allow');
        response.setHeader('MS-Author-Via', 'DAV');
        response.setHeader('Server', server.options.serverName + '/' + server.options.version);

        if(server.options.headers)
        {
            for(const headerName in server.options.headers)
                response.setHeader(headerName, server.options.headers[headerName]);
        }
        
        const setAllowHeader = (type ?: ResourceType) =>
        {
            const allowedMethods = [];
            for(const name in server.methods)
            {
                const method = server.methods[name];
                if(!method.isValidFor || method.isValidFor(ctx, type))
                    allowedMethods.push(name.toUpperCase());
            }

            response.setHeader('Allow', allowedMethods.join(','));
            callback(null, ctx);
        };

        ctx.askForAuthentication(false, (e) => {
            if(e)
            {
                callback(e, ctx);
                return;
            }

            server.httpAuthentication.getUser(ctx, (e, user) => {
                ctx.user = user;
                if(e && e !== Errors.UserNotFound)
                {
                    if(server.options.requireAuthentification || e !== Errors.MissingAuthorisationHeader)
                        return callback(e, ctx);
                }

                if(server.options.requireAuthentification && (!user || user.isDefaultUser || e === Errors.UserNotFound))
                    return callback(Errors.MissingAuthorisationHeader, ctx);

                server.getFileSystem(ctx.requested.path, (fs, _, subPath) => {
                    fs.type(ctx.requested.path.isRoot() ? server.createExternalContext() : ctx, subPath, (e, type) => {
                        if(e)
                            type = undefined;

                        setAllowHeader(type);
                    })
                })
            })
        })
    }
    
    static encodeURL(url : string)
    {
        return encodeURI(url);
    }

    noBodyExpected(callback : () => void)
    {
        if(this.server.options.strictMode && this.headers.contentLength !== 0)
        {
            this.setCode(HTTPCodes.UnsupportedMediaType);
            this.exit();
        }
        else
            callback();
    }

    checkIfHeader(resource : Resource, callback : () => void)
    checkIfHeader(fs : FileSystem, path : Path, callback : () => void)
    checkIfHeader(_fs : FileSystem | Resource, _path : Path | (() => void), _callback ?: () => void)
    {
        const fs = _callback ? _fs as FileSystem : null;
        const path = _callback ? _path as Path : null;
        let resource = _callback ? null : _fs as Resource;
        const callback = _callback ? _callback : _path as () => void;

        const ifHeader = this.headers.find('If');

        if(!ifHeader)
        {
            callback();
            return;
        }

        if(!resource)
        {
            resource = fs.resource(this, path);
        }

        parseIfHeader(ifHeader)(this, resource, (e, passed) => {
            if(e)
            {
                this.setCode(HTTPCodes.InternalServerError);
                this.exit();
            }
            else if(!passed)
            {
                this.setCode(HTTPCodes.PreconditionFailed);
                this.exit();
            }
            else
                callback();
        });
    }

    askForAuthentication(checkForUser : boolean, callback : (error : Error) => void)
    {
        if(checkForUser && this.user !== null && !this.user.isDefaultUser)
        {
            callback(Errors.AlreadyAuthenticated);
            return;
        }

        const auth = this.server.httpAuthentication.askForAuthentication(this);
        for(const name in auth)
            this.response.setHeader(name, auth[name]);
        callback(null);
    }

    writeBody(xmlObject : XMLElement | object)
    {
        let content = XML.toXML(xmlObject);
        
        switch(this.headers.findBestAccept())
        {
            default:
            case 'xml':
                this.response.setHeader('Content-Type', 'application/xml;charset=utf-8');
                this.response.setHeader('Content-Length', Buffer.from(content).length.toString());
                this.response.write(content, 'UTF-8');
                break;
                
            case 'json':
                content = XML.toJSON(content);
                this.response.setHeader('Content-Type', 'application/json;charset=utf-8');
                this.response.setHeader('Content-Length', Buffer.from(content).length.toString());
                this.response.write(content, 'UTF-8');
                break;
        }

        this.responseBody = content;
    }
    
    setCode(code : number, message ?: string)
    {
        if(!message)
            message = http.STATUS_CODES[code];
        if(!message)
        {
            this.response.statusCode = code;
        }
        else
        {
            this.response.statusCode = code;
            this.response.statusMessage = message;
        }
    }

    protected static defaultErrorStatusCodes = [
        { error: Errors.ResourceNotFound,               code: HTTPCodes.NotFound },
        { error: Errors.Locked,                         code: HTTPCodes.Locked },
        { error: Errors.BadAuthentication,              code: HTTPCodes.Unauthorized },
        { error: Errors.NotEnoughPrivilege,             code: HTTPCodes.Unauthorized },
        { error: Errors.ResourceAlreadyExists,          code: HTTPCodes.Conflict },
        { error: Errors.IntermediateResourceMissing,    code: HTTPCodes.Conflict },
        { error: Errors.WrongParentTypeForCreation,     code: HTTPCodes.Conflict },
        { error: Errors.InsufficientStorage,            code: HTTPCodes.InsufficientStorage },
        { error: Errors.Forbidden,                      code: HTTPCodes.Forbidden }
    ];

    static defaultStatusCode(error : Error) : number
    {
        let code = null;

        for(const errorCode of this.defaultErrorStatusCodes)
        {
            if(errorCode.error === error)
            {
                code = errorCode.code;
                break;
            }
        }
        
        return code;
    }

    setCodeFromError(error : Error) : boolean
    {
        const code = HTTPRequestContext.defaultStatusCode(error);

        if(code)
            this.setCode(code);

        return !!code;
    }
}