-
Notifications
You must be signed in to change notification settings - Fork 9
/
server.ts
186 lines (155 loc) · 6.9 KB
/
server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import protobuf from 'protobufjs';
import http from 'http';
import Long from 'long';
const PORT = 8080;
const protoFilePath = 'proto/happyday.proto';
const protoRequestPath = 'happyday.HappyDayRequest';
const protoResponsePath = 'happyday.HappyDayResponse';
let ProtobufDefs: protobuf.Root;
let HappyDayRequestType: protobuf.Type;
let HappyDayResponseType: protobuf.Type;
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const wednesdayDateWeekday = 3;
// these would be usually automatically generated
interface HappyDayRequest {
date: {
seconds: Long;
nanos: number;
};
includeReason: boolean;
}
interface HappyDayResponse {
isHappyDay: boolean;
reason: string;
formattedDate: string;
err: string;
}
/** A path handler specifies the path (e.g. /happy-day/verify) on which it acts.
* If the corresponding path is requested, then runHttpServer(...) parses the body into the
* reqType Protobuf message type and runs the handler method to generate the response.
* The handler returns the Protobuf message output type - which the http server uses to serialise the response.
*/
interface PathHandler {
path: string;
method: ('GET' | 'POST' | 'HEAD')[];
reqType: protobuf.Type;
handler(reqDecoded: { [p in string]: any }): Promise<[protobuf.Type, { [p in string]: any }]>;
}
/**
* Defines two paths.
*
* <p>The path `/happy-day/verify` takes an HappyDayRequest and tells us, whether the
* given date is a happy one. (Every day except Wednesday is defined to be happy, doh).
* If the specified date is too far in the future for the epochMillis in javascript
* to handle, then an error is returned to the `err` field. Additionally, the date used
* is formatted to a string and additionally a "reason" is given, if requested.
*
* <p> The path `/echo` simply returns the input body back.
*/
function defineHandlers(): PathHandler[] {
return [
{
path: '/happy-day/verify',
method: ['GET', 'POST', 'HEAD'],
reqType: HappyDayRequestType,
async handler(req: HappyDayRequest) {
let err = '';
const date = req?.date ?? { seconds: new Long(0, 0), nanos: 0 };
const seconds = date.seconds;
const nanos = date.nanos;
const epochMillis = seconds.mul(1000).add(Math.floor(nanos / 1000 / 1000));
const epochMillisNumber = epochMillis.toNumber();
if (epochMillis.toString() !== epochMillisNumber.toString()) {
return [HappyDayResponseType, { err: err + 'Cannot handle number of millis ' + epochMillis + ' as number: ' + epochMillisNumber + '\n' }];
}
const jsDate = new Date(epochMillisNumber);
const dateWeekday = jsDate.getUTCDay();
const formattedWeekday = weekdays[dateWeekday];
console.log('Weekday is ' + dateWeekday + ', ' + formattedWeekday);
const isHappyDay = dateWeekday !== wednesdayDateWeekday;
const reason = isHappyDay ? (formattedWeekday + ' is a Happy Day! ⭐') : ('Tough luck on ' + formattedWeekday + '... 😕');
return [HappyDayResponseType, {
isHappyDay,
reason: req.includeReason ? reason : undefined,
formattedDate: jsDate.toUTCString(),
err,
} as HappyDayResponse];
}
},
{
path: '/echo',
method: ['GET', 'POST', 'HEAD'],
reqType: HappyDayRequestType,
async handler(reqDecoded: { [p in string]: any }): Promise<[protobuf.Type, { [p in string]: any }]> {
return [HappyDayRequestType, reqDecoded];
}
}
];
}
function runHttpServer(handlers: PathHandler[]) {
/** The request listener accepts the incoming requests. If a path not found in the handlers is requested,
* then it returns 404. Otherwise, it invokes the corresponding handler, by converting the
* binary Protobuf request body into the Protobuf message and using the handlers' invocation method.
* If the handler returns a successful promise, the request listener converts it back to binary and sends it
* as the response body. Otherwise, the error is logged and a 500 is returned.
* */
const requestListener: http.RequestListener = (req, res) => {
console.log('=========== ' + req.method + ' ' + req.url);
console.log(req.rawHeaders.map(s => ' ' + s));
const currentHandler = handlers.find(handler =>
handler.method.includes(req.method as any) &&
new URL(req.url ?? "", `http://${req.headers.host}`).pathname == handler.path
);
if (currentHandler === undefined) {
res.statusCode = 404;
res.end();
return;
}
let buffers: any[] = [];
req.on('data', chunk => {
buffers.push(chunk);
});
req.on('end', async () => {
const data = Buffer.concat(buffers);
console.log('Extracted body: Base64(' + data.toString('base64') + '), Binary(' + data + ')');
const result = new Promise<protobuf.Message>((resolve, reject) => {
try {
const decodedMsg = currentHandler.reqType.decode(data);
console.log('Decoded request: ' + JSON.stringify(decodedMsg, null, 2));
resolve(decodedMsg);
} catch (err) {
reject(err);
}
})
.then(decodedMsg => currentHandler.handler(decodedMsg))
.then(([responseType, respMessage]) => {
console.log('Encoding response: ' + JSON.stringify(respMessage, null, 2));
const encodedMsg = responseType.encode(respMessage).finish();
res.statusCode = 200;
res.setHeader('Content-Type', 'application/x-protobuf');
res.end(encodedMsg);
console.log('=========== 200 OK');
})
.catch(err => {
console.error('Error during request handling: ');
console.error(err);
res.statusCode = 500;
res.end();
});
await Promise.allSettled([result]);
});
};
http.createServer(requestListener).listen(PORT);
// This line is used in the test runner to detect readiness during testing.
console.log('Listening to port: ' + PORT);
}
protobuf.load(protoFilePath)
.then(root => {
ProtobufDefs = root;
HappyDayRequestType = ProtobufDefs.lookupType(protoRequestPath);
HappyDayResponseType = ProtobufDefs.lookupType(protoResponsePath);
return undefined;
})
.then(defineHandlers)
.then(handlers => runHttpServer(handlers));
//