Are you Hapi(.js)? (Part 2/2)
Keywords: Node.js, NPM, Javascript, Hapi.js, HTTP, Inert, Path, Fs, Rot13-transform, Joi, Hapi-auth-basic, Routing, Static files, Streams, Validation, Upload, Cookies, Authorization
DISCLAIMER: THE VIEWS AND OPINIONS EXPRESSED IN THIS ARTICLE ARE THOSE OF THE AUTHOR AND DO NOT REFLECT THE OFFICIAL POLICY OR POSITION OF THE EMPLOYER OF THE AUTHOR. THE ARTICLE IS NOT ENDORSED BY, DIRECTLY AFFILIATED WITH, MAINTAINED, AUTHORIZED, OR SPONSORED BY ANY CORPORATION OR ORGANIZATION. THE INFORMATION CONTAINED ON THIS ARTICLE IS INTENDED SOLELY TO PROVIDE GENERAL GUIDANCE ON MATTERS OF INTEREST FOR THE PERSONAL USE OF THE READER, WHO ACCEPTS FULL RESPONSIBILITY FOR ITS USE. ALTHOUGH THE AUTHOR HAS MADE EVERY EFFORT TO ENSURE THAT THE INFORMATION IN THIS ARTICLE WAS CORRECT AT THE TIME OF THE WRITING, THE AUTHOR DOES NOT ASSUME AND HEREBY DISCLAIM ANY LIABILITY TO ANY PARTY FOR ANY LOSS, DAMAGE, OR DISRUPTION CAUSED BY ERRORS OR OMISSIONS, WHETHER SUCH ERRORS OR OMISSIONS RESULT FROM NEGLIGENCE, ACCIDENT, OR ANY OTHER CAUSE.
Programming languages, technology
This article involves the following programming languages and technology:
- Javascript
- Node.js
- hapi.js
- HTTP
The following node modules and Javascript libraries are used:
- hapi.js
- inert
- path (core module)
- fs (core module)
- rot13-transform
- joi
- boom
- hapi-auth-basic
Table of Contents
The article is divided into the following sections:
- Preparation, prerequisites
- 1.: Streams
- 2.: Validation
- 3.: Validation using JOI object
- 4.: Uploads
- 5.: Cookies
- 6.: Authentication
- Closing words
- License
- References
1.: Streams
In this exercise an additional module will be installed. This module is rot13-transform
, and it implements the ROT13 transformation using streams.
ROT13 (“rotate by 13 places”, sometimes hyphenated ROT-13) is a simple letter substitution cipher that replaces a letter with the 13th letter after it, in the alphabet. ROT13 is a special case of the Caesar cipher which was developed in ancient Rome. (Source: Wikipedia)
Install this module:
node install rot13-transform
Further reading:
The fs
core module of Node.js is used. In this example the content of a text file (makemehapi-exercise-8.txt) is read up, and the ROT13 transformation is applied to the content of it (with the pipe()
call, which I already mentioned in my earlier article about RxJS here). The result is then returned to the client. The implementation looks like the following:
2.: Validation
In this example Joi framework is used for validation. Joi is an object schema description language and validator for Javascript object.
npm install joi
In the following example path /chicken
is defined and a parameter suffix named breed
. The parameter is optional (which is indicated with the ?
sign).
If we navigate with the browser to the /chickens
path, we get the following:
Without the validation implementation (commenting out lines 28–34 and the ,
at the end of line 26) we get the following:
If we navigate to /chickens/Sussex
, we get the following:
Further reading:
3.: Validation using JOI object
By using a Joi object custom validation rules can be specified in
paths, request payloads, and responses.
In the example below complex conditions are defined for the parameters:
- isGuest is of type boolean, and a mandatory parameter
- username is of type string and when isGuest is true, then username is mandatory (remember that isGuest is mandatory). (Otherwise username is not mandatory.)
- accessToken is an alphanumeric string
- password is an alphanumeric string
When a post request is sent to the path /login
, then these validations are executed on the payload.
4.: Uploads
This example shows how to upload file to the server using Hapi.js. I enhanced the original solution with a HTML page with which it is possible to test the implemented server.
Some important points of this code example:
config
has 2 properties:handler
andpayload
- a Promise is returned in the
handler
handler: (request, h) => {
return new Promise((resolve, reject) => {
<...>
});
},
<...>
- the file can be accessed with
request.payload.uploadedFile
uploadedFile
is the name in the<input>
tag of the HTML:
<input type="file" name="uploadedFile" size="40">
- the implementation reacts on 3 events:
data
,end
anderror
request.payload.uploadedFile.on('data', (data) => {<...>request.payload.uploadedFile.on('end', () => {<...>request.payload.uploadedFile.on('error', (err) => {
- file data is collected to a variable
let body = '';<...>body += data;
- To accept the file as input and get the file as readable stream, the following is used:
payload: {
output: 'stream',
parse: true,
allow: 'multipart/form-data'
}
- the example implementation does not save to file to the server, but prints some info about it, and the file contents as well
The implementation looks like the following:
In my environment the app works the following way:
5.: Cookies
This exercise is about handling cookies.
An HTTP cookie (also called web cookie, Internet cookie, browser cookie, or simply cookie) is a small piece of data sent from a website and stored on the user’s computer by the user’s web browser while the user is browsing. Cookies were designed to be a reliable mechanism for websites to remember stateful information (such as items added in the shopping cart in an online store) or to record the user’s browsing activity (including clicking particular buttons, logging in, or recording which pages were visited in the past). They can also be used to remember arbitrary pieces of information that the user previously entered into form fields such as names, addresses, passwords, and credit card numbers. (source: Wikipedia)
HTTP State Management Mechanism is defined in RFC 6265.
“[…] the HTTP Cookie and Set-Cookie header fields […] can be used by HTTP servers to store state […] at HTTP user agents, letting the servers maintain a stateful session over the mostly stateless HTTP protocol. […] Using the Set-Cookie header field, an HTTP server can pass name/value pairs and associated metadata (called cookies) to a user agent. When the user agent makes subsequent requests to the server, the user agent uses the metadata and other information to determine whether to return the name/value pairs in the Cookie header. […] the server indicates a scope for each cookie when sending it to the user agent. The scope indicates the maximum amount of time in which the user agent should return the cookie, the servers to which the user agent should return the cookie […]. (source: RFC 6265)
This implementation uses package boom
which provides a set of utilities for returning HTTP errors.
npm install boom
In this example, the server responds to the following paths:
set-cookie
: sets a cookie with keysession
check-cookie
: checks the cookies if there is one with the keysession
. If yes, then returns{ user: 'hapi' }
, otherwise returns an unauthorized error message
The solution for this exercise works the following way:
- To use a cookie, it needs to be configured with
server.state()
call
server.state('session', {
path: '/',
encoding: 'base64json',
ttl: 10,
domain: 'localhost',
isSameSite: false,
isSecure: false,
isHttpOnly: false
});
The above code tells the server the following:
— the path used is /
— the cookie should be base64json encoded
— the cookie expires in 10 milliseconds (for example, other option would be ttl: null
which would set session lifetime, meaning the cookie will be deleted by the browser when closed)
— the domain scope of the cookie is localhost
More details about the isSecure
attribute:
The Secure attribute limits the scope of the cookie to “secure” channels (where “secure” is defined by the user agent). When a cookie has the Secure attribute, the user agent will include the cookie in an HTTP request only if the request is transmitted over a secure channel (typically HTTP over Transport Layer Security (TLS) [RFC2818]).
Although seemingly useful for protecting cookies from active network attackers, the Secure attribute protects only the cookie’s confidentiality. An active network attacker can overwrite Secure cookies from an insecure channel, disrupting their integrity (see Section 8.6 for more details). (Source)
More details about the isHttpOnly
attrbute:
The HttpOnly attribute limits the scope of the cookie to HTTP requests. In particular, the attribute instructs the user agent to omit the cookie when providing access to cookies via “non-HTTP” APIs (such as a web browser API that exposes cookies to scripts).
Note that the HttpOnly attribute is independent of the Secure attribute: a cookie can have both the HttpOnly and the Secure attribute. (Source)
- Navigation to the
/set-cookie
path will set a cookie with the key ‘session’ and the value{ key: 'makemehapi' }
. This is achieved with the following handler
handler: (request, h) => {
return h.response({
message : 'success'
}).state('session', { key : 'makemehapi' });
}
- the cookie behaviour can be further configured at a route-level with the following
config: {
state: {
parse: true, // parse cookies and store in request.state
failAction: 'log' // may also be 'error' or 'log'
}
}
- unauthorized error with the correct HTTP status can easily be returned with the help of an additional plugin called
boom
. This needs to be installed beforehand withnpm install boom
. - The check-cookie endpoint will have cookies received from the
/set-cookie
endpoint. If the session key is present in cookies then simply return{ user: 'hapi' }
, otherwise return an unauthorized access error. This is done with the following route configuration:
server.route({
method: 'GET',
path: '/check-cookie',
handler: (request, h) => {
var session = request.state.session;
var result;if (session) {
result = { user : 'hapi' };
} else {
result = Boom.unauthorized('Missing authentication');
}
return result;
}
});
To be able to run this in the browser, I modified some things compared to the official solution:
- I used
process.env.IP
andprocess.env.PORT
as the IP address and port for the server - I used
domain: process.env.IP
in theserver.state()
call so that it will work in Cloud 9 - I used
ttl: null
The source code of the solution is the following:
Further information:
6.: Authentication
The Hapi plugin for handling basic authentication is called hapi-basic-auth
:
npm install hapi-auth-basic
This is a basic authentication. With a help of a function we can authenticate a user and return if the authentication was successful or failed. (In this basic the password is a hard-coded plain text, but of course normally it should be stored encrypted.) In the example app, the username is hapi, the password is auth.
With server.auth.strategy('simple', 'basic', { validate });
we tell the server that it should use ‘simple’ auth. strategy, without setting it as default.
With server.auth.default('simple');
we tell the server that this is the default authentication strategy, all routes added afterwards will follow this one.
Further information:
Closing words
I hope you have learned something new today. Thank you for reading this article.
License
The code snippets used in this article are based on the original solutions for the workshop, therefore I reproduce here the license of makemehapi workshop.
Copyright (c) 2012-2014, Walmart and other contributors.
All rights reserved.Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials
provided with the distribution.
* The names of any contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.* * *The complete list of contributors can be found at: https://github.com/hapijs/makemehapi/graphs/contributors
Original version of the License: