UNPKG

alchemymvc

Version:
2,888 lines 62.5 kB
const FILECACHE = alchemy.getCache('served_files'),
      RX_TEXT = /svg|xml|javascript|text/i;

var libstream  = alchemy.use('stream'),
    libpath = alchemy.use('path'),
    libua   = alchemy.use('useragent'),
    zlib    = alchemy.use('zlib'),
    BODY    = Symbol('body'),
	TESTED_ROUTES = Symbol('tested_routes'),
    magic,
    fs = alchemy.use('fs'),
    prefixes = alchemy.shared('Routing.prefixes');

/**
 * The Conduit Class
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.2.0
 *
 * @param    {IncomingMessage}   req
 * @param    {ServerResponse}    res
 * @param    {Router}            router
 */
var Conduit = Function.inherits('Alchemy.Base', 'Alchemy.Conduit', function Conduit(req, res, router) {

	// Store the starting time
	this.start = new Date();

	// Create a reference to ourselves
	this.conduit = this;

	// Debug messages for this request
	this.debuglog = [];

	this._debugObject = this.debug({label: 'Initialize Conduit'});
	this._debugConduitInitialize = this._debugObject;

	// Allow use of the log in the views
	if (alchemy.settings.debugging.debug) {
		this.internal('debuglog', {_placeholder_: 'debuglog'});
	}

	// Cookies to send to the client
	this.new_cookies = {};
	this.new_cookie_header = [];

	// The headers to send
	this.response_headers = {};

	// Where the body will go
	this.body = {};

	// Where the files will go
	this.files = {};

	this.initValues();
	this.setReqRes(req, res);
});

/**
 * Deprecated property names
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.0.0
 * @version  1.1.0
 */
Conduit.setDeprecatedProperty('originalPath',    'original_path');
Conduit.setDeprecatedProperty('newCookies',      'new_cookies');
Conduit.setDeprecatedProperty('newCookieHeader', 'new_cookie_header');
Conduit.setDeprecatedProperty('viewRender',      'renderer');
Conduit.setDeprecatedProperty('view_render',     'renderer');
Conduit.setDeprecatedProperty('sceneId',         'scene_id');

/**
 * Return the cookies
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.2.0
 */
Conduit.prepareProperty(function cookies() {
	return String.decodeCookies(this.headers.cookie);
});

/**
 * Return the parsed useragent string
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.5.0
 */
Conduit.prepareProperty(function useragent() {
	return libua.lookup(this.headers['user-agent']);
});

/**
 * Create a Hawkejs Renderer
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.1.5
 */
Conduit.prepareProperty(function renderer() {

	let result;

	if (this.parent && this.parent != this && this.parent.renderer) {
		result = this.parent.renderer.createSubRenderer();
	} else {
		result = alchemy.hawkejs.createRenderer();
	}

	return result;
});

/**
 * Get the SessionScene instance
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.10
 * @version  1.3.10
 */
Conduit.setProperty(function scene() {
	return this.getSession().getScene(this.scene_id);
});

/**
 * The session cookie name to use
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.18
 * @version  1.3.18
 */
Conduit.setProperty(function session_cookie_name() {
	return alchemy.settings.sessions.cookie_name || 'alchemy_sid';
});

/**
 * Enforce the scene_id
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.1.0
 * @version  1.3.18
 */
Conduit.enforceProperty(function scene_id(new_value, old_value) {

	if (!new_value) {
		new_value = this.headers['x-scene-id'] || this.expose('scene_id');

		// If there also was no old value, create a new scene
		if (!new_value && old_value == null) {
			// Generate the scene_id
			new_value = Crypto.randomHex(8) || Crypto.pseudoHex(8);

			// Tell the session this scene can be expected
			this.getSession().expectScene(new_value, this);

			let path = this.request?.url;

			// Set the sceneid cookie
			this.cookie('scene_start_' + ~~(Math.random()*1000), {

				// The time this scene has started
				start: Date.now(),

				// The id of the scene
				id: new_value
			}, {
				// Cookie should only be visible on this path
				path: path,

				// Cookie should not live for more than 15 seconds
				maxAge: 1000 * 15,

				// It should be restricted to the same domain
				domain: false,
			});
		}
	}

	this.expose('scene_id', new_value);

	return new_value;
});

/**
 * Enforce the active_prefix
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.1.0
 * @version  1.1.5
 */
Conduit.enforceProperty(function active_prefix(new_value, old_value) {

	if (!new_value) {
		this.renderer.language = null;
		return null;
	}

	if (new_value == old_value) {
		return new_value;
	}

	// Set the active prefix
	this.internal('active_prefix', new_value);
	this.expose('active_prefix', new_value);

	if (this.locales[0] != new_value) {
		this.locales.unshift(new_value);
	}

	// Set the translate options for use in hawkejs
	this.internal('locales', this.locales);
	this.expose('locales', this.locales);

	let config = Prefix.get(new_value);

	if (config) {
		this.renderer.language = config.locale;
	}

	return new_value;
});

/**
 * Get a session object by id
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.2.0
 */
Conduit.setStatic(function getSessionById(id) {
	return alchemy.sessions.get(id);
});

/**
 * See if this is a secure connection
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.4.2
 * @version  1.0.2
 */
Conduit.setProperty(function is_secure() {

	var protocol;

	if (alchemy.settings.network.assume_https) {
		return true;
	}

	if (this.headers && this.headers['x-forwarded-proto'] == 'https') {
		return true;
	}

	if (this.url && this.url.protocol == 'https:') {
		return true;
	}

	if (this.protocol && this.protocol.startsWith('https')) {
		return true;
	}

	if (this.encrypted == true) {
		return true;
	}

	return false;
});

/**
 * Set the request body
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.1.0
 * @version  1.3.18
 *
 * @param    {Object|string}
 */
Conduit.setMethod(function setRequestBody(body) {

	if (typeof body == 'string') {
		this.body = body;
		return;
	}

	if (!body) {
		return;
	}

	Object.assign(this.body, body);
});

/**
 * Has the given route been tested yet?
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.2.5
 * @version  1.2.5
 *
 * @param    {Route}
 */
Conduit.setMethod(function hasRouteBeenTested(route) {

	if (!route || !this[TESTED_ROUTES]) {
		return false;
	}

	return this[TESTED_ROUTES].has(route);
});

/**
 * Mark this route as having been tested
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.2.5
 * @version  1.2.5
 *
 * @param    {Route}
 */
Conduit.setMethod(function markRouteAsTested(route) {

	if (!this[TESTED_ROUTES]) {
		this[TESTED_ROUTES] = new Set();
	}

	this[TESTED_ROUTES].add(route);
});

/**
 * Rewrite a certain URL parameter
 * (Causing some kind of redirect)
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.2.5
 * @version  1.2.5
 *
 * @param    {string}   route_param
 * @param    {*}        new_value
 */
Conduit.setMethod(function rewriteRequestRouteParam(route_param, new_value) {

	if (!this.rewritten_request_route_param) {
		this.rewritten_request_route_param = {};
	}

	this.rewritten_request_route_param[route_param] = new_value;
});

/**
 * Set the request files
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.1.0
 * @version  1.1.0
 *
 * @param    {Object}
 */
Conduit.setMethod(function setRequestFiles(files) {

	if (!files) {
		return;
	}

	_setRequestFiles(this, files, this.files);
});

