merge new-vbb-protocol into master

This commit is contained in:
Jannis R 2018-03-05 20:57:26 +01:00
commit 95833edd21
No known key found for this signature in database
GPG key ID: 0FE83946296A88A5
10 changed files with 128 additions and 64 deletions

View file

@ -7,7 +7,7 @@ const formatLocationIdentifier = (data) => {
for (let key in data) { for (let key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) continue if (!Object.prototype.hasOwnProperty.call(data, key)) continue
str += key + '=' + data[key] + sep // todo: escape str += key + '=' + data[key] + sep // todo: escape, but how?
} }
return str return str

View file

@ -13,9 +13,9 @@ const formatPoi = (p) => {
lid: formatLocationIdentifier({ lid: formatLocationIdentifier({
A: '4', // POI A: '4', // POI
O: p.name, O: p.name,
L: p.id,
X: formatCoord(p.longitude), X: formatCoord(p.longitude),
Y: formatCoord(p.latitude), Y: formatCoord(p.latitude)
L: p.id
}) })
} }
} }

View file

@ -1,5 +1,15 @@
'use strict' 'use strict'
const formatStation = id => ({type: 'S', lid: 'L=' + id}) const formatLocationIdentifier = require('./location-identifier')
const formatStation = (id) => {
return {
// todo: name necessary?
lid: formatLocationIdentifier({
A: '1', // station
L: id
})
}
}
module.exports = formatStation module.exports = formatStation

View file

