-
-
Notifications
You must be signed in to change notification settings - Fork 15
/
server.ts
178 lines (158 loc) · 5.7 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
import * as Sentry from '@sentry/node';
import cors from 'cors';
import express from 'express';
import { expressjwt as jwt } from 'express-jwt';
import { apiKey, isAppOrigin } from './apps/index.js';
import expressStatsd from './metrics/express.js';
import { metrics } from './metrics/index.js';
import { authTokenHandler } from './routes/auth-token.js';
import { createAppHandler } from './routes/create-app.js';
import { deleteAllDataHandler } from './routes/delete-all-data.js';
import { donateHandler } from './routes/donate.js';
import { exportHandler } from './routes/export.js';
import { importHandler } from './routes/import.js';
import { getLoadoutShareHandler, loadoutShareHandler } from './routes/loadout-share.js';
import { platformInfoHandler } from './routes/platform-info.js';
import { profileHandler } from './routes/profile.js';
import { updateHandler } from './routes/update.js';
export const app = express();
app.use(expressStatsd({ client: metrics, prefix: 'http' })); // metrics
app.use(express.json({ limit: '2mb' })); // for parsing application/json
/** CORS config that allows any origin to call */
const permissiveCors = cors({
maxAge: 3600,
});
// These paths can be accessed by any caller
app.options('/', permissiveCors);
app.get('/', permissiveCors, (_, res) => res.send({ message: 'Hello from DIM!!!' }));
app.post('/', permissiveCors, (_, res) => res.status(404).send('Not Found'));
app.get('/favicon.ico', permissiveCors, (_, res) => res.status(404).send('Not Found'));
app.options('/platform_info', permissiveCors);
app.get('/platform_info', permissiveCors, platformInfoHandler);
app.options('/new_app', permissiveCors);
app.post('/new_app', permissiveCors, createAppHandler);
// Get a shared loadout
app.get('/loadout_share', permissiveCors, getLoadoutShareHandler);
// Proxy the donation info from donordrive
app.get('/donate', permissiveCors, donateHandler);
/* ****** API KEY REQUIRED ****** */
/* Any routes declared below this will require an API Key in X-API-Key header */
app.use(apiKey);
// Use the list of known DIM apps to set the CORS header
const apiKeyCors = cors({
origin: (origin, callback) => {
// We can't check the API key in OPTIONS requests (the header isn't sent)
// so we have to just check if their origin is on *any* app and let them
// through.
if (!origin || isAppOrigin(origin)) {
callback(null, true);
} else {
console.warn('UnknownOrigin', origin);
metrics.increment('apiKey.unknownOrigin.count');
callback(null, false);
}
},
maxAge: 3600,
});
app.use(apiKeyCors);
// Validate that the API key in the header is valid for this origin.
app.use((req, res, next) => {
if (req.dimApp && req.headers.origin && req.dimApp.origin !== req.headers.origin) {
console.warn('OriginMismatch', req.dimApp?.id, req.dimApp?.origin, req.headers.origin);
metrics.increment('apiKey.wrongOrigin.count');
// TODO: sentry
res.status(401).send({
error: 'OriginMismatch',
message:
'The origin of this request and the origin registered to the provided API key do not match',
});
} else {
next();
}
});
// TODO: just explicitly use API key cors on everything so it shows up
app.options('/auth/token', (_req, res) => res.send(200)); // explicitly here so it doesn't get caught by JWT
app.post('/auth/token', authTokenHandler);
/* ****** USER AUTH REQUIRED ****** */
/* Any routes declared below this will require an auth token */
app.all(
'*',
jwt({
secret: process.env.JWT_SECRET!,
requestProperty: 'jwt',
algorithms: ['HS256'],
}),
);
// Copy info from the auth token into a "user" parameter on the request.
app.use((req, _, next) => {
if (!req.jwt) {
console.error('JWT expected', req.path);
next(new Error('Expected JWT info'));
} else {
if (req.jwt.exp) {
const nowSecs = Date.now() / 1000;
if (req.jwt.exp > nowSecs) {
metrics.timing('authToken.age', req.jwt.exp - nowSecs);
} else {
metrics.increment('authToken.expired.count');
}
}
req.user = {
bungieMembershipId: parseInt(req.jwt.sub!, 10),
dimApiKey: req.jwt.iss!,
profileIds: req.jwt['profileIds'] ?? [],
};
next();
}
});
// Validate that the auth token and the API key in the header match.
app.use((req, res, next) => {
if (req.dimApp && req.dimApp.dimApiKey !== req.jwt.iss) {
console.warn('ApiKeyMismatch', req.dimApp?.id, req.dimApp?.dimApiKey, req.jwt.iss);
metrics.increment('apiKey.mismatch.count');
res.status(401).send({
error: 'ApiKeyMismatch',
message:
'The auth token was issued for a different app than the API key in X-API-Key indicates',
});
} else {
next();
}
});
// Get user data
app.get('/profile', profileHandler);
// Add or update items in the profile
app.post('/profile', updateHandler);
// Import data from old DIM, or that was exported using /export
app.post('/import', importHandler);
// Export all data for an account
app.get('/export', exportHandler);
// Delete all data for an account
app.post('/delete_all_data', deleteAllDataHandler);
// Share a loadout
app.post('/loadout_share', loadoutShareHandler);
app.use((err: Error, req, res, _next) => {
Sentry.captureException(err);
// Allow any origin to see the response
res.header('Access-Control-Allow-Origin', '*');
if (err.name === 'UnauthorizedError') {
console.warn('Unauthorized', req.dimApp?.id, req.originalUrl, err);
res.status(401).send({
error: err.name,
message: err.message,
});
} else {
console.error(
'ServerError',
req.dimApp?.id,
req.method,
req.originalUrl,
req.user?.bungieMembershipId,
err,
);
res.status(500).send({
error: err.name,
message: err.message,
});
}
});