/**
 * Set the request files
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.2.0
 * @version  1.3.0
 *
 * @param    {Conduit}   conduit
 * @param    {Array}     files
 * @param    {Object}    target
 */
function _setRequestFiles(conduit, files, target) {

	let context,
	    upload,
	    entry,
	    key;

	for (key in files) {
		entry = files[key];

		if (Array.isArray(entry)) {
			context = target[key];

			if (!context) {
				context = target[key] = {};
			}

			_setRequestFiles(conduit, entry, context);
		} else {
			target[key] = Classes.Alchemy.Inode.File.fromUntrusted(entry);
		}
	}
}

/**
 * Don't convert a conduit to any special json data
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.2.0
 */
Conduit.setMethod(function toJSON() {
	return null;
});

/**
 * Set the request & response objects
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.2.0
 * @version  1.2.0
 */
Conduit.setMethod(function setReqRes(req, res) {

	if (req != null) {
		// Make conduit available in req
		req.conduit = this;

		// Basic HTTP objects
		this.request = req;

		// The HTTP request headers
		this.headers = req.headers;

		// Parse the original URL without host
		this.original_url = new RURL(req.url);

		// Is this an AJAX request?
		this.ajax = null;
	}

	if (res != null) {
		this.response = res;
	}
});

/**
 * Init values
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.3.3
 * @version  1.3.15
 */
Conduit.setMethod(function initValues() {

	// Use passed-along router, or default router instance
	this.router = this.router || Router;

	// The path without any prefix, including section mounts
	this.path = null;

	// The path without prefix or section mount
	this.sectionPath = null;

	// The accepted languages
	this.languages = null;

	// URL paths can be prefixed with certain locales,
	// these locales should then get preference over the user's browser locale
	this.prefix = null;

	// All the locales the user's browser accepts
	this.locales = null;

	// The matching Route instance
	this.route = null;

	// The named parameters inside the path
	this.params = null;

	// The original string parameters
	this.route_string_parameters = null;

	// The section vhost domain
	this.sectionDomain = null;

	// The section of the used route
	this.section = null;

	// The parsed path (including querystring)
	this.url = null

	// The current active theme
	this.theme = null;

	// Make sure the tested routes are reset
	this[TESTED_ROUTES] = null;
});

/**
 * Get the time since the conduit was made
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.1.0
 */
Conduit.setMethod(function time() {
	return Date.now() - this.start;
});

/**
 * Parse the request, get information from the url
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.3.18
 *
 * @param    {IncomingMessage}   req
 * @param    {ServerResponse}    res
 */
Conduit.setMethod(async function parseRequest() {

	if (this.method == null && this.request && this.request.method) {
		this.method = this.request.method.toLowerCase();
	}

	this.parseUrl();
	this.parseShortcuts();
	this.parseLanguages();
	this.parsePrefix();
	this.parseSection();

	// Try getting the route
	await this.parseRoute();

	if (this.halt_request) {
		return false;
	}

	// Is this encrypted?
	if (this.encrypted == null) {
		this.encrypted = this.request.connection.encrypted;
	}

	if (this.rewritten_request_route_param) {
		let params = Object.assign({}, this.route_string_parameters, this.rewritten_request_route_param);
		let new_url = this.route.generateUrl(params, this);
		this.overrideResponseUrl(new_url);
	}

	this.parseUrl();
});

/**
 * Parse the complete request URL if it hasn't been done yet
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.18
 * @version  1.3.19
 */
Conduit.setMethod(function parseUrl() {

	// If the url has already been parsed, return early
	if (this.url) {
		return false;
	}

	let protocol;

	if (alchemy.settings.network.assume_https) {
		protocol = 'https://';
	} else if (this.headers['x-forwarded-proto']) {
		protocol = this.headers['x-forwarded-proto'];
	} else if (this.headers[':scheme']) {
		protocol = this.headers[':scheme'];
	} else if (this.protocol) {
		protocol = this.protocol;
	} else if (this.encrypted) {
		protocol = 'https://';
	} else {
		protocol = 'http://';
	}

	// Create a new RURL instance
	this.url = new RURL();

	// Set the protocol
	this.url.protocol = protocol;

	// Get the host/domain/authority of the request
	const host = this.headers[':authority'] || this.headers.host;

	// Set the host
	this.url.hostname = host;

	// If no URL was set, use the first requests URL (without the path)
	if (!alchemy.settings.network.main_url && host) {
		alchemy.setUrl(this.url);
	}

	// Set the entire original path
	this.url.path = this.original_path;

	return true;
});

/**
 * Parse the headers for shortcuts
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.3.17
 */
Conduit.setMethod(function parseShortcuts() {

	var headers = this.headers;

	// A request can just tell us what route to use
	if (headers['x-alchemy-route-name']) {
		this.route = this.router.getRouteByName(headers['x-alchemy-route-name']);
	}

	// And which prefix (this is a forced prefix)
	if (headers['x-alchemy-prefix'] && prefixes[headers['x-alchemy-prefix']]) {
		this.prefix = headers['x-alchemy-prefix'];
	}

	// Section domains can only be requested through headers
	if (headers['x-alchemy-section-domain']) {
		this.sectionDomain = headers['x-alchemy-section-domain'];
	}

	// Only get ajax on the first parse
	if (this.ajax == null) {
		this.ajax = headers['x-requested-with'] === 'XMLHttpRequest';
	}

	// Is this request coming from a webview?
	if (this.in_webview == null) {
		this.in_webview = headers['x-protoblast-webview'] === 'true';
	}
});

/**
 * Sort the parsed accept-language header array
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.0.1
 * @version  0.0.1
 *
 * @param    {Object}   a
 * @param    {Object}   b
 */
function qualityCmp(a, b) {
	if (a.quality === b.quality) {
		return 0;
	} else if (a.quality < b.quality) {
		return 1;
	} else {
		return -1;
	}
}

/**
 * Parses the HTTP accept-language header
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.0.1
 * @version  0.2.0
 */
Conduit.setMethod(function parseLanguages() {

	var rawLangs,
	    rawLang,
	    locales,
	    parts,
	    langs,
	    qval,
	    temp,
	    i,
	    q;

	langs = [];
	locales = [];

	if (this.headers['accept-language']) {

		rawLangs = this.headers['accept-language'].split(',');

		for (i = 0; i < rawLangs.length; i++) {
			rawLang = rawLangs[i];

			parts = rawLang.split(';');
			qval = null;
			q = 1;

			if (parts.length > 1 && parts[1].indexOf('q=') === 0) {
				qval = parseFloat(parts[1].split('=')[1]);

				if (isNaN(qval) === false) {
					q = qval;
				}
			}

			// Get the lang-loc code
			temp = parts[0].trim().toLowerCase().split('-');

			langs.push({lang: temp[0], loc: temp[1], quality: q});
		}

		langs.sort(qualityCmp);
	};

	temp = {};

	for (i = 0; i < langs.length; i++) {
		if (!temp[langs[i].lang]) {
			locales.push(langs[i].lang);
			temp[langs[i].lang] = true;
		}
	}

	this.languages = langs;
	this.locales = locales;
});

/**
 * Parses accept-encoding strings
 *
 * @author   jshttp
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.2.0
 */
