added all files to project

This commit is contained in:
2022-03-10 10:36:59 +01:00
parent 09dd957b33
commit 46a936d7de
25351 changed files with 3883356 additions and 0 deletions
@@ -0,0 +1,121 @@
'use strict'
const request = require('../lib/request')
const formatStationBoardReq = require('../format/station-board-req')
const formatLocationsReq = require('../format/locations-req')
const formatStopReq = require('../format/stop-req')
const formatNearbyReq = require('../format/nearby-req')
const formatTripReq = require('../format/trip-req')
const formatRadarReq = require('../format/radar-req')
const formatReachableFromReq = require('../format/reachable-from-req')
const formatRefreshJourneyReq = require('../format/refresh-journey-req')
const formatRemarksReq = require('../format/remarks-req')
const formatLinesReq = require('../format/lines-req')
const parseDateTime = require('../parse/date-time')
const parsePlatform = require('../parse/platform')
const parseProductsBitmask = require('../parse/products-bitmask')
const parseIcon = require('../parse/icon')
const parseWhen = require('../parse/when')
const parseDeparture = require('../parse/departure')
const parseArrival = require('../parse/arrival')
const parseTrip = require('../parse/trip')
const parseJourneyLeg = require('../parse/journey-leg')
const parseJourney = require('../parse/journey')
const parseLine = require('../parse/line')
const parseLocation = require('../parse/location')
const parseCommon = require('../parse/common')
const parsePolyline = require('../parse/polyline')
const parseMovement = require('../parse/movement')
const parseNearby = require('../parse/nearby')
const parseOperator = require('../parse/operator')
const parseHint = require('../parse/hint')
const parseWarning = require('../parse/warning')
const parseStopover = require('../parse/stopover')
const formatAddress = require('../format/address')
const formatCoord = require('../format/coord')
const formatDate = require('../format/date')
const formatLocationFilter = require('../format/location-filter')
const formatProductsFilter = require('../format/products-filter')
const formatPoi = require('../format/poi')
const formatStation = require('../format/station')
const formatTime = require('../format/time')
const formatLocation = require('../format/location')
const formatRectangle = require('../format/rectangle')
const filters = require('../format/filters')
const id = (ctx, x) => x
const defaultProfile = {
request,
transformReqBody: id,
transformReq: id,
salt: null,
addChecksum: false,
addMicMac: false,
formatStationBoardReq,
formatLocationsReq,
formatStopReq,
formatNearbyReq,
formatTripReq,
formatRadarReq,
formatReachableFromReq,
formatRefreshJourneyReq,
formatRemarksReq,
formatLinesReq,
transformJourneysQuery: id,
parseDateTime,
parsePlatform,
parseProductsBitmask,
parseIcon,
parseWhen,
parseDeparture,
parseArrival,
parseTrip,
parseJourneyLeg,
parseJourney,
parseLine,
parseStationName: (_, name) => name,
parseLocation,
parseCommon,
parsePolyline,
parseMovement,
parseNearby,
parseOperator,
parseHint,
parseWarning,
parseStopover,
formatAddress,
formatCoord,
formatDate,
formatLocationFilter,
formatProductsFilter,
formatPoi,
formatStation,
formatTime,
formatLocation,
formatRectangle,
filters,
journeysOutFrwd: true, // `journeys()` method: support for `outFrwd` field?
// todo: https://github.com/KDE/kpublictransport/commit/c7c54304160d8f22eab0c91812a107aca82304b7
departuresGetPasslist: true, // `departures()` method: support for `getPasslist` field?
departuresStbFltrEquiv: true, // `departures()` method: support for `stbFltrEquiv` field?
trip: false,
radar: false,
refreshJourney: true,
// refreshJourney(): use `outReconL[]` instead of `ctxRecon`?
refreshJourneyUseOutReconL: false,
tripsByName: true,
remarks: true,
// `remarks()` method: support for `getPolyline` field?
remarksGetPolyline: true, // `remarks()` method: support for `getPolyline` field?
lines: true,
}
module.exports = defaultProfile
@@ -0,0 +1,229 @@
'use strict'
const ACCESS_DENIED = 'ACCESS_DENIED'
const INVALID_REQUEST = 'INVALID_REQUEST'
const NOT_FOUND = 'NOT_FOUND'
const SERVER_ERROR = 'SERVER_ERROR'
// https://gist.github.com/derhuerst/79d49c0f04c1c192a5d15756e5af575f/edit
// todo:
// `code: 'METHOD_NA', message: 'HCI Service: service method disabled'`
// "err": "PARAMETER", "errTxt": "HCI Service: parameter invalid"
const byErrorCode = Object.assign(Object.create(null), {
H_UNKNOWN: {
isServer: false,
code: SERVER_ERROR,
message: 'unknown internal error',
statusCode: 500,
},
AUTH: {
isClient: true,
code: ACCESS_DENIED,
message: 'invalid or missing authentication data',
statusCode: 401
},
R0001: {
isClient: true,
code: INVALID_REQUEST,
message: 'unknown method',
statusCode: 400
},
R0002: {
isClient: true,
code: INVALID_REQUEST,
message: 'invalid or missing request parameters',
statusCode: 400
},
R0007: {
isServer: true,
code: SERVER_ERROR,
message: 'internal communication error',
statusCode: 500
},
R5000: {
isClient: true,
code: ACCESS_DENIED,
message: 'access denied',
statusCode: 401
},
S1: {
isServer: true,
code: SERVER_ERROR,
message: 'journeys search: a connection to the backend server couldn\'t be established',
statusCode: 503
},
LOCATION: {
isClient: true,
code: INVALID_REQUEST,
message: 'location/stop not found',
statusCode: 400
},
H390: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: departure/arrival station replaced',
statusCode: 400
},
H410: {
// todo: or is it a client error?
// todo: statusCode?
isServer: true,
code: SERVER_ERROR,
message: 'journeys search: incomplete response due to timetable change'
},
H455: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: prolonged stop',
statusCode: 400
},
H460: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: stop(s) passed multiple times',
statusCode: 400
},
H500: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: too many trains, connection is not complete',
statusCode: 400
},
H890: {
isClient: true,
code: NOT_FOUND,
message: 'journeys search unsuccessful',
statusCode: 404
},
H891: {
isClient: true,
code: NOT_FOUND,
message: 'journeys search: no route found, try with an intermediate stations',
statusCode: 404
},
H892: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: query too complex, try less intermediate stations',
statusCode: 400
},
H895: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: departure & arrival are too near',
statusCode: 400
},
H899: {
// todo: or is it a client error?
// todo: statusCode?
isServer: true,
code: SERVER_ERROR,
message: 'journeys search unsuccessful or incomplete due to timetable change'
},
H900: {
// todo: or is it a client error?
// todo: statusCode?
isServer: true,
code: SERVER_ERROR,
message: 'journeys search unsuccessful or incomplete due to timetable change'
},
H9220: {
isClient: true,
code: NOT_FOUND,
message: 'journeys search: no stations found close to the address',
statusCode: 400
},
H9230: {
isServer: true,
code: SERVER_ERROR,
message: 'journeys search: an internal error occured',
statusCode: 500
},
H9240: {
isClient: true,
code: NOT_FOUND,
message: 'journeys search unsuccessful',
statusCode: 404
},
H9250: {
isServer: true,
code: SERVER_ERROR,
message: 'journeys search: leg query interrupted',
statusCode: 500
},
H9260: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: unknown departure station',
statusCode: 400
},
H9280: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: unknown intermediate station',
statusCode: 400
},
H9300: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: unknown arrival station',
statusCode: 400
},
H9320: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: the input is incorrect or incomplete',
statusCode: 400
},
H9360: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: invalid date/time',
statusCode: 400
},
H9380: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: departure/arrival/intermediate station defined more than once',
statusCode: 400
},
SQ001: {
isServer: true,
code: SERVER_ERROR,
message: 'no departures/arrivals data available',
statusCode: 503
},
SQ005: {
isClient: true,
code: NOT_FOUND,
message: 'no trips found',
statusCode: 404
},
TI001: {
isServer: true,
code: SERVER_ERROR,
message: 'no trip info available',
statusCode: 503
}
})
const addErrorInfo = (err, errorCode, errorText, responseId) => {
if (byErrorCode[errorCode]) {
Object.assign(err, byErrorCode[errorCode])
if (errorCode) err.hafasErrorCode = errorCode
if (errorText) err.hafasErrorMessage = errorText
} else {
err.code = errorCode || null
err.message = errorText || errorCode || null
err.responseId = responseId || null
}
}
module.exports = {
ACCESS_DENIED,
INVALID_REQUEST,
NOT_FOUND,
SERVER_ERROR,
byErrorCode,
addErrorInfo,
}
@@ -0,0 +1,23 @@
'use strict'
const objectScan = require('object-scan')
const createFindInTree = (needles) => {
const scanner = objectScan(needles, {
filterFn: ({ value, parents, matchedBy, context }) => {
matchedBy.forEach((needle) => {
context[needle].push([value, parents])
})
}
})
return (haystack) => {
const context = Object.create(null)
needles.forEach((needle) => {
context[needle] = []
})
return scanner(haystack, context)
}
}
module.exports = createFindInTree
@@ -0,0 +1,25 @@
'use strict'
// For any type of "thing to parse", there's >=1 parse functions.
// By composing custom parse function(s) with the default ones, one
// can customize the behaviour of hafas-client. Profiles extensively
// use this mechanism.
// Each parse function has the following signature:
// ({opt, profile, common, res}, ...raw) => newParsed
// Compose a new/custom parse function with the old/existing parse
// function, so that the new fn will be called with the output of the
// old fn.
const parseHook = (oldParse, newParse) => {
return (ctx, ...args) => {
return newParse({
...ctx,
parsed: oldParse({...ctx, parsed: {}}, ...args)
}, ...args)
}
}
module.exports = {
parseHook
}
@@ -0,0 +1,158 @@
'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)
}
let getAgent = () => null
if (proxyAddress) {
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})
})
const pool = roundRobin(agents)
getAgent = () => pool.get()
}
const id = randomBytes(6).toString('hex')
const randomizeUserAgent = (userAgent) => {
const i = Math.round(Math.random() * userAgent.length)
return userAgent.slice(0, i) + id + userAgent.slice(i)
}
const md5 = input => createHash('md5').update(input).digest()
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
]))
if (DEBUG) console.error(JSON.stringify(body))
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)
},
redirect: 'follow',
query: {}
})
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
@@ -0,0 +1,47 @@
'use strict'
const findById = (needle) => {
const needleStopId = needle.id
const needleStationId = needle.station ? needle.station.id : null
return (stop) => {
if (needleStopId === stop.id) return true
const stationId = stop.station ? stop.station.id : null
if (needleStationId && stationId && needleStationId === stationId) return true
// todo: `needleStationId === stop.id`? `needleStopId === stationId`?
return false
}
}
const sliceLeg = (leg, from, to) => {
if (!Array.isArray(leg.stopovers)) throw new Error('leg.stopovers must be an array.')
const stops = leg.stopovers.map(st => st.stop)
const fromI = stops.findIndex(findById(from))
if (fromI === -1) throw new Error('from not found in stopovers')
const fromStopover = leg.stopovers[fromI]
const toI = stops.findIndex(findById(to))
if (toI === -1) throw new Error('to not found in stopovers')
const toStopover = leg.stopovers[toI]
if (fromI === 0 && toI === leg.stopovers.length - 1) return leg
const newLeg = Object.assign({}, leg)
newLeg.stopovers = leg.stopovers.slice(fromI, toI + 1)
newLeg.origin = fromStopover.stop
newLeg.departure = fromStopover.departure
newLeg.departureDelay = fromStopover.departureDelay
newLeg.scheduledDeparture = fromStopover.scheduledDeparture
newLeg.departurePlatform = fromStopover.departurePlatform
newLeg.destination = toStopover.stop
newLeg.arrival = toStopover.arrival
newLeg.arrivalDelay = toStopover.arrivalDelay
newLeg.scheduledArrival = toStopover.scheduledArrival
newLeg.arrivalPlatform = toStopover.arrivalPlatform
return newLeg
}
module.exports = sliceLeg
@@ -0,0 +1,95 @@
'use strict'
const types = {
locale: 'string',
timezone: 'string',
request: 'function',
transformReq: 'function',
transformReqBody: 'function',
formatStationBoardReq: 'function',
formatLocationsReq: 'function',
formatStopReq: 'function',
formatNearbyReq: 'function',
formatTripReq: 'function',
formatRadarReq: 'function',
formatReachableFromReq: 'function',
formatRefreshJourneyReq: 'function',
transformJourneysQuery: 'function',
products: 'array',
parseDateTime: 'function',
parseDeparture: 'function',
parseArrival: 'function',
parseJourneyLeg: 'function',
parseJourney: 'function',
parseLine: 'function',
parseStationName: 'function',
parseLocation: 'function',
parsePolyline: 'function',
parseMovement: 'function',
parseNearby: 'function',
parseOperator: 'function',
parseHint: 'function',
parseWarning: 'function',
parseStopover: 'function',
formatAddress: 'function',
formatCoord: 'function',
formatDate: 'function',
formatLocationFilter: 'function',
formatProductsFilter: 'function',
formatPoi: 'function',
formatStation: 'function',
formatTime: 'function',
formatLocation: 'function',
formatRectangle: 'function'
}
const validateProfile = (profile) => {
for (let key of Object.keys(types)) {
const type = types[key]
if (type === 'array') {
if (!Array.isArray(profile[key])) {
throw new TypeError(`profile.${key} must be an array.`)
}
} else if (type !== typeof profile[key]) {
throw new TypeError(`profile.${key} must be a ${type}.`)
}
if (type === 'object' && profile[key] === null) {
throw new TypeError(`profile.${key} must not be null.`)
}
}
if (!Array.isArray(profile.products)) {
throw new TypeError('profile.products must be an array.')
}
if (profile.products.length === 0) throw new Error('profile.products is empty.')
for (let product of profile.products) {
if ('string' !== typeof product.id) {
throw new TypeError('profile.products[].id must be a string.')
}
if ('boolean' !== typeof product.default) {
throw new TypeError('profile.products[].default must be a boolean.')
}
if (!Array.isArray(product.bitmasks)) {
throw new TypeError(product.id + '.bitmasks must be an array.')
}
for (let bitmask of product.bitmasks) {
if ('number' !== typeof bitmask) {
throw new TypeError(product.id + '.bitmasks[] must be a number.')
}
}
}
if ('trip' in profile && 'boolean' !== typeof profile.trip) {
throw new Error('profile.trip must be a boolean.')
}
if ('journeyLeg' in profile) {
throw new Error('profile.journeyLeg has been removed. Use profile.trip.')
}
}
module.exports = validateProfile