make it possible to stream public timelines without authorization (#5977)

* make it possible to stream public timelines without authorization

* Fix

* Make eslint allow `value == null`

* Remove redundant line

* Improve style and revert .eslintrc.yml

* Fix streamWsEnd

* Show IP address instead of (anonymous user)

* Add missing semicolon
signup-info-prompt
nullkal 2017-12-12 23:13:24 +09:00 committed by Eugen Rochko
parent 19257d91bf
commit cfea28216f
1 changed files with 71 additions and 28 deletions

View File

@ -97,6 +97,8 @@ const startWorker = (workerId) => {
}; };
const app = express(); const app = express();
app.set('trusted proxy', process.env.TRUSTED_PROXY_IP || 'loopback,uniquelocal');
const pgPool = new pg.Pool(Object.assign(pgConfigs[env], dbUrlToConfig(process.env.DATABASE_URL))); const pgPool = new pg.Pool(Object.assign(pgConfigs[env], dbUrlToConfig(process.env.DATABASE_URL)));
const server = http.createServer(app); const server = http.createServer(app);
const redisNamespace = process.env.REDIS_NAMESPACE || null; const redisNamespace = process.env.REDIS_NAMESPACE || null;
@ -177,6 +179,12 @@ const startWorker = (workerId) => {
next(); next();
}; };
const setRemoteAddress = (req, res, next) => {
req.remoteAddress = req.connection.remoteAddress;
next();
};
const accountFromToken = (token, req, next) => { const accountFromToken = (token, req, next) => {
pgPool.connect((err, client, done) => { pgPool.connect((err, client, done) => {
if (err) { if (err) {
@ -208,17 +216,22 @@ const startWorker = (workerId) => {
}); });
}; };
const accountFromRequest = (req, next) => { const accountFromRequest = (req, next, required = true) => {
const authorization = req.headers.authorization; const authorization = req.headers.authorization;
const location = url.parse(req.url, true); const location = url.parse(req.url, true);
const accessToken = location.query.access_token; const accessToken = location.query.access_token;
if (!authorization && !accessToken) { if (!authorization && !accessToken) {
const err = new Error('Missing access token'); if (required) {
err.statusCode = 401; const err = new Error('Missing access token');
err.statusCode = 401;
next(err); next(err);
return; return;
} else {
next();
return;
}
} }
const token = authorization ? authorization.replace(/^Bearer /, '') : accessToken; const token = authorization ? authorization.replace(/^Bearer /, '') : accessToken;
@ -226,7 +239,17 @@ const startWorker = (workerId) => {
accountFromToken(token, req, next); accountFromToken(token, req, next);
}; };
const PUBLIC_STREAMS = [
'public',
'public:local',
'hashtag',
'hashtag:local',
];
const wsVerifyClient = (info, cb) => { const wsVerifyClient = (info, cb) => {
const location = url.parse(info.req.url, true);
const authRequired = !PUBLIC_STREAMS.some(stream => stream === location.query.stream);
accountFromRequest(info.req, err => { accountFromRequest(info.req, err => {
if (!err) { if (!err) {
cb(true, undefined, undefined); cb(true, undefined, undefined);
@ -234,16 +257,24 @@ const startWorker = (workerId) => {
log.error(info.req.requestId, err.toString()); log.error(info.req.requestId, err.toString());
cb(false, 401, 'Unauthorized'); cb(false, 401, 'Unauthorized');
} }
}); }, authRequired);
}; };
const PUBLIC_ENDPOINTS = [
'/api/v1/streaming/public',
'/api/v1/streaming/public/local',
'/api/v1/streaming/hashtag',
'/api/v1/streaming/hashtag/local',
];
const authenticationMiddleware = (req, res, next) => { const authenticationMiddleware = (req, res, next) => {
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
next(); next();
return; return;
} }
accountFromRequest(req, next); const authRequired = !PUBLIC_ENDPOINTS.some(endpoint => endpoint === req.path);
accountFromRequest(req, next, authRequired);
}; };
const errorMiddleware = (err, req, res, {}) => { const errorMiddleware = (err, req, res, {}) => {
@ -275,8 +306,10 @@ const startWorker = (workerId) => {
}; };
const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false, notificationOnly = false) => { const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false, notificationOnly = false) => {
const accountId = req.accountId || req.remoteAddress;
const streamType = notificationOnly ? ' (notification)' : ''; const streamType = notificationOnly ? ' (notification)' : '';
log.verbose(req.requestId, `Starting stream from ${id} for ${req.accountId}${streamType}`); log.verbose(req.requestId, `Starting stream from ${id} for ${accountId}${streamType}`);
const listener = message => { const listener = message => {
const { event, payload, queued_at } = JSON.parse(message); const { event, payload, queued_at } = JSON.parse(message);
@ -286,7 +319,7 @@ const startWorker = (workerId) => {
const delta = now - queued_at; const delta = now - queued_at;
const encodedPayload = typeof payload === 'object' ? JSON.stringify(payload) : payload; const encodedPayload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
log.silly(req.requestId, `Transmitting for ${req.accountId}: ${event} ${encodedPayload} Delay: ${delta}ms`); log.silly(req.requestId, `Transmitting for ${accountId}: ${event} ${encodedPayload} Delay: ${delta}ms`);
output(event, encodedPayload); output(event, encodedPayload);
}; };
@ -313,26 +346,30 @@ const startWorker = (workerId) => {
return; return;
} }
const queries = [ if (!req.accountId) {
client.query(`SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})) OR (account_id = $2 AND target_account_id = $1) UNION SELECT 1 FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})`, [req.accountId, unpackedPayload.account.id].concat(targetAccountIds)), const queries = [
]; client.query(`SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})) OR (account_id = $2 AND target_account_id = $1) UNION SELECT 1 FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})`, [req.accountId, unpackedPayload.account.id].concat(targetAccountIds)),
];
if (accountDomain) { if (accountDomain) {
queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain])); queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain]));
}
Promise.all(queries).then(values => {
done();
if (values[0].rows.length > 0 || (values.length > 1 && values[1].rows.length > 0)) {
return;
} }
Promise.all(queries).then(values => {
done();
if (values[0].rows.length > 0 || (values.length > 1 && values[1].rows.length > 0)) {
return;
}
transmit();
}).catch(err => {
done();
log.error(err);
});
} else {
transmit(); transmit();
}).catch(err => { }
done();
log.error(err);
});
}); });
} else { } else {
transmit(); transmit();
@ -345,13 +382,15 @@ const startWorker = (workerId) => {
// Setup stream output to HTTP // Setup stream output to HTTP
const streamToHttp = (req, res) => { const streamToHttp = (req, res) => {
const accountId = req.accountId || req.remoteAddress;
res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Transfer-Encoding', 'chunked'); res.setHeader('Transfer-Encoding', 'chunked');
const heartbeat = setInterval(() => res.write(':thump\n'), 15000); const heartbeat = setInterval(() => res.write(':thump\n'), 15000);
req.on('close', () => { req.on('close', () => {
log.verbose(req.requestId, `Ending stream for ${req.accountId}`); log.verbose(req.requestId, `Ending stream for ${accountId}`);
clearInterval(heartbeat); clearInterval(heartbeat);
}); });
@ -383,8 +422,10 @@ const startWorker = (workerId) => {
// Setup stream end for WebSockets // Setup stream end for WebSockets
const streamWsEnd = (req, ws, closeHandler = false) => (id, listener) => { const streamWsEnd = (req, ws, closeHandler = false) => (id, listener) => {
const accountId = req.accountId || req.remoteAddress;
ws.on('close', () => { ws.on('close', () => {
log.verbose(req.requestId, `Ending stream for ${req.accountId}`); log.verbose(req.requestId, `Ending stream for ${accountId}`);
unsubscribe(id, listener); unsubscribe(id, listener);
if (closeHandler) { if (closeHandler) {
closeHandler(); closeHandler();
@ -392,7 +433,7 @@ const startWorker = (workerId) => {
}); });
ws.on('error', () => { ws.on('error', () => {
log.verbose(req.requestId, `Ending stream for ${req.accountId}`); log.verbose(req.requestId, `Ending stream for ${accountId}`);
unsubscribe(id, listener); unsubscribe(id, listener);
if (closeHandler) { if (closeHandler) {
closeHandler(); closeHandler();
@ -401,6 +442,7 @@ const startWorker = (workerId) => {
}; };
app.use(setRequestId); app.use(setRequestId);
app.use(setRemoteAddress);
app.use(allowCrossDomain); app.use(allowCrossDomain);
app.use(authenticationMiddleware); app.use(authenticationMiddleware);
app.use(errorMiddleware); app.use(errorMiddleware);
@ -451,6 +493,7 @@ const startWorker = (workerId) => {
const req = ws.upgradeReq; const req = ws.upgradeReq;
const location = url.parse(req.url, true); const location = url.parse(req.url, true);
req.requestId = uuid.v4(); req.requestId = uuid.v4();
req.remoteAddress = ws._socket.remoteAddress;
ws.isAlive = true; ws.isAlive = true;