function parseEncoding(s, i) {
	var match = s.match(/^\s*(\S+?)\s*(?:;(.*))?$/);

	if (!match) return null;

	var encoding = match[1];
	var q = 1;
	if (match[2]) {
		var params = match[2].split(';');
		for (var i = 0; i < params.length; i ++) {
			var p = params[i].trim().split('=');
			if (p[0] === 'q') {
				q = parseFloat(p[1]);
				break;
			}
		}
	}

	return {
		encoding: encoding,
		q: q,
		i: i
	};
}

/**
 * Parses accept-encoding strings
 *
 * @author   jshttp
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.2.0
 */
function specify(encoding, spec, index) {
	var s = 0;
	if(spec.encoding.toLowerCase() === encoding.toLowerCase()){
		s |= 1;
	} else if (spec.encoding !== '*' ) {
		return null
	}

	return {
		i: index,
		o: spec.i,
		q: spec.q,
		s: s
	}
};

/**
 * Parses the HTTP accept-encoding header
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.2.0
 */
Conduit.setMethod(function parseAcceptEncoding() {

	var hasIdentity,
	    minQuality,
	    encoding,
	    accepts,
	    i,
	    j;

	// Make sure this only runs once
	if (this.accepted_encodings != null) {
		return;
	}

	if (!this.headers['accept-encoding']) {
		this.accepted_encodings = false;
		return;
	}

	accepts = this.headers['accept-encoding'].split(',');
	minQuality = 1;

	for (i = 0, j = 0; i < accepts.length; i++) {
		encoding = parseEncoding(accepts[i].trim(), i);

		if (encoding) {
			accepts[j++] = encoding;
			hasIdentity = hasIdentity || specify('identity', encoding);
			minQuality = Math.min(minQuality, encoding.q || 1);
		}
	}

	if (!hasIdentity) {
		/*
		 * If identity doesn't explicitly appear in the accept-encoding header,
		 * it's added to the list of acceptable encoding with the lowest q
		 */
		accepts[j++] = {
			encoding: 'identity',
			q: minQuality,
			i: i
		};
	}

	// trim accepts
	accepts.length = j;

	this.accepted_encodings = accepts;
});

/**
 * See if the wanted encoding is accepted by the client
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.2.0
 */
Conduit.setMethod(function accepts(encoding) {

	var i;

	// Parse the encodings on the fly
	this.parseAcceptEncoding();

	if (!this.accepted_encodings) {
		return false;
	}

	for (i = 0; i < this.accepted_encodings.length; i++) {
		if (this.accepted_encodings[i].encoding == encoding) {
			return true;
		}
	}

	return false;
});

/**
 * Create a loopback conduit
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.1.3
 *
 * @param    {Object}   args
 * @param    {Function} callback
 *
 * @return   {Alchemy.LoopbackConduit}
 */
Conduit.setMethod(function loopback(args, callback) {
	return Classes.Alchemy.Conduit.Loopback.create(this, args, callback);
});

/**
 * Parse the request, get information from the url
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.1.0
 */
Conduit.setMethod(function parsePrefix() {

	let path = this.original_path;

	if (!path) {
		return;
	}

	let active_prefix,
	    prefix,
		begin;

	// Look for the prefix at the beginning of the url path
	if (!this.prefix) {
		for (prefix in prefixes) {
			begin = '/' + prefix + '/';

			if (path.indexOf(begin) === 0) {
				this.prefix = prefix;
				break;
			}
		}
	}

	// Handle urls with ONLY the prefix and no ending slash
	if (!this.prefix) {
		for (prefix in prefixes) {
			begin = '/' + prefix;

			if (this.original_pathname == begin) {
				this.prefix = prefix;
				break;
			}
		}

		if (this.prefix && path.endsWith('/' + this.prefix)) {
			this.path = '/';
		} else if (this.prefix && path.startsWith('/' + this.prefix + '?')) {
			this.path = '/?' + path.after('?');
		} else {
			this.path = path;
		}

	} else if (this.prefix && path.indexOf('/' + this.prefix + '/') === 0) {
		// Remove the prefix from the path if one is given
		this.path = path.slice(this.prefix.length+1);
	} else {
		this.path = path;
	}

	// Add this prefix to the top of the locales
	if (this.prefix) {
		active_prefix = this.prefix;
		this.locales.unshift(this.prefix);

		// Remember this prefix in the session
		this.session('last_forced_prefix', this.prefix);

		// Let the client know this prefix should be used
		this.expose('forced_prefix', this.prefix);
	} else {

		let last_forced_prefix = this.session('last_forced_prefix');

		if (last_forced_prefix) {
			active_prefix = last_forced_prefix;
		} else if (this.active_prefix) {
			// There already is an active prefix, so just keep on using that
			// (Is the case in redirects)
			return;
		} else {

			// If no prefix has been found yet, look for the default prefix
			// This will override the browser locale
			if (this.headers['x-alchemy-default-prefix']) {
				if (prefixes[this.headers['x-alchemy-default-prefix']]) {
					active_prefix = this.headers['x-alchemy-default-prefix'];

					if (this.locales[0] != active_prefix) {
						this.locales.unshift(active_prefix);
					}
				}
			}

			if (!active_prefix) {
				active_prefix = this.locales[0];
			}
		}
	}

	this.active_prefix = active_prefix;
});

/**
 * Get the section
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.2.1
 */
Conduit.setMethod(function parseSection() {

	// Get the section this path is using
	this.section = this.router.getPathSection(this.path);

	if (!this.section) {
		log.warn('No section found for path "' + this.path + '"');
	}

	// If the section has a parent it's not the root
	if (this.section && this.section.parent) {
		this.sectionPath = this.path.slice(this.section.mount.length) || '/';
	} else {
		this.sectionPath = this.path;
	}
});

/**
 * Get a route by its name
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.2.0
 *
 * @param    {string|Object}   name   The name of the route
 */
Conduit.setMethod(function getRouteByName(name) {

	// See if the name is an object, which means it's for sockets
	if (name && typeof name == 'object') {
		this.route = name;
	} else {
		this.route = this.router.getRouteByName(name);
	}

	return this.route;
});

/**
 * Get the Route instance & named parameters
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.3.0
 *
 * @param    {Route}   after_route   Only check routes after this one
 *
 * @return   {boolean}   Continue processing this request or not?
 */
Conduit.setMethod(async function parseRoute(after_route) {

	var temp;

	this.section = this.router.getPathSection(this.path);

	// Remove the current found route
	if (after_route) {
		this.route_rematch = true;
		this.route = null;
	}

	// If the route hasn't been found in the header shortcuts yet, look for it
	if (!this.route) {

		temp = this.router.getRouteBySectionPath(this, this.method, this.section, this.sectionPath, this.prefix, after_route);

		if (temp && temp.then) {
			temp = await temp;
		}

		if (temp) {
			this.route = temp.route;
			this.setRouteParameters(temp.parameters);
			this.route_string_parameters = temp.original_parameters;
			this.path_definition = temp.definition;
		} else {
			// Is this a HEAD request? Then we need to check if a GET exists
			if (this.method == 'head') {
				let get_route = this.router.getRouteBySectionPath(this, 'get', this.section, this.sectionPath, this.prefix, after_route);

				if (get_route && get_route.then) {
					get_route = await get_route;
				}

				// A GET route was found, so we just need to end this request
				if (get_route) {
					this.end();
					this.halt_request = true;
					return;
				}
			} else {
				// See if the path matches another method
				temp = await this.router.getRouteBySectionPath(this, ['get', 'post', 'put'], this.section, this.sectionPath, this.prefix, after_route);

				if (temp) {
					this.route_mismatch = temp.route;

					temp = null;
				}
			}

			this.route_not_found = true;
		}
	} else {
		temp = this.route.match(this, this.method, this.sectionPath);

		if (temp && temp.then) {
			temp = await temp;
		}

		if (temp) {
			this.setRouteParameters(temp.parameters);
			this.route_string_parameters = temp.original_parameters || {};
			this.path_definition = temp.definition;
		} else {
			this.setRouteParameters();
		}
	}
});

