-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
server.js
235 lines (212 loc) · 8.95 KB
/
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
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
/**
* This is the main entry point for the application, a simple server that
* runs some checks, and then serves up the app from the ./dist directory
* Also imports some routes for status checks/ ping and config saving
* Note: The app must first be built (yarn build) before this script is run
* */
/* Import built-in Node server modules */
const fs = require('fs');
const os = require('os');
const dns = require('dns');
const http = require('http');
const path = require('path');
const util = require('util');
const crypto = require('crypto');
/* Import NPM dependencies */
const yaml = require('js-yaml');
const RateLimit = require('express-rate-limit');
/* Import Express + middleware functions */
const express = require('express');
const basicAuth = require('express-basic-auth');
const history = require('connect-history-api-fallback');
/* Kick of some basic checks */
require('./services/update-checker'); // Checks if there are any updates available, prints message
let config = {}; // setup the config
config = require('./services/config-validator'); // Include and kicks off the config file validation script
/* Include route handlers for API endpoints */
const statusCheck = require('./services/status-check'); // Used by the status check feature, uses GET
const saveConfig = require('./services/save-config'); // Saves users new conf.yml to file-system
const rebuild = require('./services/rebuild-app'); // A script to programmatically trigger a build
const systemInfo = require('./services/system-info'); // Basic system info, for resource widget
const sslServer = require('./services/ssl-server'); // TLS-enabled web server
const corsProxy = require('./services/cors-proxy'); // Enables API requests to CORS-blocked services
const getUser = require('./services/get-user'); // Enables server side user lookup
/* Helper functions, and default config */
const printMessage = require('./services/print-message'); // Function to print welcome msg on start
const ENDPOINTS = require('./src/utils/defaults').serviceEndpoints; // API endpoint URL paths
/* Checks if app is running within a container, from env var */
const isDocker = !!process.env.IS_DOCKER;
/* Checks env var for port. If undefined, will use Port 8080 for Docker, or 4000 for metal */
const port = process.env.PORT || (isDocker ? 8080 : 4000);
/* Checks env var for host. If undefined, will use 0.0.0.0 */
const host = process.env.HOST || '0.0.0.0';
/* Indicates for the webpack config, that running as a server */
process.env.IS_SERVER = 'True';
/* Attempts to get the users local IP, used as part of welcome message */
const getLocalIp = () => {
const dnsLookup = util.promisify(dns.lookup);
return dnsLookup(os.hostname());
};
/* Gets the users local IP and port, then calls to print welcome message */
const printWelcomeMessage = () => {
try {
getLocalIp().then(({ address }) => {
const ip = process.env.HOST || address || 'localhost';
console.log(printMessage(ip, port, isDocker)); // eslint-disable-line no-console
});
} catch (e) {
// No clue what could of gone wrong here, but print fallback message if above failed
console.log(`Shipyard server has started (${port})`); // eslint-disable-line no-console
}
};
/* Just console.warns an error */
const printWarning = (msg, error) => {
console.warn(`\x1b[103m\x1b[34m${msg}\x1b[0m\n`, error || ''); // eslint-disable-line no-console
};
/* Load appConfig.auth.users from config (if present) for authorization purposes */
function loadUserConfig() {
try {
const filePath = path.join(__dirname, process.env.USER_DATA_DIR || 'user-data', 'conf.yml');
const fileContents = fs.readFileSync(filePath, 'utf8');
const data = yaml.load(fileContents);
return data?.appConfig?.auth?.users || null;
} catch (e) {
return [];
}
}
/* If HTTP auth is enabled, and no username/password are pre-set, then check passed credentials */
function customAuthorizer(username, password) {
const sha256 = (input) => crypto.createHash('sha256').update(input).digest('hex').toUpperCase();
const generateUserToken = (user) => {
if (!user.user || (!user.hash && !user.password)) return '';
const strAndUpper = (input) => input.toString().toUpperCase();
const passwordHash = user.hash || sha256(process.env[user.password]);
const sha = sha256(strAndUpper(user.user) + strAndUpper(passwordHash));
return strAndUpper(sha);
};
if (password.startsWith('Bearer ')) {
const token = password.slice('Bearer '.length);
const users = loadUserConfig();
return users.some(user => generateUserToken(user) === token);
} else {
const users = loadUserConfig();
const userHash = sha256(password);
return users.some(user => (
user.user.toLowerCase() === username.toLowerCase() && user.hash.toUpperCase() === userHash
));
}
}
/* If a username and password are set, setup auth for config access, otherwise skip */
function getBasicAuthMiddleware() {
const configUsers = process.env.ENABLE_HTTP_AUTH ? loadUserConfig() : null;
const { BASIC_AUTH_USERNAME, BASIC_AUTH_PASSWORD } = process.env;
if (BASIC_AUTH_USERNAME && BASIC_AUTH_PASSWORD) {
return basicAuth({
users: { [BASIC_AUTH_USERNAME]: BASIC_AUTH_PASSWORD },
challenge: true,
unauthorizedResponse: () => 'Unauthorized - Incorrect username or password',
});
} else if ((configUsers && configUsers.length > 0)) {
return basicAuth({
authorizer: customAuthorizer,
challenge: true,
unauthorizedResponse: () => 'Unauthorized - Incorrect token',
});
} else {
return (req, res, next) => next();
}
}
const protectConfig = getBasicAuthMiddleware();
/* A middleware function for Connect, that filters requests based on method type */
const method = (m, mw) => (req, res, next) => (req.method === m ? mw(req, res, next) : next());
// set up rate limiter: maximum of 100 requests per 15 minutes
const limiter = RateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requests per windowMs
});
const app = express()
// Apply rate limiter to all requests
.use(limiter)
// Load SSL redirection middleware
.use(sslServer.middleware)
// Load middlewares for parsing JSON, and supporting HTML5 history routing
.use(express.json({ limit: '1mb' }))
// GET endpoint to run status of a given URL with GET request
.use(ENDPOINTS.statusCheck, (req, res) => {
try {
statusCheck(req.url, async (results) => {
await res.end(results);
});
} catch (e) {
printWarning(`Error running status check for ${req.url}\n`, e);
}
})
// POST Endpoint used to save config, by writing config file to disk
.use(ENDPOINTS.save, method('POST', (req, res) => {
try {
saveConfig(req.body, (results) => { res.end(results); });
config = req.body.config; // update the config
} catch (e) {
printWarning('Error writing config file to disk', e);
res.end(JSON.stringify({ success: false, message: e }));
}
}))
// GET endpoint to trigger a build, and respond with success status and output
.use(ENDPOINTS.rebuild, (req, res) => {
rebuild().then((response) => {
res.end(JSON.stringify(response));
}).catch((response) => {
res.end(JSON.stringify(response));
});
})
// GET endpoint to return system info, for widget
.use(ENDPOINTS.systemInfo, (req, res) => {
try {
const results = systemInfo();
systemInfo.success = true;
res.end(JSON.stringify(results));
} catch (e) {
res.end(JSON.stringify({ success: false, message: e }));
}
})
// GET for accessing non-CORS API services
.use(ENDPOINTS.corsProxy, (req, res) => {
try {
corsProxy(req, res);
} catch (e) {
res.end(JSON.stringify({ success: false, message: e }));
}
})
// GET endpoint to return user info
.use(ENDPOINTS.getUser, (req, res) => {
try {
const user = getUser(config, req);
res.end(JSON.stringify(user));
} catch (e) {
res.end(JSON.stringify({ success: false, message: e }));
}
})
// Middleware to serve any .yml files in USER_DATA_DIR with optional protection
.get('/*.yml', protectConfig, (req, res) => {
const ymlFile = req.path.split('/').pop();
res.sendFile(path.join(__dirname, process.env.USER_DATA_DIR || 'user-data', ymlFile));
})
// Serves up static files
.use(express.static(path.join(__dirname, process.env.USER_DATA_DIR || 'user-data')))
.use(express.static(path.join(__dirname, 'dist')))
.use(express.static(path.join(__dirname, 'public'), { index: 'initialization.html' }))
.use(history())
// If no other route is matched, serve up the index.html with a 404 status
.use((req, res) => {
res.status(404).sendFile(path.join(__dirname, 'dist', 'index.html'));
});
/* Create HTTP server from app on port, and print welcome message */
http.createServer(app)
.listen(port, host, () => {
printWelcomeMessage();
})
.on('error', (err) => {
printWarning('Unable to start Shipyard\'s Node server', err);
});
/* Check, and if possible start SSL server too */
sslServer.startSSLServer(app);