From 321dc6dafb405ad97ee71aae33b6f53e57a952f0 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Tue, 13 Mar 2018 21:17:27 +0100 Subject: [PATCH 1/5] Reduce regex usage. --- lib/resource-mapper.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index 73c7660c0..30dbb53fe 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -44,7 +44,7 @@ class ResourceMapper { // Determine the path of an existing file } else { // Read all files in the corresponding folder - const filename = /[^/]*$/.exec(pathname)[0] + const filename = pathname.substr(pathname.lastIndexOf('/') + 1) const folder = `${basePath}${pathname.substr(0, pathname.length - filename.length)}` const files = await this._readdir(folder) @@ -81,12 +81,14 @@ class ResourceMapper { // Removes a possible trailing slash from a path function removeTrailingSlash (path) { - return path ? path.replace(/\/+$/, '') : '' + const lastPos = path.length - 1 + return lastPos < 0 || path[lastPos] !== '/' ? path : path.substr(0, lastPos) } // Removes everything beyond the dollar sign from a path function removeDollarExtension (path) { - return path.replace(/\$.*/, '') + const dollarPos = path.lastIndexOf('$') + return dollarPos < 0 ? path : path.substr(0, dollarPos) } // Gets the expected content type based on the extension of the path From eb29ddd0403d4095d2c26907be446398d2dfe16f Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Tue, 1 May 2018 17:54:11 -0400 Subject: [PATCH 2/5] Handle percent encoding. --- lib/resource-mapper.js | 19 ++++++++-------- test/unit/resource-mapper-test.js | 36 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index 30dbb53fe..d13d39f57 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -26,26 +26,27 @@ class ResourceMapper { // Maps the request for a given resource and representation format to a server file async mapUrlToFile ({ url, contentType, createIfNotExists }) { - // Split the URL into components + // Determine the full URL const { pathname, hostname } = typeof url === 'string' ? URL.parse(url) : url - if (pathname.indexOf('/..') >= 0) { + const fullPath = decodeURIComponent(`${this.getBasePath(hostname)}${pathname}`) + if (fullPath.indexOf('/..') >= 0) { throw new Error('Disallowed /.. segment in URL') } + // Determine the path and content type let path - const basePath = this.getBasePath(hostname) - // Create the path for a new file if (createIfNotExists) { - path = `${basePath}${pathname}` + // Create the path for a new file + path = fullPath // If the extension is not correct for the content type, append the correct extension - if (getContentType(pathname) !== contentType) { + if (getContentType(path) !== contentType) { path += contentType in extensions ? `$.${extensions[contentType][0]}` : '$.unknown' } // Determine the path of an existing file } else { // Read all files in the corresponding folder - const filename = pathname.substr(pathname.lastIndexOf('/') + 1) - const folder = `${basePath}${pathname.substr(0, pathname.length - filename.length)}` + const filename = fullPath.substr(fullPath.lastIndexOf('/') + 1) + const folder = fullPath.substr(0, fullPath.length - filename.length) const files = await this._readdir(folder) // Find a file with the same name (minus the dollar extension) @@ -64,7 +65,7 @@ class ResourceMapper { async mapFileToUrl ({ path, hostname }) { // Determine the URL by chopping off everything after the dollar sign const pathname = removeDollarExtension(path.substring(this._rootPath.length)) - const url = `${this.getBaseUrl(hostname)}${pathname}` + const url = `${this.getBaseUrl(hostname)}${encodeURI(pathname)}` return { url, contentType: getContentType(path) } } diff --git a/test/unit/resource-mapper-test.js b/test/unit/resource-mapper-test.js index 538494fca..394b25b6e 100644 --- a/test/unit/resource-mapper-test.js +++ b/test/unit/resource-mapper-test.js @@ -164,6 +164,29 @@ describe('ResourceMapper', () => { contentType: 'text/html' }) + itMapsUrl(mapper, 'a URL of an existing file with encoded characters', + { + url: 'http://localhost/space/foo%20bar%20bar.html' + }, + [ + `${rootPath}space/foo bar bar.html` + ], + { + path: `${rootPath}space/foo bar bar.html`, + contentType: 'text/html' + }) + + itMapsUrl(mapper, 'a URL of a new file with encoded characters', + { + url: 'http://localhost/space%2Ffoo%20bar%20bar.html', + contentType: 'text/html', + createIfNotExists: true + }, + { + path: `${rootPath}space/foo bar bar.html`, + contentType: 'text/html' + }) + // Security cases itMapsUrl(mapper, 'a URL with an unknown content type', @@ -183,6 +206,12 @@ describe('ResourceMapper', () => { }, new Error('Disallowed /.. segment in URL')) + itMapsUrl(mapper, 'a URL with an encoded /.. path segment', + { + url: 'http://localhost/space%2F..%2Fbar' + }, + new Error('Disallowed /.. segment in URL')) + // File to URL mapping itMapsFile(mapper, 'an HTML file', @@ -254,6 +283,13 @@ describe('ResourceMapper', () => { url: 'http://localhost/space/foo', contentType: 'text/html' }) + + itMapsFile(mapper, 'a file with disallowed IRI characters', + { path: `${rootPath}space/foo bar bar.html` }, + { + url: 'http://localhost/space/foo%20bar%20bar.html', + contentType: 'text/html' + }) }) describe('A ResourceMapper instance for a multi-host setup', () => { From 3a42fd497d76b4dfa49d06844127e558186b8cba Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Wed, 2 May 2018 14:22:34 -0400 Subject: [PATCH 3/5] Add LegacyResourceMapper. --- lib/legacy-resource-mapper.js | 21 ++ lib/resource-mapper.js | 75 +++---- test/unit/legacy-resource-mapper-test.js | 246 +++++++++++++++++++++++ 3 files changed, 306 insertions(+), 36 deletions(-) create mode 100644 lib/legacy-resource-mapper.js create mode 100644 test/unit/legacy-resource-mapper-test.js diff --git a/lib/legacy-resource-mapper.js b/lib/legacy-resource-mapper.js new file mode 100644 index 000000000..de9d44a2b --- /dev/null +++ b/lib/legacy-resource-mapper.js @@ -0,0 +1,21 @@ +const ResourceMapper = require('./resource-mapper') + +// A LegacyResourceMapper models the old mapping between HTTP URLs and server filenames, +// and is intended to be replaced by ResourceMapper +class LegacyResourceMapper extends ResourceMapper { + constructor (options) { + super(Object.assign({ defaultContentType: 'text/turtle' }, options)) + } + + // Maps the request for a given resource and representation format to a server file + async mapUrlToFile ({ url }) { + return { path: this._getFullPath(url), contentType: this._getContentTypeByExtension(url) } + } + + // Preserve dollars in paths + _removeDollarExtension (path) { + return path + } +} + +module.exports = LegacyResourceMapper diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index d13d39f57..ce0aa6e75 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -4,42 +4,35 @@ const { promisify } = require('util') const { types, extensions } = require('mime-types') const readdir = promisify(fs.readdir) -const DEFAULT_CONTENTTYPE = 'application/octet-stream' - // A ResourceMapper maintains the mapping between HTTP URLs and server filenames, // following the principles of the “sweet spot” discussed in // https://www.w3.org/DesignIssues/HTTPFilenameMapping.html class ResourceMapper { - constructor ({ rootUrl, rootPath, includeHost }) { - this._rootUrl = removeTrailingSlash(rootUrl) - this._rootPath = removeTrailingSlash(rootPath) + constructor ({ rootUrl, rootPath, includeHost, defaultContentType = 'application/octet-stream' }) { + this._rootUrl = this._removeTrailingSlash(rootUrl) + this._rootPath = this._removeTrailingSlash(rootPath) this._includeHost = includeHost this._readdir = readdir + this._defaultContentType = defaultContentType // If the host needs to be replaced on every call, pre-split the root URL if (includeHost) { const { protocol, pathname } = URL.parse(rootUrl) this._protocol = protocol - this._rootUrl = removeTrailingSlash(pathname) + this._rootUrl = this._removeTrailingSlash(pathname) } } // Maps the request for a given resource and representation format to a server file async mapUrlToFile ({ url, contentType, createIfNotExists }) { - // Determine the full URL - const { pathname, hostname } = typeof url === 'string' ? URL.parse(url) : url - const fullPath = decodeURIComponent(`${this.getBasePath(hostname)}${pathname}`) - if (fullPath.indexOf('/..') >= 0) { - throw new Error('Disallowed /.. segment in URL') - } - - // Determine the path and content type + const fullPath = this._getFullPath(url) let path + + // Create the path for a new file if (createIfNotExists) { - // Create the path for a new file path = fullPath // If the extension is not correct for the content type, append the correct extension - if (getContentType(path) !== contentType) { + if (this._getContentTypeByExtension(path) !== contentType) { path += contentType in extensions ? `$.${extensions[contentType][0]}` : '$.unknown' } // Determine the path of an existing file @@ -50,23 +43,23 @@ class ResourceMapper { const files = await this._readdir(folder) // Find a file with the same name (minus the dollar extension) - const match = files.find(f => removeDollarExtension(f) === filename) + const match = files.find(f => this._removeDollarExtension(f) === filename) if (!match) { throw new Error('File not found') } path = `${folder}${match}` - contentType = getContentType(match) + contentType = this._getContentTypeByExtension(match) } - return { path, contentType: contentType || DEFAULT_CONTENTTYPE } + return { path, contentType: contentType || this._defaultContentType } } // Maps a given server file to a URL async mapFileToUrl ({ path, hostname }) { // Determine the URL by chopping off everything after the dollar sign - const pathname = removeDollarExtension(path.substring(this._rootPath.length)) + const pathname = this._removeDollarExtension(path.substring(this._rootPath.length)) const url = `${this.getBaseUrl(hostname)}${encodeURI(pathname)}` - return { url, contentType: getContentType(path) } + return { url, contentType: this._getContentTypeByExtension(path) } } // Gets the base file path for the given hostname @@ -78,24 +71,34 @@ class ResourceMapper { getBaseUrl (hostname) { return this._includeHost ? `${this._protocol}//${hostname}${this._rootUrl}` : this._rootUrl } -} -// Removes a possible trailing slash from a path -function removeTrailingSlash (path) { - const lastPos = path.length - 1 - return lastPos < 0 || path[lastPos] !== '/' ? path : path.substr(0, lastPos) -} + // Determine the full file path corresponding to a URL + _getFullPath (url) { + const { pathname, hostname } = typeof url === 'string' ? URL.parse(url) : url + const fullPath = decodeURIComponent(`${this.getBasePath(hostname)}${pathname}`) + if (fullPath.indexOf('/..') >= 0) { + throw new Error('Disallowed /.. segment in URL') + } + return fullPath + } -// Removes everything beyond the dollar sign from a path -function removeDollarExtension (path) { - const dollarPos = path.lastIndexOf('$') - return dollarPos < 0 ? path : path.substr(0, dollarPos) -} + // Gets the expected content type based on the extension of the path + _getContentTypeByExtension (path) { + const extension = /\.([^/.]+)$/.exec(path) + return extension && types[extension[1].toLowerCase()] || this._defaultContentType + } -// Gets the expected content type based on the extension of the path -function getContentType (path) { - const extension = /\.([^/.]+)$/.exec(path) - return extension && types[extension[1].toLowerCase()] || DEFAULT_CONTENTTYPE + // Removes a possible trailing slash from a path + _removeTrailingSlash (path) { + const lastPos = path.length - 1 + return lastPos < 0 || path[lastPos] !== '/' ? path : path.substr(0, lastPos) + } + + // Removes everything beyond the dollar sign from a path + _removeDollarExtension (path) { + const dollarPos = path.lastIndexOf('$') + return dollarPos < 0 ? path : path.substr(0, dollarPos) + } } module.exports = ResourceMapper diff --git a/test/unit/legacy-resource-mapper-test.js b/test/unit/legacy-resource-mapper-test.js new file mode 100644 index 000000000..f9efec22c --- /dev/null +++ b/test/unit/legacy-resource-mapper-test.js @@ -0,0 +1,246 @@ +const LegacyResourceMapper = require('../../lib/legacy-resource-mapper') +const chai = require('chai') +const { expect } = chai +chai.use(require('chai-as-promised')) + +const rootUrl = 'http://localhost/' +const rootPath = '/var/www/folder/' + +const itMapsUrl = asserter(mapsUrl) +const itMapsFile = asserter(mapsFile) + +describe('LegacyResourceMapper', () => { + describe('A LegacyResourceMapper instance for a single-host setup', () => { + const mapper = new LegacyResourceMapper({ rootUrl, rootPath }) + + // adapted PUT base cases from https://www.w3.org/DesignIssues/HTTPFilenameMapping.html + + itMapsUrl(mapper, 'a URL with an extension that matches the content type', + { + url: 'http://localhost/space/foo.html', + contentType: 'text/html', + createIfNotExists: true + }, + { + path: `${rootPath}space/foo.html`, + contentType: 'text/html' + }) + + // Additional PUT cases + + itMapsUrl(mapper, 'a URL without content type', + { + url: 'http://localhost/space/foo.html', + createIfNotExists: true + }, + { + path: `${rootPath}space/foo.html`, + contentType: 'text/html' + }) + + itMapsUrl(mapper, 'a URL with an alternative extension that matches the content type', + { + url: 'http://localhost/space/foo.jpeg', + contentType: 'image/jpeg', + createIfNotExists: true + }, + { + path: `${rootPath}space/foo.jpeg`, + contentType: 'image/jpeg' + }) + + itMapsUrl(mapper, 'a URL with an uppercase extension that matches the content type', + { + url: 'http://localhost/space/foo.JPG', + contentType: 'image/jpeg', + createIfNotExists: true + }, + { + path: `${rootPath}space/foo.JPG`, + contentType: 'image/jpeg' + }) + + itMapsUrl(mapper, 'a URL with a mixed-case extension that matches the content type', + { + url: 'http://localhost/space/foo.jPeG', + contentType: 'image/jpeg', + createIfNotExists: true + }, + { + path: `${rootPath}space/foo.jPeG`, + contentType: 'image/jpeg' + }) + + // GET/HEAD/POST/DELETE/PATCH base cases + + itMapsUrl(mapper, 'a URL of an existing file with extension', + { + url: 'http://localhost/space/foo.html' + }, + { + path: `${rootPath}space/foo.html`, + contentType: 'text/html' + }) + + itMapsUrl(mapper, 'an extensionless URL of an existing file', + { + url: 'http://localhost/space/foo' + }, + { + path: `${rootPath}space/foo`, + contentType: 'text/turtle' + }) + + itMapsUrl(mapper, 'a URL of an existing file with encoded characters', + { + url: 'http://localhost/space/foo%20bar%20bar.html' + }, + { + path: `${rootPath}space/foo bar bar.html`, + contentType: 'text/html' + }) + + itMapsUrl(mapper, 'a URL of a new file with encoded characters', + { + url: 'http://localhost/space%2Ffoo%20bar%20bar.html', + contentType: 'text/html', + createIfNotExists: true + }, + { + path: `${rootPath}space/foo bar bar.html`, + contentType: 'text/html' + }) + + // Security cases + + itMapsUrl(mapper, 'a URL with a /.. path segment', + { + url: 'http://localhost/space/../bar' + }, + new Error('Disallowed /.. segment in URL')) + + itMapsUrl(mapper, 'a URL with an encoded /.. path segment', + { + url: 'http://localhost/space%2F..%2Fbar' + }, + new Error('Disallowed /.. segment in URL')) + + // File to URL mapping + + itMapsFile(mapper, 'an HTML file', + { path: `${rootPath}space/foo.html` }, + { + url: 'http://localhost/space/foo.html', + contentType: 'text/html' + }) + + itMapsFile(mapper, 'a Turtle file', + { path: `${rootPath}space/foo.ttl` }, + { + url: 'http://localhost/space/foo.ttl', + contentType: 'text/turtle' + }) + + itMapsFile(mapper, 'a file with an uppercase extension', + { path: `${rootPath}space/foo.HTML` }, + { + url: 'http://localhost/space/foo.HTML', + contentType: 'text/html' + }) + + itMapsFile(mapper, 'a file with a mixed-case extension', + { path: `${rootPath}space/foo.HtMl` }, + { + url: 'http://localhost/space/foo.HtMl', + contentType: 'text/html' + }) + + itMapsFile(mapper, 'a file with disallowed IRI characters', + { path: `${rootPath}space/foo bar bar.html` }, + { + url: 'http://localhost/space/foo%20bar%20bar.html', + contentType: 'text/html' + }) + }) + + describe('A LegacyResourceMapper instance for a multi-host setup', () => { + const mapper = new LegacyResourceMapper({ rootUrl, rootPath, includeHost: true }) + + itMapsUrl(mapper, 'a URL with a host', + { + url: 'http://example.org/space/foo.html', + contentType: 'text/html', + createIfNotExists: true + }, + { + path: `${rootPath}example.org/space/foo.html`, + contentType: 'text/html' + }) + + itMapsUrl(mapper, 'a URL with a host with a port', + { + url: 'http://example.org:3000/space/foo.html', + contentType: 'text/html', + createIfNotExists: true + }, + { + path: `${rootPath}example.org/space/foo.html`, + contentType: 'text/html' + }) + + itMapsFile(mapper, 'a file on a host', + { + path: `${rootPath}space/foo.html`, + hostname: 'example.org' + }, + { + url: 'http://example.org/space/foo.html', + contentType: 'text/html' + }) + }) + + describe('A LegacyResourceMapper instance for a multi-host setup with a subfolder root URL', () => { + const rootUrl = 'http://localhost/foo/bar/' + const mapper = new LegacyResourceMapper({ rootUrl, rootPath, includeHost: true }) + + itMapsFile(mapper, 'a file on a host', + { + path: `${rootPath}space/foo.html`, + hostname: 'example.org' + }, + { + url: 'http://example.org/foo/bar/space/foo.html', + contentType: 'text/html' + }) + }) +}) + +function asserter (assert) { + const f = (...args) => assert(it, ...args) + f.skip = (...args) => assert(it.skip, ...args) + f.only = (...args) => assert(it.only, ...args) + return f +} + +function mapsUrl (it, mapper, label, options, expected) { + // Set up positive test + if (!(expected instanceof Error)) { + it(`maps ${label}`, async () => { + const actual = await mapper.mapUrlToFile(options) + expect(actual).to.deep.equal(expected) + }) + // Set up error test + } else { + it(`does not map ${label}`, async () => { + const actual = mapper.mapUrlToFile(options) + await expect(actual).to.be.rejectedWith(expected.message) + }) + } +} + +function mapsFile (it, mapper, label, options, expected) { + it(`maps ${label}`, async () => { + const actual = await mapper.mapFileToUrl(options) + expect(actual).to.deep.equal(expected) + }) +} From a704e96a4ff330f5a86cfea3a074e668879f1b8c Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Wed, 2 May 2018 20:09:15 -0400 Subject: [PATCH 4/5] Use LegacyResourceMapper in allow handler. Fixes #656. --- lib/handlers/allow.js | 56 ++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/lib/handlers/allow.js b/lib/handlers/allow.js index 5e2ccadd1..fdca2b107 100644 --- a/lib/handlers/allow.js +++ b/lib/handlers/allow.js @@ -2,9 +2,9 @@ module.exports = allow var ACL = require('../acl-checker') var $rdf = require('rdflib') -var url = require('url') var utils = require('../utils') var debug = require('../debug.js').ACL +var LegacyResourceMapper = require('../legacy-resource-mapper') function allow (mode) { return function allowHandler (req, res, next) { @@ -13,6 +13,14 @@ function allow (mode) { return next() } + // Set up URL to filesystem mapping + const rootUrl = utils.getBaseUri(req) + const mapper = new LegacyResourceMapper({ + rootUrl, + rootPath: ldp.root, + includeHost: ldp.multiuser + }) + // Determine the actual path of the request var reqPath = res && res.locals && res.locals.path ? res.locals.path @@ -27,11 +35,10 @@ function allow (mode) { } // Obtain and store the ACL of the requested resource - const baseUri = utils.getBaseUri(req) - req.acl = new ACL(baseUri + reqPath, { + req.acl = new ACL(rootUrl + reqPath, { origin: req.get('origin'), host: req.protocol + '://' + req.get('host'), - fetch: fetchDocument(req.hostname, ldp, baseUri), + fetch: fetchFromLdp(mapper, ldp), suffix: ldp.suffixAcl, strictOrigin: ldp.strictOrigin }) @@ -53,40 +60,23 @@ function allow (mode) { * The `fetch(uri, callback)` results in the callback, with either: * - `callback(err, graph)` if any error is encountered, or * - `callback(null, graph)` with the parsed RDF graph of the fetched resource - * @method fetchDocument - * @param host {string} req.hostname. Used in determining the location of the - * document on the file system (the root directory) - * @param ldp {LDP} LDP instance - * @param baseUri {string} Base URI of the solid server (including any root - * mount), for example `https://example.com/solid` * @return {Function} Returns a `fetch(uri, callback)` handler */ -function fetchDocument (host, ldp, baseUri) { - return function fetch (uri, callback) { - readFile(uri, host, ldp, baseUri).then(body => { +function fetchFromLdp (mapper, ldp) { + return function fetch (url, callback) { + // Convert the URL into a filename + mapper.mapUrlToFile({ url }) + // Read the file from disk + .then(({ path }) => new Promise((resolve, reject) => { + ldp.readFile(path, (e, c) => e ? reject(e) : resolve(c)) + })) + // Parse the file as Turtle + .then(body => { const graph = $rdf.graph() - $rdf.parse(body, graph, uri, 'text/turtle') + $rdf.parse(body, graph, url, 'text/turtle') return graph }) + // Return the ACL graph .then(graph => callback(null, graph), callback) } } - -// Reads the given file, returning its contents -function readFile (uri, host, ldp, baseUri) { - return new Promise((resolve, reject) => { - // If local request, slice off the initial baseUri - // S(uri).chompLeft(baseUri).s - var newPath = uri.startsWith(baseUri) - ? uri.slice(baseUri.length) - : uri - // Determine the root file system folder to look in - // TODO prettify this - var root = !ldp.multiuser ? ldp.root : ldp.root + host + '/' - // Derive the file path for the resource - var documentPath = utils.uriToFilename(newPath, root) - var documentUri = url.parse(documentPath) - documentPath = documentUri.pathname - ldp.readFile(documentPath, (e, c) => e ? reject(e) : resolve(c)) - }) -} From c121ccea2779bec3a142d1aa5a0d6884f0d83b9a Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Thu, 3 May 2018 14:06:44 -0400 Subject: [PATCH 5/5] Include ports in URL generation. --- lib/resource-mapper.js | 8 +++-- test/unit/resource-mapper-test.js | 58 +++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index ce0aa6e75..3725711a8 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -17,8 +17,9 @@ class ResourceMapper { // If the host needs to be replaced on every call, pre-split the root URL if (includeHost) { - const { protocol, pathname } = URL.parse(rootUrl) + const { protocol, port, pathname } = URL.parse(rootUrl) this._protocol = protocol + this._port = port === null ? '' : `:${port}` this._rootUrl = this._removeTrailingSlash(pathname) } } @@ -64,12 +65,13 @@ class ResourceMapper { // Gets the base file path for the given hostname getBasePath (hostname) { - return this._includeHost ? `${this._rootPath}/${hostname}` : this._rootPath + return !this._includeHost ? this._rootPath : `${this._rootPath}/${hostname}` } // Gets the base URL for the given hostname getBaseUrl (hostname) { - return this._includeHost ? `${this._protocol}//${hostname}${this._rootUrl}` : this._rootUrl + return !this._includeHost ? this._rootUrl + : `${this._protocol}//${hostname}${this._port}${this._rootUrl}` } // Determine the full file path corresponding to a URL diff --git a/test/unit/resource-mapper-test.js b/test/unit/resource-mapper-test.js index 394b25b6e..9575a9541 100644 --- a/test/unit/resource-mapper-test.js +++ b/test/unit/resource-mapper-test.js @@ -329,7 +329,7 @@ describe('ResourceMapper', () => { }) describe('A ResourceMapper instance for a multi-host setup with a subfolder root URL', () => { - const rootUrl = 'http://localhost/foo/bar/' + const rootUrl = 'https://localhost/foo/bar/' const mapper = new ResourceMapper({ rootUrl, rootPath, includeHost: true }) itMapsFile(mapper, 'a file on a host', @@ -338,7 +338,61 @@ describe('ResourceMapper', () => { hostname: 'example.org' }, { - url: 'http://example.org/foo/bar/space/foo.html', + url: 'https://example.org/foo/bar/space/foo.html', + contentType: 'text/html' + }) + }) + + describe('A ResourceMapper instance for an HTTP host with non-default port', () => { + const mapper = new ResourceMapper({ rootUrl: 'http://localhost:81/', rootPath }) + + itMapsFile(mapper, 'a file with the port', + { + path: `${rootPath}space/foo.html` + }, + { + url: 'http://localhost:81/space/foo.html', + contentType: 'text/html' + }) + }) + + describe('A ResourceMapper instance for an HTTP host with non-default port in a multi-host setup', () => { + const mapper = new ResourceMapper({ rootUrl: 'http://localhost:81/', rootPath, includeHost: true }) + + itMapsFile(mapper, 'a file with the port', + { + path: `${rootPath}space/foo.html`, + hostname: 'example.org' + }, + { + url: 'http://example.org:81/space/foo.html', + contentType: 'text/html' + }) + }) + + describe('A ResourceMapper instance for an HTTPS host with non-default port', () => { + const mapper = new ResourceMapper({ rootUrl: 'https://localhost:81/', rootPath }) + + itMapsFile(mapper, 'a file with the port', + { + path: `${rootPath}space/foo.html` + }, + { + url: 'https://localhost:81/space/foo.html', + contentType: 'text/html' + }) + }) + + describe('A ResourceMapper instance for an HTTPS host with non-default port in a multi-host setup', () => { + const mapper = new ResourceMapper({ rootUrl: 'https://localhost:81/', rootPath, includeHost: true }) + + itMapsFile(mapper, 'a file with the port', + { + path: `${rootPath}space/foo.html`, + hostname: 'example.org' + }, + { + url: 'https://example.org:81/space/foo.html', contentType: 'text/html' }) })