/**
 * Run the middleware
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.1.5
 */
Conduit.setMethod(async function callMiddleware() {

	if (!this.section) {
		return this.callHandler();
	}

	let that = this,
	    middlewares = await this.section.getMiddleware(this, this.section, this.path, this.prefix),
	    debugObject = this._debugObject,
	    middleDebug = this.debug({label: 'middleware', data: {title: 'Doing middleware'}}),
	    routeDebug,
	    theme;

	if (middleDebug) {
		this._debugObject = middleDebug;
	}

	middlewares = new Iterator(middlewares);

	Function.while(function test() {
		return middlewares.hasNext();
	}, function middlewareTask(next) {

		var route = middlewares.next().value,
		    middlePath,
		    req;

		// Skip middleware that does not listen to the request method
		if (route.methods.indexOf(that.method) === -1) {
			return next();
		}

		// Augment the request object
		req = Object.create(that.request);

		// Get the path without the middleware mount path
		middlePath = req.conduit.sectionPath.replace(route.paths[''].source, '');

		// Strip any query parameters
		if (middlePath.indexOf('?') > -1) {
			middlePath = middlePath.before('?');
		}

		if (middlePath[0] !== '/') {
			middlePath = '/' + middlePath;
		}

		// Look for theme settings
		if (req.conduit.url) {
			theme = req.conduit.url.query.theme;

			if (theme) {
				middlePath = ['/' + theme + middlePath, middlePath];
			}
		}

		req.middlePath = middlePath;
		req.original = that.request;

		if (routeDebug) {
			routeDebug.stop();
		}

		if (middleDebug) {
			routeDebug = middleDebug.debug('route', {title: 'Doing "' + route.name + '"'});
			that._debugObject = routeDebug;
		}

		route.fnc(req, that.response, next);
	}, function done(err) {

		if (err) {
			return that.emit('error', err);
		}

		if (routeDebug) {
			routeDebug.stop();
		}

		// Don't do this for websockets
		if (that.websocket) {
			return;
		}

		if (middleDebug) {
			middleDebug.mark('Preparing viewrender');
		}

		// An action will be called,
		// so we can already prepare the viewrender now.
		that.prepareViewRender();

		if (middleDebug) {
			middleDebug.mark(false);
			middleDebug.stop();
		}

		if (that._debugConduitInitialize) {
			that._debugConduitInitialize.stop();
		}

		// Return the original debug object
		that._debugObject = debugObject;

		that.callHandler();
	});
});

/**
 * Create a new Hawkejs' ViewRender instance
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.3.17
 */
Conduit.setMethod(function prepareViewRender() {

	if (this.renderer.conduit) {
		return;
	}

	// Add a link to this conduit
	this.renderer.conduit = this;
	this.renderer.server_var('conduit', this);

	// Let the ViewRender get some request info
	this.renderer.prepare(this.request, this);

	// Pass url parameters to the client
	this.renderer.internal('urlparams', this.route_string_parameters);
	this.renderer.internal('url', this.url);

	if (this.route) {
		this.renderer.internal('route', this.route.name);
	}

	this.renderer.is_for_client_side = this.ajax;
});

/**
 * Call the handler of this route when parsing is finished
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.3.16
 */
Conduit.setMethod(function callHandler() {

	if (!this.route) {

		if (this.route_mismatch) {

			if (alchemy.settings.debugging.debug) {
				console.log('Route method not allowed:', this);
			}

			this.error(405, 'Method Not Allowed', false);

		} else {
			if (alchemy.settings.debugging.debug && this.original_path.indexOf('.js.map') == -1) {
				console.log('Route not found:', this);
			}

			this.notFound('Route was not found');
		}

		return;
	}

	this.route.callHandler(this);
});

/**
 * Put this request in a queue
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.1
 * @version  1.3.1
 *
 * @param    {Object}   options   Options or url
 */
Conduit.setMethod(function postponeAndQueue(options) {

	if (!options) {
		options = {};
	}

	const postponement = this.postponeRequest({
		put_in_queue : true,
	});

	return postponement;
});

/**
 * Postpone the response and the request
 *
 * This does not stop the current request from processing.
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.1
 * @version  1.3.1
 *
 * @param    {number|Object}   options   Options or time to wait
 *
 * @return   {Alchemy.Conduit.Postponement}
 */
Conduit.setMethod(function postponeRequest(options) {

	let postponement = this.postponeResponse(options);

	this.afterOnce('get-postponed-response', () => {
		this.callMiddleware();
	});

	return postponement;
});

/**
 * End the current request with a 202 status
 * and tell the client to look at another url later.
 *
 * This does not stop the current request from processing.
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.1.0
 * @version  1.3.1
 *
 * @param    {number|Object}   options   Options or time to wait
 *
 * @return   {Alchemy.Conduit.Postponement}
 */
Conduit.setMethod(function postponeResponse(options) {

	if (typeof options == 'number') {
		options = {
			expected_duration: options
		};
	} else if (!options) {
		options = {};
	}

	return this._postpone(options);
});

/**
 * Handle the postponement
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.1
 * @version  1.3.1
 *
 * @param    {Object}   options
 *
 * @return   {Alchemy.Conduit.Postponement}
 */
Conduit.setMethod(function _postpone(options) {

	let session = this.getSession();

	let postponement = session.getExistingPostponement(this);

	if (postponement) {
		return postponement.showPostponementMessage(this);
	}

	let response = this.response;

	this.postponed_response = response;

	// Make sure the scene id exists
	this.createScene();

	postponement = session.postpone(this, options);

	if (options.put_in_queue) {
		postponement.putInQueue();
	}

	if (options.show_postponement_message !== false) {
		postponement.showPostponementMessage();
	}

	// Nullify the response
	this.response = null;

	// Set the original url
	this.overrideResponseUrl(this.url);

	// Return the postponement
	return postponement;
});

/**
 * Set the response url
 *
 * @deprecated   Use {@link #setResponseUrl} instead
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.2.5
 * @version  1.3.0
 *
 * @param    {string|RURL}   url
 */
Conduit.setMethod(function overrideResponseUrl(url) {
	return this.setResponseUrl(url);
});

/**
 * Set the response url
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.0
 * @version  1.3.0
 *
 * @param    {string|RURL|boolean}   new_url
 */
Conduit.setMethod(function setResponseUrl(new_url) {

	if (new_url == null) {
		return;
	}

	if (!new_url) {
		this.renderer.history = false;
		return;
	} else {
		this.renderer.history = true;
	}

	if (typeof new_url != 'string') {
		new_url = String(new_url);
	}

	this.setHeader('x-history-url', new_url);
	this.expose('redirected_to', new_url);
});

/**
 * Redirect to another url
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.3.17
 *
 * @param    {number}          status    3xx redirection codes. 302 (temporary redirect) by default
 * @param    {string|Object}   options   Options or url
 */
