-
-
Notifications
You must be signed in to change notification settings - Fork 52
/
request.js
180 lines (159 loc) · 4.91 KB
/
request.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
'use strict'
const DEV = process.env.NODE_ENV === 'dev'
const DEBUG = /(^|,)hafas-client(,|$)/.test(process.env.DEBUG || '')
const ProxyAgent = require('https-proxy-agent')
const {isIP} = require('net')
const {Agent: HttpsAgent} = require('https')
const roundRobin = require('@derhuerst/round-robin-scheduler')
const {randomBytes} = require('crypto')
const createHash = require('create-hash')
const pick = require('lodash/pick')
const captureStackTrace = DEV ? require('capture-stack-trace') : () => {}
const {stringify} = require('qs')
const Promise = require('pinkie-promise')
const {fetch} = require('fetch-ponyfill')({Promise})
const {parse: parseContentType} = require('content-type')
const {addErrorInfo} = require('./errors')
const proxyAddress = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || null
const localAddresses = process.env.LOCAL_ADDRESS || null
if (proxyAddress && localAddresses) {
console.error('Both env vars HTTPS_PROXY/HTTP_PROXY and LOCAL_ADDRESS are not supported.')
process.exit(1)
}
const plainAgent = new HttpsAgent({
keepAlive: true,
})
let getAgent = () => plainAgent
if (proxyAddress) {
// todo: this doesn't honor `keepAlive: true`
// related:
// - https://github.com/TooTallNate/node-https-proxy-agent/pull/112
// - https://github.com/TooTallNate/node-agent-base/issues/5
const agent = new ProxyAgent(proxyAddress)
getAgent = () => agent
} else if (localAddresses) {
const agents = process.env.LOCAL_ADDRESS.split(',')
.map((addr) => {
const family = isIP(addr)
if (family === 0) throw new Error('invalid local address:' + addr)
return new HttpsAgent({
localAddress: addr, family,
keepAlive: true,
})
})
const pool = roundRobin(agents)
getAgent = () => pool.get()
}
const id = randomBytes(3).toString('hex')
const randomizeUserAgent = (userAgent) => {
let ua = userAgent
for (
let i = Math.round(5 + Math.random() * 5);
i < ua.length;
i += Math.round(5 + Math.random() * 5)
) {
ua = ua.slice(0, i) + id + ua.slice(i)
i += id.length
}
return ua
}
const md5 = input => createHash('md5').update(input).digest()
// todo [breaking]: remove userAgent parameter
const request = (ctx, userAgent, reqData) => {
const {profile, opt} = ctx
const body = profile.transformReqBody(ctx, {
// todo: is it `eng` actually?
// RSAG has `deu` instead of `de`
lang: opt.language || profile.defaultLanguage || 'en',
svcReqL: [reqData]
})
Object.assign(body, pick(profile, [
'client', // client identification
'ext', // ?
'ver', // HAFAS protocol version
'auth', // static authentication
]))
const req = profile.transformReq(ctx, {
agent: getAgent(),
method: 'post',
// todo: CORS? referrer policy?
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
'Accept-Encoding': 'gzip, br, deflate',
'Accept': 'application/json',
'user-agent': randomizeUserAgent(userAgent),
'connection': 'keep-alive', // prevent excessive re-connecting
},
redirect: 'follow',
query: {}
})
if (DEBUG) console.error(req.body)
if (profile.addChecksum || profile.addMicMac) {
if (!Buffer.isBuffer(profile.salt) && 'string' !== typeof profile.salt) {
throw new TypeError('profile.salt must be a Buffer or a string.')
}
// Buffer.from(buf, 'hex') just returns buf
const salt = Buffer.from(profile.salt, 'hex')
if (profile.addChecksum) {
const checksum = md5(Buffer.concat([
Buffer.from(req.body, 'utf8'),
salt,
]))
req.query.checksum = checksum.toString('hex')
}
if (profile.addMicMac) {
const mic = md5(Buffer.from(req.body, 'utf8'))
req.query.mic = mic.toString('hex')
const micAsHex = Buffer.from(mic.toString('hex'), 'utf8')
const mac = md5(Buffer.concat([micAsHex, salt]))
req.query.mac = mac.toString('hex')
}
}
const url = profile.endpoint + '?' + stringify(req.query)
// Async stack traces are not supported everywhere yet, so we create our own.
const err = new Error()
err.isHafasError = true // todo: rename to `isHafasClientError`
err.request = req.body // todo: commit as bugfix
err.url = url
captureStackTrace(err)
return fetch(url, req)
.then((res) => {
err.statusCode = res.status
if (!res.ok) {
err.message = res.statusText
throw err
}
let cType = res.headers.get('content-type')
if (cType) {
const {type} = parseContentType(cType)
if (type !== 'application/json') {
const err = new Error('invalid response content-type: ' + cType)
err.response = res
throw err
}
}
return res.json()
})
.then((b) => {
if (DEBUG) console.error(JSON.stringify(b))
if (b.err && b.err !== 'OK') {
addErrorInfo(err, b.err, b.errTxt, b.id)
throw err
}
if (!b.svcResL || !b.svcResL[0]) {
err.message = 'invalid response'
throw err
}
if (b.svcResL[0].err !== 'OK') {
addErrorInfo(err, b.svcResL[0].err, b.svcResL[0].errTxt, b.id)
throw err
}
const res = b.svcResL[0].res
return {
res,
common: profile.parseCommon({...ctx, res})
}
})
}
module.exports = request