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) {
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

View file

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

View file

@ -1,5 +1,15 @@
'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

View file

@ -101,41 +101,64 @@ const createClient = (profile, request = _request) => {
filters.push(profile.filters.accessibility[opt.accessibility])
}
const query = profile.transformJourneysQuery({
outDate: profile.formatDate(profile, opt.when),
outTime: profile.formatTime(profile, opt.when),
ctxScr: journeysRef,
numF: opt.results,
getPasslist: !!opt.passedStations,
maxChg: opt.transfers,
minChgTime: opt.transferTime,
depLocL: [from],
viaLocL: opt.via ? [{loc: opt.via}] : null,
arrLocL: [to],
jnyFltrL: filters,
getTariff: !!opt.tickets,
// With protocol version `1.16`, the VBB endpoint fails with
// `CGI_READ_FAILED` if you pass `numF`, the parameter for the number
// 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,
// numF: opt.results,
getPasslist: !!opt.passedStations,
maxChg: opt.transfers,
minChgTime: opt.transferTime,
depLocL: [from],
viaLocL: opt.via ? [{loc: opt.via}] : null,
arrLocL: [to],
jnyFltrL: filters,
getTariff: !!opt.tickets,
// todo: what is req.gisFltrL?
getPT: true, // todo: what is this?
outFrwd: true, // todo: what is this?
getIV: false, // todo: walk & bike as alternatives?
getPolyline: false // todo: shape for displaying on a map?
}, opt)
// todo: what is req.gisFltrL?
getPT: true, // todo: what is this?
outFrwd: true, // todo: what is this?
getIV: false, // todo: walk & bike as alternatives?
getPolyline: false // todo: shape for displaying on a map?
}
return request(profile, {
cfg: {polyEnc: 'GPA'},
meth: 'TripSearch',
req: query
})
.then((d) => {
if (!Array.isArray(d.outConL)) return []
const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks)
const res = d.outConL.map(parse)
return request(profile, {
cfg: {polyEnc: 'GPA'},
meth: 'TripSearch',
req: profile.transformJourneysQuery(query, opt)
})
.then((d) => {
if (!Array.isArray(d.outConL)) return []
const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks)
if (!journeys.earlierRef) journeys.earlierRef = d.outCtxScrB
if (d.outCtxScrB) res.earlierRef = d.outCtxScrB
if (d.outCtxScrF) res.laterRef = d.outCtxScrF
return res
})
let latestDep = -Infinity
for (let j of d.outConL) {
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 = {}) => {

View file

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

View file

@ -1,5 +1,6 @@
'use strict'
const crypto = require('crypto')
let captureStackTrace = () => {}
if (process.env.NODE_ENV === 'dev') {
captureStackTrace = require('capture-stack-trace')
@ -8,6 +9,8 @@ const {stringify} = require('query-string')
const Promise = require('pinkie-promise')
const {fetch} = require('fetch-ponyfill')({Promise})
const md5 = input => crypto.createHash('md5').update(input).digest()
const request = (profile, data) => {
const body = profile.transformReqBody({lang: 'en', svcReqL: [data]})
const req = profile.transformReq({
@ -19,14 +22,37 @@ const request = (profile, data) => {
'Accept-Encoding': 'gzip, deflate',
'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.
const err = new Error()
err.isHafasError = true
err.request = body
err.url = url
captureStackTrace(err)
return fetch(url, req)

View file

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

View file

@ -20,10 +20,10 @@ const modes = require('./modes')
const formatBitmask = createFormatBitmask(modes)
const transformReqBody = (body) => {
body.client = {type: 'IPA', id: 'BVG', name: 'FahrInfo', v: '4070700'}
body.ext = 'BVG.1'
body.ver = '1.15' // todo: 1.16 with `mic` and `mac` query params
body.auth = {type: 'AID', aid: '1Rxs112shyHLatUX4fofnmdxK'}
body.client = {type: 'IPA', id: 'VBB', name: 'vbbPROD', v: '4010300'}
body.ext = 'VBB.1'
body.ver = '1.16'
body.auth = {type: 'AID', aid: 'hafas-vbb-apps'}
return body
}
@ -167,7 +167,13 @@ const formatProducts = (products) => {
const vbbProfile = {
locale: 'de-DE',
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,
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.
- [`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-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.
- [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)

View file

@ -246,8 +246,9 @@ test('journey leg details', co(function* (t) {
test('journeys  station to address', co(function* (t) {
const journeys = yield client.journeys(spichernstr, {
type: 'location', address: 'Torfstraße 17',
latitude: 52.5416823, longitude: 13.3491223
type: 'location',
address: 'Torfstr. 17, Berlin',
latitude: 52.541797, longitude: 13.350042
}, {results: 1, when})
t.ok(Array.isArray(journeys))
@ -261,9 +262,9 @@ test('journeys  station to address', co(function* (t) {
const dest = leg.destination
assertValidAddress(t, dest)
t.strictEqual(dest.address, 'Torfstraße 17')
t.ok(isRoughlyEqual(.0001, dest.latitude, 52.5416823))
t.ok(isRoughlyEqual(.0001, dest.longitude, 13.3491223))
t.strictEqual(dest.address, '13353 Berlin-Wedding, Torfstr. 17')
t.ok(isRoughlyEqual(.0001, dest.latitude, 52.541797))
t.ok(isRoughlyEqual(.0001, dest.longitude, 13.350042))
assertValidWhen(t, leg.arrival, when)
t.end()
@ -273,7 +274,9 @@ test('journeys  station to address', co(function* (t) {
test('journeys  station to POI', co(function* (t) {
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
}, {results: 1, when})
@ -288,7 +291,8 @@ test('journeys  station to POI', co(function* (t) {
const dest = leg.destination
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.longitude, 13.351686))
assertValidWhen(t, leg.arrival, when)