Conduit.setMethod(function redirect(status, options) {

	let hard_refresh = false,
	    url;

	if (typeof status != 'number') {

		if (typeof options == 'object') {
			options.url = status;
		} else {
			options = status;
		}

		status = 302;
	}

	if (typeof options == 'object' && options) {

		if (options.href || options.path) {
			url = options.href || options.path;
		} else {

			if (options.body) {
				Object.defineProperty(this, 'body', {
					value        : options.body,
					configurable : true
				});
			}

			if (options.method) {
				this.method = options.method;
			}

			// When headers are given, the redirect is internal
			if (options.headers) {
				this.headers = options.headers;

				this.oldoriginal_path = this.original_path;

				if (typeof options.url == 'string') {
					let temp = options.url;
					temp = RURL.parse(temp);
					url = temp.path;
				} else {
					url = options.url.path;
				}

				if (url == null) {
					throw new Error('Conduit#redirect can not redirect to null path');
				}

				// Register the new url as the one to use for the history
				this.overrideResponseUrl(url);

				this.original_path = url;

				// Reinitialize the conduit
				this.initValues();
				this.initHttp();

				return;
			} else {
				url = options.url;
			}
		}

		if (options.hard_refresh) {
			hard_refresh = options.hard_refresh;
		}

	} else if (typeof options == 'string') {
		url = options;
		options = null;
	} else {
		throw new Error('Conduit#redirect requires a valid url or options object');
	}

	this.status = status;

	// Make sure the url is an internal one if no hard refresh is requested
	if (!hard_refresh && alchemy.settings.network.main_url) {
		let rurl = RURL.parse(url);

		// If an explicit hostname is set, this might be an external url
		if (rurl.hostname) {
			let base_url = RURL.parse(alchemy.settings.network.main_url);

			if (base_url.hostname != rurl.hostname) {
				hard_refresh = true;
			}
		}
	}

	if (options?.popup) {
		this.setHeader('x-hawkejs-popup', options.popup);

		if (options.popup_url) {
			this.setHeader('x-hawkejs-popup-url', options.popup_url);
		}

		hard_refresh = true;
	}

	if (hard_refresh && this.headers['x-hawkejs-request']) {
		this.setHeader('x-hawkejs-navigate', url);
	} else {
		this.setHeader('Location', url);
	}

	this._end();
});

/**
 * Respond with an error
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.3.21
 *
 * @param    {Nulber}  status       Response statuscode
 * @param    {Error}   message      Optional error to send
 * @param    {boolean} print_error  Print the error, defaults to true
 */
Conduit.setMethod(function error(status, message, print_error) {

	let print_dev = false;

	if (status instanceof Classes.Alchemy.Error.HTTP) {
		message = status;
		status = message.status;
	}

	if (typeof status !== 'number') {
		message = status;
		status = 500;
	}

	if (!message) {
		message = 'Unknown server error';
	}

	if (typeof message == 'string') {
		let error = new Classes.Alchemy.Error.HTTP(status, message);
		error[Symbol.for('extra_skip_levels')] = 1;
		message = error;
	}

	let is_400 = (status >= 400 && status <= 500);

	if (alchemy.settings.environment == 'dev') {
		print_dev = true;

		if (is_400 && this.original_path && this.original_path.indexOf('.js.map') > -1) {
			print_dev = false;
			print_error = false;
		}
	}

	if (print_error == null) {
		if (is_400) {
			print_error = false;
		} else {
			print_error = true;
		}
	}

	if (print_dev) {
		let subject = 'Error found on ' + this.original_path + '';
		log.error(subject, message, this);
	} else if (print_error) {
		let subject = 'Error found on ' + this.original_path + '';

		if (is_400) {
			log.error(subject + ':\n' + message);
		} else if (message instanceof Error) {
			alchemy.printLog('error', [subject, String(message), message], {err: message, level: -2});
		} else {
			log.error(subject + ':\n' + message);
		}
	}

	// Make sure the client doesn't expect compression
	this.setHeader('content-encoding', '');

	this.status = status;

	// Only render an expensive "Error" template when the client directly
	// browses to an HTML page.
	// Don't render a template for AJAX or asset requests
	if (this.renderer && (this.ajax || (this.headers.accept && this.headers.accept.indexOf('html') > -1))) {

		// Hawkejs will have the option to throw the error OR render the error
		if (this.ajax) {
			this.end({
				error           : true,
				status          : status,
				message         : message,
				error_templates : ['error/' + status, 'error/unknown'],
			});
		} else {
			this.set('status', status);
			this.set('message', message);

			if (alchemy.isTooBusyForRequests()) {
				this._end(`Error ${status}:\n${message}`);
			} else {
				this.render(['error/' + status, 'error/unknown']);
			}
		}
	} else {
		// Requests for images or scripts just get a non-expensive string response
		this.setHeader('content-type', 'text/plain');
		this._end(status + ':\n' + message + '\n');
	}

	// Emit this as a conduit error
	alchemy.emit('conduit_error', this, status, message);
});

/**
 * Deny access
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.1.0
 *
 * @param    {number}  status
 * @param    {Error}   message   optional error to send
 */
Conduit.setMethod(function deny(status, message) {

	if (typeof status == 'string') {
		message = status;
		status = 403;
	} else if (status instanceof Classes.Alchemy.Error.HTTP) {
		return this.error(status);
	}

	if (message == null) {
		message = 'Forbidden';
	}

	this.error(status, message);
});

/**
 * The current user is not authorized and needs to log in
 * (Default implementation, is overriden by the acl plugin)
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.0.7
 * @version  1.0.7
 *
 * @param    {boolean}   tried_auth   Indicate that this was an auth attempt
 */
Conduit.setMethod(function notAuthorized(tried_auth) {
	return this.deny();
});

/**
 * The current user is authenticated, but not allowed
 * (Default implementation, is overriden by the acl plugin)
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.0.7
 * @version  1.1.0
 */
Conduit.setMethod(function forbidden() {

	let error = new Classes.Alchemy.Error.HTTP(403, 'Forbidden');
	error.skipTraceLines(1);

	return this.deny(error);
});

/**
 * Respond with a not found error status
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.1.0
 *
 * @param    {Error}   message   optional error to send
 */
Conduit.setMethod(async function notFound(message) {

	// Look for other paths
	if (!this.route_not_found && this.route && !this.route_rematch) {

		// Try matching against paths after the ones we currently matched
		await this.parseRoute(this.route);

		// Call the handler of that route if it has been found
		if (this.route) {
			return this.route.callHandler(this);
		}
	}

	if (message == null) {
		message = 'Not found';
	}

	this.error(404, message, false);
});

/**
 * Respond with a "Not Modified" 304 status
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.4.0
 */
Conduit.setMethod(function notModified() {
	this.status = 304;
	this._end();
});

/**
 * Respond with text. Objects get JSON-dry encoded
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.3.0
 *
 * @param    {string|Object}   message
 */
