The module shellfish/server provides elements for building servers.
HTTP Server
The element HTTPServer sets up a HTTP server listening on a specified port and host address.
require "shellfish/server";
HTTPServer {
port: 8000
host: "0.0.0.0"
}
In this example, the server is listening on port 8000 on all interfaces, since
the host address is "0.0.0.0"
.
HTTPS with SSL Encryption
By switching the secure
property to true
and providing a
X.509 server certificate
and key in PEM format, you instruct the server to use SSL encryption for all
communication.
require "shellfish/server";
HTTPServer {
port: 8443
host: "0.0.0.0"
secure: true
certificate: "/etc/ssl/server-cert.pem"
key: "/etc/ssl/server-key.pem"
}
Routing Requests
Every incoming HTTP request may take a different route depending on various
properties. In order to create routes, place a number of HTTPRoute
elements inside the HTTPServer
. The HTTPRoute
has a when
property for
a predicate function that decides for each request whether that particular route
is to be taken.
The routes are tested one by one until one matches the request with its when
predicate.
This is an example of routing depending on the path in the request URLs.
require "shellfish/server";
HTTPServer {
port: 8000
host: "0.0.0.0"
HTTPRoute {
// this route handles URL paths under /www/
when: req => req.url.path.startsWith("/www/")
}
HTTPRoute {
// this route handles the /login POST request only
when: req => req.method === "POST" && req.url.path === "/login"
}
HTTPRoute {
// this route has no special predicate and thus handles
// all other requests
}
}
You can also use the when
predicate to look at the HTTP headers, and
implement virtual servers by recognizing the Host
header.
require "shellfish/server";
HTTPServer {
port: 8000
host: "0.0.0.0"
HTTPRoute {
// this route handles requests to the address http://virtualhostA.com
when: req => req.headers.get("Host") === "virtualhostA.com"
}
HTTPRoute {
// this route handles GET requests to the address http://virtualhostB.com/www/...
when: req => req.headers.get("Host") === "virtualhostB.com" &&
req.method === "GET" &&
req.url.path.startsWith("/www/")
}
}
Since testing for a path prefix is pretty common, you may also use the method
pathPrefix()
of HTTPRoute
for creating a predicate function.
HTTPRoute {
when: pathPrefix("/www/")
}
The Request Object
Lets take a further look at the request object, which is of the type HTTPRequestEvent.
The property method
tells you about the HTTP method of the request, such
as for example GET
, POST
, or HEAD
for the most common ones.
The IP address and port from which the request originated is available in the
sourceAddress
and sourcePort
properties, respectively. With this, you could
limit access to a particular route to a certain source address.
HTTPRoute {
// allow requests from localhost only
when: req => req.sourceAddress === "127.0.0.1"
}
The HTTP headers of the request are found as a map in the headers
property.
Likewise, the values of the HTTP cookies are found in the cookies
property.
The url
property holds an URL object including these properties:
href
: The full URL string, e.g.http://www.example.com:8000/www/index.html?a=42&b=foo
protocol
: The protocol part of the URL, e.g.http:
.hostname
: The hostname, e.g.www.example.com
.port
: The port number, e.g.8000
.path
: The path string, e.g./www/index.html
.search
: The raw search expression, usually used forGET
parameters.parameters
: A dictionary of the parameters from thesearch
expression.
Serving Static Web Content
So far, our example routes did not do anything. It is up to session delegates
to actually handle the incoming requests. These delegates are created dynamically
by the HTTPRoute
from a template as needed. Thus, do not forget to mark the delegate
with the template
keyword.
Let's handle static web content with the WebSession element. This element serves a given root path on any file system. The next example uses LocalFS for accessing the local file system on the hard disk.
This is already a fully working web server serving static content from /var/www
.
require "shellfish/server";
HTTPServer {
port: 8000
host: "0.0.0.0"
HTTPRoute {
delegate: template WebSession {
filesystem: LocalFS { }
root: "/var/www"
}
}
}
Handling Generic Requests
The most generic session delegate is the HTTPSession, which is also the base class for WebSession.
The request
event gets emitted on incoming requests and lets you respond to
the request with the response()
method of the request.
HTTPSession {
onRequest: req =>
{
if (req.method === "GET")
{
req.response(200, "OK")
.body("You requested " + req.url.href)
.send();
}
else
{
req.response(404, "Not Found")
.send();
}
}
}
response()
takes the HTTP result code (e.g. 200 for OK) and a textual
representation of the code and returns a HTTPResponse object.
The methods of the response object may be chained until you finally submit the
response with send()
.
req.response(200, "OK")
.header("X-Custom-Header", "foo")
.body("You requested " + req.url.href, "text/plain")
.send();
Besides sending a string with the body()
method, you may also use the stream
method for streaming from a ReadableStream
.
// stream a PNG file of length 112960 bytes from disk
req.response(200, "OK")
.stream(myStream, "image/png", 112960)
.send();
If you don't know the content size before-hand, you may switch to chunked transfer
by suppliying -1
for the size.
// stream large content of unknown length
req.response(200, "OK")
.stream(myStream, "application/zip", -1)
.send();
If the client stated that it accepts compressed data, the data gets compressed automatically in order to save network bandwidth.
Receiving Data
Some HTTP request methods like POST
or PUT
may send additional data in its
body. The request object provides methods for reading this data.
The method body()
returns a promise for reading the data as string.
HTTPSession {
onRequest: async req =>
{
const data = await req.body();
console.log("Data Received:");
console.log(data);
req.response(200, "OK").send();
}
}
Likewise, the method arrayBuffer()
returns a promise for reading the data as
binary array buffer.
HTTPSession {
onRequest: async req =>
{
const data = await req.body();
console.log("Binary Data Received:");
console.log("Size: " + data.byteLength + " Bytes");
req.response(200, "OK").send();
}
}
If you want to read from the request as a ReadableStream, access the stream
property.