As part of the tools which works with Single SPA Framework we have one called import-map-overrides which allow us override the import maps values. By overriding an specific "micro front end" with a local environment version we are able to work with the deployed environment and improve our experience as a full stack engineer.
However, working with the deployed environment "is not all roses", specially if it is over HTTPS... and one of those problems is "Same Origin Policy"
The Same-origin policy
"The same-origin policy is a critical security mechanism that restricts how a document or script loaded by one origin can interact with a resource from another origin."
For this URL https://maxmartinez.dev/about-me we can have the following origin comparisons examples:
maxmartinez.dev/newsletter: Same origin, only the path differs.
maxmartinez.dev/about-me: Failure, different protocol.
To tackle this situation we are going to setup an https server for a local development.
Setting up the project
Firstly, we have to create our nodejs project:
npm init --yes # this will trigger automatically populated initialization with default values
Then, install the project's dependencies which are necessary to work with typescript and expressjs.
npm install express office-addin-dev-certs cors
# Dev dependencies
npm install --save-dev @types/express @types/node @types/cors typescript ts-node ts-node-dev
Special mention for "office-addin-devs-certs" which allows us to manage certificates for development server using localhost.
Project structure
Create the following structure in your local project folder:
.
├── src
│ ├── app.ts
│ └── www
│ ├── api.router.ts
│ ├── http.server.ts
│ ├── https.server.ts
│ └── server.interface.ts
└── tsconfig.json
Typescript configurations
For setup typescript properly, add the following settings to the tsconfig.json file:
{
"compilerOptions": {
"target": "es2017",
"outDir": "./dist",
"rootDir": "src",
"moduleResolution": "node",
"module": "commonjs",
"declaration": true,
"inlineSourceMap": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"types": ["node"],
"typeRoots": ["node_modules/@types"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules/**"],
}
The web server layer
Server interface
We are going to define the IServer interface contract which ensure that the HTTP and HTTPS servers implement the same behavior. Add the following interface into the server.interface.ts file:
export interface IServer {
port: number;
bootstrap: () => Promise<void>;
listen: () => void;
}
Let's explain the IServer interface:
port: Define port number which the server use to listens the connections.
bootstrap: The bootstrap method allows to implement what "staffs" the server needs to do before start. For example: Load routes.
listen: The listen method allows to implement how the frameworks start listening the connections.
HTTP Server
Add the HttpServer class into the http.server.ts file to handle the Express web server configuration over http protocol:
import express from "express";
import { apiRouter } from "./api.router";
import { IServer } from "./server.interface";
export class HttpServer implements IServer {
port: number;
protected framework: express.Application;
constructor(port: number) {
this.port = port;
this.framework = express();
}
async bootstrap(): Promise<void> {
// Load Express Middlewares
this.framework.use(this.loadMiddlewares());
// Routers
this.framework.use(apiRouter());
}
async listen(): Promise<void> {
this.framework.listen(this.port, () => {
console.log(`[HTTP] Server started at port: ${this.port}`);
});
}
protected loadMiddlewares() {
return [express.json()];
}
}
bootstrap: The "bootstrap" method is setting the express json middleware which parses incoming requests with JSON payloads. And also, this method is loading the API Routes.
listen: The "listen" method encapsulate the logic related to how the "ExpressJs Framework" needs to be configured to listening for connections.
loadMiddlewares: Load the builtin express middlewares. In this case, we only have "express.json()" midleware.
HTTPS Server
Basically the HttpsServer class extends from HttpServer class and override the listen method to add the https feature.
Add the HttpsServer class into the https.server.ts file to handle the Express web server configuration over https protocol:
import https from "https";
import cors from "cors";
import { HttpServer } from "./http.server";
import devCerts = require("office-addin-dev-certs");
export class HttpsServer extends HttpServer {
async listen() {
const options = await devCerts.getHttpsServerOptions();
const server = https.createServer(options, this.framework);
server.listen(this.port, () => {
console.log(`[HTTPS] Cors enabled: *`);
console.log(`[HTTPS] Server started at port: ${this.port}`);
});
}
protected loadMiddlewares() {
return [...super.loadMiddlewares(), cors()];
}
}
listen: By using the node:https module we need to provide two parameters:
The localhost certificate options (from "office-addin-dev-certs")
Express instance (this.framework) as "Request Listener"
loadMiddlewares: We override the super class method to keep the HTTP server middlewares and add the "cors" middleware only for HTTPS scope.
API Router
To define how our web server handles client requests we have to implement the ExpressJs Router. So, add the following functions into the api.router.ts file:
import {
Router,
Request as ExpressRequest,
Response as ExpressResponse,
NextFunction as ExpressNextFunction,
} from "express";
const router = Router();
export const apiRouter = () => {
router.get(
"/health-check",
(req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => {
res.status(200).json({ status: "OK" });
}
);
return router;
};
\we only define the "/health-check" route. You can have whatever you need.*
App
The app acts as the central point for managing the running process. So, add the App class into the app.ts file:
import { HttpServer } from "./www/http.server";
import { HttpsServer } from "./www/https.server";
import { IServer } from "./www/server.interface";
export class App {
constructor(private server: IServer) {}
async bootstrap(): Promise<void> {
// framework bootstrapping
await this.server.bootstrap();
}
async run(): Promise<void> {
this.server.listen();
}
}
const makeServerInstance = (port: number): IServer => {
return process.env.ENABLE_HTTPS === "true"
? new HttpsServer(port)
: new HttpServer(port);
};
async function start() {
const port = 9001;
const server = makeServerInstance(port);
const app = new App(server);
await app.bootstrap();
await app.run();
}
start();
bootstrap: The "bootstrap" method initializes the web server.
run: The "run" method encapsulates the logic to put the server running.
makeServerInstance: A "factory" function which make and return the http or https server instance.
start: The "start" function set the server port, injected the Application Dependencies (web server), initialize the application and run the server.
Package json
To run our project we have to modify the scripts section of the package.json file and ad the necessary scripts to build and run the http or https server.
...
"scripts": {
"build": "tsc --build",
"start": "ts-node-dev --respawn ./src/app.ts",
"start:https": "ENABLE_HTTPS=true npm start"
},
...
build: Build the typescript project and their dependencies.
start: Start express server over HTTP protocol
start:https: Start express server over HTTPS protocol. By passing the "ENABLE_HTTPS" environment variable equal true we tell to the App that we wanted to run a HTTPS server.
Testing
HTTP Server
In your terminal execute the followed command to start an instance of http server:
npm run start
HTTP CURL request:
curl -I http://localhost:9001/health-check
#output
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 15
ETag: W/"f-v/Y1JusChTxrQUzPtNAKycooOTA"
Date: Fri, 12 Apr 2024 04:48:20 GMT
Connection: keep-alive
Keep-Alive: timeout=5
HTTPS Server
To start an https server, execute in your terminal execute:
npm run start:https
HTTPS CURL request:
curl --verbose https://localhost:9001/health-check
#output
* Trying [::1]:9001...
* Connected to localhost (::1) port 9001
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/cert.pem
* CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384
* ALPN: server accepted http/1.1
* Server certificate:
* subject: CN=127.0.0.1
* start date: Apr 8 05:44:00 2024 GMT
* expire date: May 8 05:44:00 2024 GMT
* subjectAltName: host "localhost" matched cert's "localhost"
* issuer: CN=Developer CA for Microsoft Office Add-ins; C=US; ST=WA; L=Redmond; O=Developer CA for Microsoft Office Add-ins
* SSL certificate verify ok.
* using HTTP/1.1
> GET /health-check HTTP/1.1
> Host: localhost:9001
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Access-Control-Allow-Origin: *
< Content-Type: application/json; charset=utf-8
< Content-Length: 15
< ETag: W/"f-v/Y1JusChTxrQUzPtNAKycooOTA"
< Date: Fri, 12 Apr 2024 05:06:11 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
We can notice that the issuer of the certificate is "Developer CA for Microsoft Office Add-ins" and also, we got a new header related the "cors" configuration: "Access-Control-Allow-Origin: *"
Now that you’ve read this post, you know exactly how implement an http & https server and what useful it is when you need to integrate your deployed environment with your local env.
See you in the next article.
Get the source files from my Github repository.