Conduit.setMethod(function end(message) {

	var that = this,
	    json_type,
	    json_fnc,
	    cache,
	    etag,
	    temp;

	if (this.websocket) {
		throw new Error('You can not end a websocket, use the callback instead');
	}

	if (this.method == 'head') {
		return this._end();
	}

	if (typeof message !== 'string') {

		if (message && message instanceof Classes.Stream.Stream) {
			return this._endWithStream(message);
		}

		// Use regular JSON if DRY has been disabled in settings
		if (alchemy.settings.network.use_json_dry_response === false || this.json_dry === false) {
			json_type = 'json';
			json_fnc = JSON.stringify;
		} else {
			json_type = 'json-dry';
			json_fnc = JSON.dry;

			// Clone the object
			message = JSON.clone(message, 'toHawkejs');
		}

		// Only send the mimetype if it hasn't been set yet
		if (this.setHeader('content-type') == null) {
			this.setHeader('content-type', "application/" + json_type + ";charset=utf-8");
		}

		message = json_fnc(message) || 'null';
	}

	cache = this.headers['cache-control'] || this.headers['pragma'];

	// Only generate etags when caching is enabled locally & on the browser
	if (alchemy.settings.performance.cache !== false && (cache == null || cache != 'no-cache')) {

		// Calculate the hash as etag
		etag = alchemy.checksum(message);

		if (etag != null) {

			if (this.headers['if-none-match'] == etag) {
				return this.notModified();
			}

			// Responses through `end` should always be privately cached
			this.setHeader('cache-control', 'private');

			// Send the hash as a response header
			this.setHeader('etag', etag);
		}
	}

	// No need to replace anything if debugging is disabled or the  log is empty
	if (alchemy.settings.debugging.debug && this.debuglog && this.debuglog.length && message.indexOf('_placeholder_') > -1) {
		temp = JSON.dry(this.debuglog);
		message = message.replace(/{\s*"_placeholder_":\s*"debuglog"\s*}/g, temp);
		message = message.replace(/{\s*\\"_placeholder_\\":\s*\\"debuglog\\"\s*}/g, JSON.stringify(temp).slice(1,-1));
	}

	// Compress the output if the client accepts it,
	// but only if the file is at least 150 bytes
	if (alchemy.settings.network.use_compression !== false && message.length > 150 && this.accepts('gzip')) {

		// Set the decompressed content-length for use in progress bars
		this.setHeader('x-decompressed-content-length', Buffer.byteLength(message));

		// Set the gzip header
		this.setHeader('content-encoding', 'gzip');

		// Make sure proxy servers only cache this under this content-encoding type
		this.setHeader('vary', 'accept-encoding');

		zlib.gzip(message, function gotZipped(err, zipped) {
			that._end(zipped, 'utf-8');
		});

		return;
	}

	this._end(message, 'utf-8');
});

/**
 * End with a stream
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.22
 * @version  1.3.22
 */
Conduit.setMethod(function _endWithStream(stream) {

	this.ended = new Date();
	this.emit('ending');

	this.flushHeaders();

	stream.pipe(this.response);
});

/**
 * Call the actual end method
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.3.22
 */
Conduit.setMethod(function _end(message, encoding = 'utf-8') {

	this.ended = new Date();

	this.emit('ending');

	if (!this.response) {
		let args = [];

		if (arguments.length) {
			args.push(message);
			args.push(encoding);
		}

		this._end_arguments = args;

		this.emit('after-postponed-end', args);

		return;
	}

	// Set the content-length if it hasn't been set yet
	if (arguments.length > 0 && !this.response_headers['content-length']) {
		this.response_headers['content-length'] = Buffer.byteLength(message);
	}

	this.flushHeaders();

	if (arguments.length === 0) {
		return this.response.end();
	}

	// End the response
	return this.response.end(message, encoding);
});

/**
 * Flush the headers
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.22
 * @version  1.3.22
 */
Conduit.setMethod(function flushHeaders() {

	if (this._flushed) {
		return;
	}

	this._flushed = true;

	if (this.status) {
		this.response.statusCode = this.status;
	}

	let value,
	    key;

	for (key in this.response_headers) {
		value = this.response_headers[key];

		if (value == null) {
			continue;
		}

		this.response.setHeader(key, value);
	}

	if (this.new_cookie_header.length) {
		this.response.setHeader('set-cookie', this.new_cookie_header);
	}

	// Write the actual headers
	this.response.writeHead(this.status);
});

/**
 * Send a response to the client
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.2.0
 */
Conduit.setMethod(function send(content) {
	return this.end(content);
});

/**
 * Create the scene id (if it doesn't exist already)
 * We do this using cookies, so the HTML response can be cached
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.1.0
 */
Conduit.setMethod(function createScene() {
	return this.scene_id;
});

/**
 * Render a view and send it to the client
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.3.17
 */
Conduit.setMethod(function render(template_name, options, callback) {

	let that = this,
	    templates;

	if (typeof options == 'function') {
		callback = options;
		options = {};
	} else if (options == null) {
		options = {};
	}

	// Make sure the viewrender is prepared
	// (Fallback)
	this.prepareViewRender();

	if (template_name) {
		templates = [template_name];
	}

	if (templates) {
		templates.push('error/404');
	}

	// Expose the useragent info to the hawkejs renderer
	this.internal('useragent', this.useragent);

	this.createScene();

	// Pass along the clientRender property,
	// can be used to force rendering HTML
	if (options.clientRender != null) {
		this.renderer.clientRender = options.clientRender;
	}

	if (this.layout) {
		this.renderer.layout = this.layout;
	}

	this.renderer.renderHTML(templates).done(function afterRender(err, html) {

		var mimetype;

		if (err != null) {

			if (callback) {
				return callback(err);
			}

			throw err;
		}

		that.registerBindings();

		if (typeof html !== 'string') {

			// Stringify using json-dry
			html = JSON.dry(html);

			// Tell the client to expect a json-dry response
			mimetype = 'application/json-dry';
		} else {
			mimetype = 'text/html';
		}

		// Only send the mimetype if it hasn't been set yet
		if (that.setHeader('content-type') == null) {
			that.setHeader('content-type', mimetype + ";charset=utf-8");
		}

		if (callback) {
			return callback(null, html);
		}

		that.end(html);
	});
});

/**
 * Convert a buffer into a stream
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.1.0
 * @version  1.1.0
 *
 * @param    {Buffer}   buffer
 *
 * @return   {Readable}
 */
function bufferToStream(buffer) {

	const readable_stream = new libstream.Readable({
		read() {
			this.push(buffer);
			this.push(null);
		}
	});

	return readable_stream;
}

/**
 * Send a file to the browser
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.3.0
 *
 * @param    {string}   path      The path on the server to send to the browser
 * @param    {Object}   options   Options, including headers
 */
Conduit.setTypedMethod([Types.String, Types.Object.optional()], function serveFile(path, options = {}) {

	let file = FILECACHE.get(path);

	if (!file) {
		file = new Classes.Alchemy.Inode.File(path);
	}

	return this.serveFile(file, options);
});

/**
 * Send a file to the browser
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.3.13
 *
 * @param    {Alchemy.Inode.File}   file      The file to serve
 * @param    {Object}               options   Options, including headers
 */
Conduit.setTypedMethod([Types.Alchemy.Inode.File, Types.Object.optional()], async function serveFile(file, options = {}) {

	if (file.path && !FILECACHE.has(file.path)) {
		FILECACHE.set(file.path, file);
	}

	let stats = await file.getStats();

	if (alchemy.settings.performance.cache && (!options.cache_time && options.cache_time !== false)) {
		options.cache_time = stats.mtime;
	}

	if (options.content_length == null) {
		options.content_length = stats.size;
	}

	if (!options.mimetype) {
		options.mimetype = await file.getMimetype();
	}

	if (!options.filename) {
		options.filename = file.name;
	}
	
	return this.serveFile(file.createReadStream(), options);
});