@ -101,11 +101,19 @@ const createClient = (profile, request = _request) => {
filters.push(profile.filters.accessibility[opt.accessibility]) filters.push(profile.filters.accessibility[opt.accessibility])
} }
const query = profile.transformJourneysQuery({ // With protocol version `1.16`, the VBB endpoint fails with
outDate: profile.formatDate(profile, opt.when), // `CGI_READ_FAILED` if you pass `numF`, the parameter for the number
outTime: profile.formatTime(profile, opt.when), // of results. To circumvent this, we loop here, collecting journeys
// until we have enough.
// see https://github.com/derhuerst/hafas-client/pull/23#issuecomment-370246163
// todo: check if `numF` is supported again, revert this change
const journeys = []
const more = (when, journeysRef) => {
const query = {
outDate: profile.formatDate(profile, when),
outTime: profile.formatTime(profile, when),
ctxScr: journeysRef, ctxScr: journeysRef,
numF: opt.results, // numF: opt.results,
getPasslist: !!opt.passedStations, getPasslist: !!opt.passedStations,
maxChg: opt.transfers, maxChg: opt.transfers,
minChgTime: opt.transferTime, minChgTime: opt.transferTime,
@ -120,24 +128,39 @@ const createClient = (profile, request = _request) => {
outFrwd: true, // todo: what is this? outFrwd: true, // todo: what is this?
getIV: false, // todo: walk & bike as alternatives? getIV: false, // todo: walk & bike as alternatives?
getPolyline: false // todo: shape for displaying on a map? getPolyline: false // todo: shape for displaying on a map?
}, opt) }
return request(profile, { return request(profile, {
cfg: {polyEnc: 'GPA'}, cfg: {polyEnc: 'GPA'},
meth: 'TripSearch', meth: 'TripSearch',
req: query req: profile.transformJourneysQuery(query, opt)
}) })
.then((d) => { .then((d) => {
if (!Array.isArray(d.outConL)) return [] if (!Array.isArray(d.outConL)) return []
const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks) const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks)
const res = d.outConL.map(parse) if (!journeys.earlierRef) journeys.earlierRef = d.outCtxScrB
if (d.outCtxScrB) res.earlierRef = d.outCtxScrB let latestDep = -Infinity
if (d.outCtxScrF) res.laterRef = d.outCtxScrF for (let j of d.outConL) {
return res j = parse(j)
journeys.push(j)
if (journeys.length === opt.results) { // collected enough
journeys.laterRef = d.outCtxScrF
return journeys
}
const dep = +new Date(j.departure)
if (dep > latestDep) latestDep = dep
}
const when = new Date(latestDep)
return more(when, d.outCtxScrF) // otherwise continue
}) })
} }
return more(opt.when, journeysRef)
}
const locations = (query, opt = {}) => { const locations = (query, opt = {}) => {
if (!isNonEmptyString(query)) { if (!isNonEmptyString(query)) {
throw new Error('query must be a non-empty string.') throw new Error('query must be a non-empty string.')

View file

@ -26,6 +26,10 @@ const filters = require('../format/filters')
const id = x => x const id = x => x
const defaultProfile = { const defaultProfile = {
salt: null,
addChecksum: false,
addMicMac: false,
transformReqBody: id, transformReqBody: id,
transformReq: id, transformReq: id,

View file

@ -1,5 +1,6 @@
'use strict' 'use strict'
const crypto = require('crypto')
let captureStackTrace = () => {} let captureStackTrace = () => {}
if (process.env.NODE_ENV === 'dev') { if (process.env.NODE_ENV === 'dev') {
captureStackTrace = require('capture-stack-trace') captureStackTrace = require('capture-stack-trace')
@ -8,6 +9,8 @@ const {stringify} = require('query-string')
const Promise = require('pinkie-promise') const Promise = require('pinkie-promise')
const {fetch} = require('fetch-ponyfill')({Promise}) const {fetch} = require('fetch-ponyfill')({Promise})
const md5 = input => crypto.createHash('md5').update(input).digest()
const request = (profile, data) => { const request = (profile, data) => {
const body = profile.transformReqBody({lang: 'en', svcReqL: [data]}) const body = profile.transformReqBody({lang: 'en', svcReqL: [data]})
const req = profile.transformReq({ const req = profile.transformReq({
@ -19,14 +22,37 @@ const request = (profile, data) => {
'Accept-Encoding': 'gzip, deflate', 'Accept-Encoding': 'gzip, deflate',
'user-agent': 'https://github.com/derhuerst/hafas-client' 'user-agent': 'https://github.com/derhuerst/hafas-client'
}, },
query: null query: {}
}) })
const url = profile.endpoint + (req.query ? '?' + stringify(req.query) : '')
if (profile.addChecksum || profile.addMicMac) {
if (!Buffer.isBuffer(profile.salt)) {
throw new Error('profile.salt must be a Buffer.')
}
if (profile.addChecksum) {
const checksum = md5(Buffer.concat([
Buffer.from(req.body, 'utf8'),
profile.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, profile.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. // Async stack traces are not supported everywhere yet, so we create our own.
const err = new Error() const err = new Error()
err.isHafasError = true err.isHafasError = true
err.request = body err.request = body
err.url = url
captureStackTrace(err) captureStackTrace(err)
return fetch(url, req) return fetch(url, req)

View file

@ -1,7 +1,5 @@
'use strict' 'use strict'
const crypto = require('crypto')
const _createParseLine = require('../../parse/line') const _createParseLine = require('../../parse/line')
const _createParseJourney = require('../../parse/journey') const _createParseJourney = require('../../parse/journey')
const _formatStation = require('../../format/station') const _formatStation = require('../../format/station')
@ -23,17 +21,6 @@ const transformReqBody = (body) => {
return body return body
} }
const salt = 'bdI8UVj40K5fvxwf'
const transformReq = (req) => {
const hash = crypto.createHash('md5')
hash.update(req.body + salt)
if (!req.query) req.query = {}
req.query.checksum = hash.digest('hex')
return req
}
const transformJourneysQuery = (query, opt) => { const transformJourneysQuery = (query, opt) => {
const filters = query.jnyFltrL const filters = query.jnyFltrL
if (opt.bike) filters.push(bike) if (opt.bike) filters.push(bike)
@ -142,8 +129,11 @@ const dbProfile = {
locale: 'de-DE', locale: 'de-DE',
timezone: 'Europe/Berlin', timezone: 'Europe/Berlin',
endpoint: 'https://reiseauskunft.bahn.de/bin/mgate.exe', endpoint: 'https://reiseauskunft.bahn.de/bin/mgate.exe',
salt: Buffer.from('bdI8UVj40K5fvxwf', 'utf8'),
addChecksum: true,
transformReqBody, transformReqBody,
transformReq,
transformJourneysQuery, transformJourneysQuery,
products: modes.allProducts, products: modes.allProducts,

View file

@ -20,10 +20,10 @@ const modes = require('./modes')
const formatBitmask = createFormatBitmask(modes) const formatBitmask = createFormatBitmask(modes)
const transformReqBody = (body) => { const transformReqBody = (body) => {
body.client = {type: 'IPA', id: 'BVG', name: 'FahrInfo', v: '4070700'} body.client = {type: 'IPA', id: 'VBB', name: 'vbbPROD', v: '4010300'}
body.ext = 'BVG.1' body.ext = 'VBB.1'
body.ver = '1.15' // todo: 1.16 with `mic` and `mac` query params body.ver = '1.16'
body.auth = {type: 'AID', aid: '1Rxs112shyHLatUX4fofnmdxK'} body.auth = {type: 'AID', aid: 'hafas-vbb-apps'}
return body return body
} }
@ -167,7 +167,13 @@ const formatProducts = (products) => {
const vbbProfile = { const vbbProfile = {
locale: 'de-DE', locale: 'de-DE',
timezone: 'Europe/Berlin', timezone: 'Europe/Berlin',
endpoint: 'https://bvg-apps.hafas.de/bin/mgate.exe', endpoint: 'https://fahrinfo.vbb.de/bin/mgate.exe',
// https://gist.github.com/derhuerst/a8d94a433358abc015ff77df4481070c#file-haf_config_base-properties-L39
// https://runkit.com/derhuerst/hafas-decrypt-encrypted-mac-salt
salt: Buffer.from('5243544a4d3266467846667878516649', 'hex'),
addMicMac: true,
transformReqBody, transformReqBody,
products: modes.allProducts, products: modes.allProducts,

View file

@ -175,6 +175,7 @@ The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript
- [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas#vbb-hafas) JavaScript client for Berlin & Brandenburg public transport HAFAS API. - [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas#vbb-hafas) JavaScript client for Berlin & Brandenburg public transport HAFAS API.
- [`hafas-departures-in-direction`](https://github.com/derhuerst/hafas-departures-in-direction#hafas-departures-in-direction)  Pass in a HAFAS client, get departures in a certain direction. - [`hafas-departures-in-direction`](https://github.com/derhuerst/hafas-departures-in-direction#hafas-departures-in-direction)  Pass in a HAFAS client, get departures in a certain direction.
- [`hafas-collect-departures-at`](https://github.com/derhuerst/hafas-collect-departures-at#hafas-collect-departures-at)  Utility to collect departures, using any HAFAS client. - [`hafas-collect-departures-at`](https://github.com/derhuerst/hafas-collect-departures-at#hafas-collect-departures-at)  Utility to collect departures, using any HAFAS client.
- [`hafas-discover-stations`](https://github.com/derhuerst/hafas-discover-stations#hafas-discover-stations) Pass in a HAFAS client, discover stations by querying departures.
- [`hafas-rest-api`](https://github.com/derhuerst/hafas-rest-api#hafas-rest-api) Expose a HAFAS client via an HTTP REST API. - [`hafas-rest-api`](https://github.com/derhuerst/hafas-rest-api#hafas-rest-api) Expose a HAFAS client via an HTTP REST API.
- [List of european long-distance transport operators, available API endpoints, GTFS feeds and client modules.](https://github.com/public-transport/european-transport-operators) - [List of european long-distance transport operators, available API endpoints, GTFS feeds and client modules.](https://github.com/public-transport/european-transport-operators)
- [Collection of european transport JavaScript modules.](https://github.com/public-transport/european-transport-modules) - [Collection of european transport JavaScript modules.](https://github.com/public-transport/european-transport-modules)

View file

@ -246,8 +246,9 @@ test('journey leg details', co(function* (t) {
test('journeys  station to address', co(function* (t) { test('journeys  station to address', co(function* (t) {
const journeys = yield client.journeys(spichernstr, { const journeys = yield client.journeys(spichernstr, {
type: 'location', address: 'Torfstraße 17', type: 'location',
latitude: 52.5416823, longitude: 13.3491223 address: 'Torfstr. 17, Berlin',
latitude: 52.541797, longitude: 13.350042
}, {results: 1, when}) }, {results: 1, when})
t.ok(Array.isArray(journeys)) t.ok(Array.isArray(journeys))
@ -261,9 +262,9 @@ test('journeys  station to address', co(function* (t) {
const dest = leg.destination const dest = leg.destination
assertValidAddress(t, dest) assertValidAddress(t, dest)
t.strictEqual(dest.address, 'Torfstraße 17') t.strictEqual(dest.address, '13353 Berlin-Wedding, Torfstr. 17')
t.ok(isRoughlyEqual(.0001, dest.latitude, 52.5416823)) t.ok(isRoughlyEqual(.0001, dest.latitude, 52.541797))
t.ok(isRoughlyEqual(.0001, dest.longitude, 13.3491223)) t.ok(isRoughlyEqual(.0001, dest.longitude, 13.350042))
assertValidWhen(t, leg.arrival, when) assertValidWhen(t, leg.arrival, when)
t.end() t.end()
@ -273,7 +274,9 @@ test('journeys  station to address', co(function* (t) {
test('journeys  station to POI', co(function* (t) { test('journeys  station to POI', co(function* (t) {
const journeys = yield client.journeys(spichernstr, { const journeys = yield client.journeys(spichernstr, {
type: 'location', id: '9980720', name: 'ATZE Musiktheater', type: 'location',
id: '900980720',
name: 'Berlin, Atze Musiktheater für Kinder',
latitude: 52.543333, longitude: 13.351686 latitude: 52.543333, longitude: 13.351686
}, {results: 1, when}) }, {results: 1, when})
@ -288,7 +291,8 @@ test('journeys  station to POI', co(function* (t) {
const dest = leg.destination const dest = leg.destination
assertValidPoi(t, dest) assertValidPoi(t, dest)
t.strictEqual(dest.name, 'ATZE Musiktheater') t.strictEqual(dest.id, '900980720')
t.strictEqual(dest.name, 'Berlin, Atze Musiktheater für Kinder')
t.ok(isRoughlyEqual(.0001, dest.latitude, 52.543333)) t.ok(isRoughlyEqual(.0001, dest.latitude, 52.543333))
t.ok(isRoughlyEqual(.0001, dest.longitude, 13.351686)) t.ok(isRoughlyEqual(.0001, dest.longitude, 13.351686))
assertValidWhen(t, leg.arrival, when) assertValidWhen(t, leg.arrival, when)