2 * Copyright (C) 2018-2022 Nick Downing <nick@ndcode.org>
3 * SPDX-License-Identifier: MIT
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to
7 * deal in the Software without restriction, including without limitation the
8 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9 * sell copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
12 * The above copyright notice and this permission notice shall be included in
13 * all copies or substantial portions of the Software.
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24 import JSTCache from '@ndcode/jst_cache'
25 import Listener from './Listener.mjs'
26 import Problem from './Problem.mjs'
27 import Resources from './Resources.mjs'
28 import Site from './Site.mjs'
29 import assert from 'assert'
30 import jst_server from './index.mjs'
34 constructor(resources, options/*, prev_server*/) {
35 this.resources = resources
36 this.options = Object.assign(
52 'localhost.localdomain': {
61 this.jst_cache = undefined
62 this.site_roots = undefined
63 this.listeners = undefined
65 (request, response, listener) =>
66 /*await*/ this.respond(request, response, listener.options.protocol)
67 // returned Promise will be ignored
71 assert(this.jst_cache === undefined)
72 this.jst_cache = await this.resources.ref(
74 async () => new JSTCache('.', {_jst_server: jst_server}, true)
77 assert(this.site_roots === undefined)
79 for (let i in this.options.hosts) {
82 Object.prototype.hasOwnProperty.call(this.options.hosts, i) &&
83 (host = this.options.hosts[i]).type === 'site' &&
84 !Object.prototype.hasOwnProperty.call(this.site_roots, host.root)
86 this.site_roots[host.root] = await this.resources.ref(
87 `site_root:${host.root}`,
90 jst_cache: await this.resources.ref(
91 `jst_cache:${host.root}`,
93 new JSTCache(host.root, {_jst_server: jst_server}, true)
100 await this.resources.unref(`jst_cache:${site_root.root}`)
101 if (site_root.site !== undefined)
102 await site_root.site.stop()
107 assert(this.listeners === undefined)
109 for (let i = 0; i < this.options.listeners.length; ++i) {
111 switch (this.options.listeners[i].protocol) {
113 options = Object.assign(
119 this.options.listeners[i]
123 options = Object.assign(
128 ssl_cert: '_ssl/localhost_cert_bundle.pem',
129 ssl_key: '_ssl/localhost_key.pem'
131 this.options.listeners[i]
137 let listener = await this.resources.ref(
138 `listener:${JSON.stringify(options)}`,
139 async () => new Listener(undefined, options, true),
140 listener => /*await*/ listener.stop()
142 this.listeners.push(listener)
147 assert(this.jst_cache !== undefined)
148 await this.resources.unref('jst_cache:.')
150 assert(this.site_roots !== undefined)
151 for (let i in this.site_roots)
152 if (Object.prototype.hasOwnProperty.call(this.site_roots, i))
153 await this.resources.unref(`site_root:${i}`)
155 assert(this.listeners !== undefined)
156 for (let i = 0; i < this.listeners.length; ++i)
157 await this.resources.unref(
158 `listener:${JSON.stringify(this.listeners[i].options)}`
163 assert(this.jst_cache !== undefined)
164 this.jst_cache.kick()
166 assert(this.site_roots !== undefined)
167 for (let i in this.site_roots)
168 if (Object.prototype.hasOwnProperty.call(this.site_roots, i)) {
169 let site_root = this.site_roots[i]
170 let config = await site_root.jst_cache.get(
171 `${i}/_config/site.jst`,
174 if (config !== undefined) {
175 let prev_site = site_root.site
176 let new_site = await config(this.resources, i, prev_site)
177 await new_site.start() // exception here cancels site change
178 site_root.site = new_site
179 if (prev_site !== undefined)
180 await prev_site.stop() // exception here doesn't cancel change
182 await site_root.site.kick()
185 assert(this.listeners !== undefined)
186 for (let i = 0; i < this.listeners.length; ++i) {
187 this.listeners[i].request_func = this.request_func
188 await this.listeners[i].start()
192 serve_internal(response, status, mime_type, data) {
193 response.statusCode = status
194 // no real need to cache errors and hostname redirects
195 // (pathname redirects on the other hand will be cached, see Site.js)
196 response.setHeader('Content-Type', mime_type)
197 response.setHeader('Content-Length', data.length)
201 redirect(response, location, message) {
203 response.statusCode = 301
204 response.setHeader('Location', location)
208 'text/html; charset=utf-8',
212 <meta http-equiv="content-type" content="text/html;charset=utf-8">
213 <title>301 Moved Permanently</title>
215 <body style="font-family: sans-serif, serif, monospace; font-size: 20px;">
216 <h2>301 Moved Permanently</h2>
217 The document has moved <a href="${location}">here</a>.
226 async respond(request, response, protocol) {
227 let env = {request, response, server: this}
229 env.parsed_url = url.parse(
230 protocol + '//' + (request.headers.host || 'localhost') + request.url,
235 !Object.prototype.hasOwnProperty.call(
237 env.parsed_url.hostname
242 `Site "${env.parsed_url.hostname}" not found.`,
246 let host = this.options.hosts[env.parsed_url.hostname]
249 let new_host = host.host
250 if (env.parsed_url.port !== null)
251 new_host += ':' + env.parsed_url.port
254 `${env.parsed_url.protocol}//${new_host}${request.url}`,
255 `redirecting ${env.parsed_url.host} to ${new_host}`
259 await this.site_roots[host.root].site.respond(env)
266 let problem = Problem.from(error)
268 env.parsed_url === undefined ?
269 `${problem.status} ${problem.title}` :
270 `${env.parsed_url.host} ${problem.status} ${problem.title}`
272 console.log(` ${problem.detail}`)
274 // for API endpoints, serve the JSON-encoded Problem object directly
275 if (env.mime_type === 'application/json; charset=utf-8')
279 'application/problem+json; charset=utf-8',
283 title: problem.title,
284 detail: problem.detail,
285 status: problem.status
297 'text/html; charset=utf-8',
301 <meta http-equiv="content-type" content="text/html; charset=utf-8">
302 <title>${problem.status} ${problem.title}</title>
304 <body style="font-family: sans-serif, serif, monospace; font-size: 20px;">
305 <h2>${problem.status} ${problem.title}</h2>
306 ${problem.detail.replaceAll('&', '&').replaceAll('<', '<').replaceAll('\n', '<br>')}
317 export default Server