/**
 * Send a buffer to the client
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.0
 * @version  1.3.0
 *
 * @param    {Stream}   buffer    The buffer to send
 * @param    {Object}   options   Options, including headers
 */
Conduit.setTypedMethod([Buffer, Types.Object.optional()], function serveFile(buffer, options = {}) {
	return this.serveFile(bufferToStream(buffer), options);
});

/**
 * Send a stream to the client
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.0
 * @version  1.3.0
 *
 * @param    {Stream}   stream    The stream to send
 * @param    {Object}   options   Options, including headers
 */
Conduit.setTypedMethod([Types.Stream, Types.Object.optional()], function serveFile(stream, options = {}) {

	let is_text = false;

	if (options.mimetype && RX_TEXT.test(options.mimetype)) {
		options.mimetype += '; charset=utf-8';
		is_text = true;
	}

	if (alchemy.settings.performance.cache === false) {
		options.cache_time = null;
	}

	if (options.compress == null) {
		options.compress = is_text;
	}

	if (options.compress && (alchemy.settings.network.use_compression === false || !this.accepts('gzip'))) {
		options.compress = false;
	}

	if (options.cache_time && !alchemy.settings.performance.cache) {
		options.cache_time = false;
	}

	if (!options.onError) {
		options.onError = err => this.notFound(err);
	}

	return this._sendStream(stream, options);
});

/**
 * Send a stream to the client
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.0
 * @version  1.3.14
 *
 * @param    {Stream}   stream    The stream to send
 * @param    {Object}   options   Options, including headers
 */
Conduit.setMethod(function _sendStream(stream, options) {

	if (options.cache_time) {
		let modified_since = this.headers['if-modified-since'];

		if (modified_since != null) {
			let since = new Date(modified_since);

			// If the file's modifytime is smaller or equal to the since time,
			// don't serve the contents!
			if (options.cache_time <= since) {
				return this.notModified();
			}
		}

		// Allow the browser to cache this for 60 minutes,
		// after which it has to revalidate the content
		// by seeing if it has been modified
		this.setHeader('cache-control', 'public, max-age=3600, must-revalidate');
		this.setHeader('last-modified', options.cache_time.toGMTString());
	} else {
		this.setHeader('cache-control', 'no-cache');
	}

	let disposition,
	    key;

	if (options.mimetype) {
		this.setHeader('content-type', options.mimetype);
	}

	// Setting the disposition makes the browser download the file
	// This is on by default, but can be disabled
	if (options.disposition == 'inline') {
		disposition = 'inline';

		if (options.filename) {
			disposition += '; filename=' + JSON.stringify(options.filename)
		}

		this.setHeader('content-disposition', disposition);
	} else if (options.disposition !== false) {
		if (options.filename) {
			disposition = 'attachment; filename=' + JSON.stringify(options.filename);
		} else {
			disposition = 'attachment';
		}

		this.setHeader('content-disposition', disposition);
	}

	// Set all the headers
	for (key in options.headers) {
		this.setHeader(key, options.headers[key]);
	}

	// Don't send anything if it's a HEAD request
	if (this.method == 'head') {
		return this.end();
	}

	const response = this.response;
	let out_stream = stream;

	if (options.compress) {
		// Set the gzip header
		this.setHeader('content-encoding', 'gzip');
		this.setHeader('vary', 'accept-encoding');
		this.setHeader('transfer-encoding', 'chunked');
		this.setHeader('content-length', null);

		// Create the gzip stream
		out_stream = out_stream.pipe(zlib.createGzip());
	} else if (options.content_length != null) {
		this.setHeader('content-length', options.content_length);
	}

	if (options.cleanup_stream) {
		const cleanup = function cleanupOriginalStream() {
			// Remove all pipes
			stream.unpipe();

			if (stream.destroy) {
				stream.destroy();
			} else if (stream.end) {
				stream.end();
			}
		};

		response.on('end', cleanup);
		response.on('finish', cleanup);
		response.on('error', cleanup);
		response.on('close', cleanup);
	}

	// Set the response headers
	for (key in this.response_headers) {
		let value = this.response_headers[key];

		if (value == null) {
			continue;
		}

		response.setHeader(key, value);
	}

	if (this.new_cookie_header.length) {
		response.setHeader('set-cookie', this.new_cookie_header);
	}

	// If we got this far, the file has been found!
	response.statusCode = 200;

	// Actually stream the contents to the client
	out_stream.pipe(response);
});

/**
 * Create a session
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.4.0
 *
 * @param    {boolean}   create   Create a session if none exist
 *
 * @return   {UserSession}
 */
Conduit.setMethod(function getSession(allow_create = true) {

	// Only do this once per request
	if (this.session_instance != null) {
		return this.session_instance;
	}

	let cookie_name = this.session_cookie_name,
	    session_id,
	    session;

	// Get the ID of the session
	session_id = this.cookie(cookie_name);

	if (session_id) {
		// Get the session
		session = alchemy.sessions.get(session_id);
	}

	// If no valid session exists, create a new one
	if (!session && allow_create) {
		session = new Classes.Alchemy.ClientSession(this);
		session_id = session.id;

		this.cookie(cookie_name, session_id, {httpOnly: true});

		alchemy.sessions.set(session_id, session);
	}

	if (session) {
		this.session_instance = session;
		session.request_count++;
	} else {
		return false;
	}

	return session;
});

/**
 * Create a controller of the given type and attach this conduit
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.16
 * @version  1.3.16
 *
 * @param    {string}   controller_name
 *
 * @return   {Controller}
 */
Conduit.setMethod(function getController(controller_name) {

	if (!controller_name) {
		return this.controller;
	}

	let instance = Controller.get(controller_name);

	if (!instance) {
		return false;
	}

	instance.conduit = this;

	return instance;
});

/**
 * Register live data bindings via websockets
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.4.0
 */
Conduit.setMethod(function registerBindings(arr) {

	const websockets = alchemy.settings.network.use_websockets;

	// Don't do anything is websockets aren't enabled
	if (!websockets || websockets == 'never') {
		return;
	}

	let data_ids;

	if (arr) {
		data_ids = arr;
	} else {
		data_ids = this.renderer.live_bindings;
	}

	if (Object.isEmpty(data_ids)) {
		return;
	}

	this.getSession().registerBindings(data_ids, this.sceneId);
});

/**
 * Get a a value from the session object
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.4.0
 *
 * @param    {string}   name
 * @param    {Mixed}    value
 *
 * @return   {Mixed}
 */
Conduit.setMethod(function session(name, value) {

	const session = this.getSession();

	if (arguments.length === 0) {
		return session;
	}

	if (arguments.length === 1) {
		return session.get(name);
	}

	session.set(name, value);
});

/**
 * Get a route URL
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.2.7
 * @version  1.2.7
 *
 * @param    {string}   name
 * @param    {Object}   parameters
 * @param    {Object}   options
 *
 * @return   {string}
 */
Conduit.setMethod(function routeUrl(name, parameters, options) {
	return this.renderer.helpers.Router.routeUrl(name, parameters, options);
});

/**
 * Get a parameter from the route
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.2.0
 *
 * @param    {string}   name
 */
Conduit.setMethod(function routeParam(name) {
	return this.params[name];
});

