Skip to content

Commit eb76b1a

Browse files
authored
Merge pull request #15774 from github/repo-sync
repo sync
2 parents eaddd39 + 8835a17 commit eb76b1a

7 files changed

Lines changed: 70 additions & 207 deletions

File tree

middleware/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import archivedEnterpriseVersionsAssets from './archived-enterprise-versions-ass
3636
import events from './events.js'
3737
import search from './search.js'
3838
import healthz from './healthz.js'
39+
import remoteIP from './remote-ip.js'
3940
import archivedEnterpriseVersions from './archived-enterprise-versions.js'
4041
import robots from './robots.js'
4142
import earlyAccessLinks from './contextualizers/early-access-links.js'
@@ -182,6 +183,10 @@ export default function (app) {
182183
// *** Early exits ***
183184
// Don't use the proxy's IP, use the requester's for rate limiting
184185
// See https://expressjs.com/en/guide/behind-proxies.html
186+
// Essentially, setting this means it believe that the IP is the
187+
// first of the `X-Forwarded-For` header values.
188+
// If it was 0 (or false), the value would be that
189+
// of `req.socket.remoteAddress`.
185190
app.set('trust proxy', 1)
186191
app.use(rateLimit)
187192
app.use(instrument(handleInvalidPaths, './handle-invalid-paths'))
@@ -237,6 +242,7 @@ export default function (app) {
237242
app.use('/events', asyncMiddleware(instrument(events, './events')))
238243
app.use('/search', asyncMiddleware(instrument(search, './search')))
239244
app.use('/healthz', asyncMiddleware(instrument(healthz, './healthz')))
245+
app.get('/_ip', asyncMiddleware(instrument(remoteIP, './remoteIP')))
240246

241247
// Check for a dropped connection before proceeding (again)
242248
app.use(haltOnDroppedConnection)

middleware/rate-limit.js

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,27 @@
11
import rateLimit from 'express-rate-limit'
2-
import RedisStore from 'rate-limit-redis'
3-
import createRedisClient from '../lib/redis/create-client.js'
2+
import statsd from '../lib/statsd.js'
43

5-
const isProduction = process.env.NODE_ENV === 'production'
6-
const { REDIS_URL } = process.env
7-
const rateLimitDatabaseNumber = 0
84
const EXPIRES_IN_AS_SECONDS = 60
95

10-
// The reason the options object is created outside like this is for the
11-
// necessity of avoiding setting a key called `store` even if it's set
12-
// to `undefined`.
13-
// More context here: https://github.com/nfriedly/express-rate-limit/issues/289
14-
const options = {
15-
// 1 minute (or practically unlimited outside of production)
16-
windowMs: isProduction ? EXPIRES_IN_AS_SECONDS * 1000 : 1, // Non-Redis configuration in `ms`. Used as a fallback when Redis is not working or active.
6+
export default rateLimit({
7+
// 1 minute
8+
windowMs: EXPIRES_IN_AS_SECONDS * 1000,
179
// limit each IP to X requests per windowMs
18-
max: 250,
19-
// Don't rate limit requests for 200s and redirects
20-
// Or anything with a status code less than 400
21-
skipSuccessfulRequests: true,
22-
}
10+
// We currently have about 25 instances in production. That's routed
11+
// in Azure to spread the requests to each healthy instance.
12+
// So, the true rate limit, per `windowMs`, is this number multiplied
13+
// by the current number of instances.
14+
// We have see DDoS attempts against prod that hits the `/` endpoint
15+
// (and not following the redirect to `/en`) at roughly 200k per minute.
16+
max: 100,
2317

24-
// When available, use Redis; if not, defaults to an in-memory store
25-
if (REDIS_URL) {
26-
options.store = new RedisStore({
27-
client: createRedisClient({
28-
url: REDIS_URL,
29-
db: rateLimitDatabaseNumber,
30-
name: 'rate-limit',
31-
}),
32-
// 1 minute (or practically unlimited outside of production)
33-
expiry: isProduction ? EXPIRES_IN_AS_SECONDS : 1, // Redis configuration in `s`
34-
// If Redis is not connected, let the request succeed as failover
35-
passIfNotConnected: true,
36-
})
37-
}
38-
39-
export default rateLimit(options)
18+
handler: (request, response, next, options) => {
19+
const tags = [`url:${request.url}`, `ip:${request.ip}`]
20+
statsd.increment('rate_limit', 1, tags)
21+
// NOTE! At the time of writing, the actual rate limiting is disabled!
22+
// At least we can start recording how often this happens in Datadog.
23+
// The following line is commented out and replaced with `next()`
24+
// response.status(options.statusCode).send(options.message)
25+
next()
26+
},
27+
})

middleware/remote-ip.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { cacheControlFactory } from './cache-control.js'
2+
3+
const noCacheControl = cacheControlFactory(0)
4+
5+
export default async function remoteIp(req, res, next) {
6+
noCacheControl(res)
7+
res.send(
8+
`ip=${req.ip}\t` +
9+
`x-forwarded-for=${req.headers['x-forwarded-for']}\t` +
10+
`fastly-client-ip=${req.headers['fastly-client-ip']}`
11+
)
12+
}

package-lock.json

Lines changed: 6 additions & 139 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,10 @@
6060
"node-fetch": "^3.2.0",
6161
"parse5": "^6.0.1",
6262
"port-used": "^2.0.8",
63-
"rate-limit-redis": "^2.1.0",
6463
"react": "^17.0.2",
6564
"react-dom": "^17.0.2",
6665
"react-markdown": "^8.0.0",
6766
"react-syntax-highlighter": "^15.4.5",
68-
"redis": "^3.1.2",
6967
"rehype-autolink-headings": "^6.1.1",
7068
"rehype-highlight": "^5.0.2",
7169
"rehype-raw": "^6.1.1",

tests/routing/remote-ip.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { get } from '../helpers/supertest.js'
2+
import { expect, jest } from '@jest/globals'
3+
4+
describe('remote ip debugging', () => {
5+
jest.setTimeout(5 * 60 * 1000)
6+
7+
test('basics', async () => {
8+
const res = await get('/_ip')
9+
expect(res.statusCode).toBe(200)
10+
const kv = res.text.trim().split(/\s/g)
11+
expect(kv[0].startsWith('ip=')).toBeTruthy()
12+
expect(kv[1].startsWith('x-forwarded-for=')).toBeTruthy()
13+
expect(kv[2].startsWith('fastly-client-ip=')).toBeTruthy()
14+
})
15+
16+
test('carrying the x-forwarded-for header', async () => {
17+
const res = await get('/_ip', {
18+
headers: {
19+
'X-Forwarded-For': '123.123.0.1',
20+
},
21+
})
22+
expect(res.statusCode).toBe(200)
23+
expect(res.text.includes('x-forwarded-for=123.123.0.1')).toBeTruthy()
24+
})
25+
})

0 commit comments

Comments
 (0)