This repository was archived by the owner on Mar 20, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathcreate-command-server.js
148 lines (128 loc) · 4.51 KB
/
create-command-server.js
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
'use strict';
const { WebSocketServer } = require('ws');
const robotjs = require('robotjs');
const postJSON = require('./post-json');
const DEFAULT_AT_PORT = process.env.NVDA_CONFIGURATION_SERVER_PORT ||
require('../shared/default-at-configuration-port.json');
const SUB_PROTOCOL = 'v1.aria-at.bocoup.com';
const HTTP_UNPROCESSABLE_ENTITY = 422;
const supportedAts = ['nvda'];
const broadcast = (server, message) => {
const packedMessage = JSON.stringify(message);
server.clients.forEach((websocket) => {
websocket.send(
packedMessage,
(error) => {
if (error) {
server.emit('error', `error sending message: ${error}`);
}
}
);
});
};
const onConnection = (websocket, request) => {
const send = (value) => websocket.send(JSON.stringify(value));
const {at, port} = validateRequest(request);
websocket.on('message', async (data) => {
let parsed;
const type = 'response';
try {
parsed = JSON.parse(data);
} catch ({}) {
send({type, id: null, error: `Unable to parse message: "${data}".`});
return;
}
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
send({type, id: null, error: `Malformed message: "${data}".`});
return;
}
if (parsed.type !== 'command') {
send({type, id: null, error: `Unrecognized message type: "${data}".`});
return;
}
const {id} = parsed;
if (typeof id !== 'number') {
send({type, id: null, error: `Command missing required "id": "${data}".`});
return;
}
try {
if (parsed.name === 'pressKey') {
robotjs.keyToggle(parsed.params[0], 'down');
} else if (parsed.name === 'releaseKey') {
robotjs.keyToggle(parsed.params[0], 'up');
} else if (parsed.name === 'configure') {
await postJSON(port, parsed.params[0]);
} else {
throw new Error(`Unrecognized command name: "${data}".`);
}
send({type, id, result: null});
} catch (error) {
send({type, id, error: error.message});
}
});
};
/**
* Determine if the server can initiate a WebSocket session to satisfy a given
* HTTP request.
*
* @param {http.IncomingMesssage} - an object representing an HTTP request
* which has been submitted as a "handshake"
* to initiate a WebSocket session
* @returns {Error|object} if the request can be satisfied, an object defining
* the requested assistive technology and TCP port if
* the request can be satisfied; if the request cannot
* be satisfied, an Error instance describing the
* reason
*/
const validateRequest = (request) => {
const subProtocol = request.headers['sec-websocket-protocol'];
if (subProtocol !== SUB_PROTOCOL) {
return new Error(`Unrecognized sub-protocol: ${subProtocol}"`);
}
const {searchParams} = new URL(request.url, `http://${request.headers.host}`);
const at = searchParams.get('at');
if (!at) {
return new Error(
'An assistive technology must be specified via the "at" URL query string parameter.'
);
}
if (!supportedAts.includes(at)) {
return new Error(`Unrecognized assistive technology: "${at}".`);
}
const port = searchParams.has('port') ?
parseInt(searchParams.get('port'), 10) : DEFAULT_AT_PORT;
if (Number.isNaN(port)) {
return new Error(
`Invalid value for "port" URL query string parameter: "${searchParams.get('port')}".`
);
}
return {at, port};
};
/**
* Create a server which communicates with external clients using the WebSocket
* protocol described in the project README.md file.
*
* @param {number} port - the port on which the server should listen for new
* connections
*
* @returns {Promise<EventEmitter>} an eventual value which is fulfilled when
* the server has successfully bound to the
* requested port
*/
module.exports = async function createWebSocketServer(port) {
const server = new WebSocketServer({
clientTracking: true,
verifyClient({req}, done) {
const result = validateRequest(req);
if (result instanceof Error) {
return done(false, HTTP_UNPROCESSABLE_ENTITY, result.message);
}
return done(true);
},
port,
});
await new Promise((resolve) => server.once('listening', resolve));
server.broadcast = broadcast.bind(null, server);
server.on('connection', onConnection);
return server;
};