/**
 * Set route parameters
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.0
 * @version  1.3.0
 */
Conduit.setMethod(function setRouteParameters(data) {

	if (!this.params) {
		this.params = {};
	}

	if (data) {
		Object.assign(this.params, data);
	}
});

/**
 * Get/set a cookie
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.4.0
 *
 * @param    {string}   name
 * @param    {Mixed}    value
 * @param    {Object}   options
 */
Conduit.setMethod(function cookie(name, value, options) {

	// Return if cookies are disabled
	if (!alchemy.settings.frontend.cookies.enabled) {
		return;
	}

	if (arguments.length == 1) {
		return this.new_cookies[name] || this.cookies[name];
	}

	if (options == null) options = {};

	// If the value is null or undefined, the cookie should be removed
	if (value == null) {
		options.expires = new Date(0);
	}

	// If no path is given, default to the root path
	if (options.path == null) options.path = '/';

	// If the `secure` flag is not set,
	// see if this connection is secure
	if (options.secure == null) {
		if (this.is_secure) {
			options.secure = true;
		}
	}

	// Store it in the new_cookies object, for quick access
	this.new_cookies[name] = value;

	// If there is no domain set, see if it is required
	if ((options.domain === true || options.domain == null) && alchemy.settings.frontend.cookies.domain && this.url) {
		let wanted_domain = alchemy.settings.frontend.cookies.domain,
		    actual_domain = this.url.hostname;

		if (wanted_domain != actual_domain) {
			// You can only set a cookie on the current domain,
			// or a parent domain. But not on a child domain.
			if (wanted_domain.length < actual_domain.length && actual_domain.endsWith(wanted_domain)) {
				// It's allowed!
			} else {
				// When on a domain like "11ways.be" you
				// can not set a cookie for "www.11ways.be"
				wanted_domain = false;
			}
		}

		if (wanted_domain) {
			options.domain = wanted_domain;
		}
	}

	if (this.websocket) {
		return this.socket.emit('alchemy-set-cookie', {name: name, value: value, options: options});
	}

	// Create the basic header string
	let header = String.encodeCookie(name, value, options);

	// Add this to the cookieheader array
	this.new_cookie_header.push(header);
});

/**
 * Set a response header
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  1.3.0
 *
 * @param    {string}   name
 * @param    {Mixed}    value
 */
Conduit.setMethod(function setHeader(name, value) {

	if (arguments.length == 1) {
		return this.getHeader(name);
	}

	if (this.websocket) {
		throw new Error("Can't set header `" + name + "` on a websocket connection");
	}

	this.response_headers[name] = value;
});

/**
 * Get a response header
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.1.0
 * @version  1.1.0
 *
 * @param    {string}   name
 */
Conduit.setMethod(function getHeader(name) {

	if (this.response_headers[name] != null) {
		return this.response_headers[name];
	}

	if (this.response) {
		return this.response.getHeader(name);
	}
});

/**
 * Update data to this scene only
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.4.0
 *
 * @param    {string}   name
 * @param    {Mixed}    value
 */
Conduit.setMethod(function update(name, value) {

	// Make sure a scene id is created
	this.createScene();

	// Send this update to this scene only
	this.getSession().sendDataUpdate(name, value, this.sceneId);
});

/**
 * Push a flash message to the client
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.2.0
 */
Conduit.setMethod(function flash(message, options) {

	var newFlashes;

	if (options == null) {
		options = {};
	}

	newFlashes = this.internal('newFlashes');

	if (newFlashes == null) {
		newFlashes = {};
	}

	newFlashes[Date.now() + '-' + Number.random(100)] = {
		message: message,
		options: options
	};

	this.internal('newFlashes', newFlashes);
});

/**
 * Set a theme to use
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.2.0
 * @version  0.2.0
 *
 * @param    {string}   name
 */
Conduit.setMethod(function setTheme(name) {
	this.theme = name;
	this.renderer.setTheme(name);
});

/**
 * Does this user support a certain feature?
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.0.4
 * @version  1.0.4
 *
 * @param    {string}   feature
 *
 * @return   {boolean}
 */
Conduit.setMethod(function supports(feature) {

	if (this.useragent && (feature == 'async' || feature == 'await')) {
		let agent = this.useragent;

		if (agent.family == 'IE') {
			return false;
		}

		if (agent.family == 'Edge' && agent.major < 15) {
			return false;
		}

		// Its actually supported on 10.1, but oh well
		if (agent.family == 'Safari' && agent.major < 11) {
			return false;
		}

		if (agent.family == 'Samsung Internet' && agent.major < 6) {
			return false;
		}

		if (agent.family == 'Opera Mini') {
			return false;
		}
	}

	return null;
});

/**
 * Should this request be delayed because the server is too busy?
 * This performs an early check, the controller might also check this later.
 * This is mainly so we can prevent Server-Side-Renders from happening
 * when the system is already overloaded.
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    1.3.1
 * @version  1.3.1
 *
 * @return   {boolean}
 */
Conduit.setMethod(function shouldBePostponed() {

	if (!alchemy.settings.performance.postpone_requests_on_overload) {
		return false;
	}

	const route = this.route;

	// If no route is found, it can be allowed.
	// Most of the time this means it's a middleware-request
	// (like stylesheets, images, scripts, ...)
	// But also 404s (This can be delayed later by the controller)
	if (route == null) {
		return false;
	}

	// Some routes can't even be postponed
	if (!route.can_be_postponed) {
		return false;
	}

	// If alchemy isn't even too busy, it's obviously allowed
	if (!alchemy.isTooBusyForRequests()) {
		return false;
	}

	// Some users have the permission to skip postponements
	if (this.hasPermission('alchemy.queue.skip')) {
		return false;
	}

	let session = this.getSession(false);

	if (session) {

		// Do not postpone clients that have already been in the queue
		if (session.hasAlreadyQueued()) {
			return false;
		}

		// Do not postpone clients that have already rendered something
		if (session.render_counter > 0 && session.is_active) {
			return false;
		}
	}

	// AJAX requests are allowed a bit more
	if (this.ajax) {
		return alchemy.isTooBusyForAjax();
	}

	return true;
});

/**
 * Broadcast data to every connected user
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.3.0
 * @version  1.3.16
 *
 * @param    {string}   type
 * @param    {Object}   data
 */
Alchemy.setMethod(function broadcast(type, data) {

	alchemy.sessions.forEach(function eachSession(session, key) {

		// Go over every listening socket and submit the data
		Object.each(session.connections, function eachScene(socket_conduit, scene_id) {

			if (!socket_conduit?.is_connected) {
				return;
			}

			if (alchemy.settings.debugging.debug) {
				log.debug('Broadcasting', type, {data, scene: socket_conduit});
			}

			socket_conduit.submit(type, data);
		});
	});
});

/**
 * Get the magic mimetype function
 *
 * @author   Jelle De Loecker <jelle@elevenways.be>
 * @since    0.3.0
 * @version  1.4.0
 */
function getMagic() {

	var mmmagic;

	if (magic || magic === false) {
		return magic;
	}

	// Get mmmagic module
	mmmagic = alchemy.use('@picturae/mmmagic')

	if (mmmagic) {
		magic = new mmmagic.Magic(mmmagic.MAGIC_MIME_TYPE);
	} else {
		log.error('Could not load mmmagic module');
		magic = false;
	}

	return magic;
}

global.Conduit = Conduit;