Skip to content

Instantly share code, notes, and snippets.

@derhuerst
Last active October 19, 2021 22:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save derhuerst/f60e49de1190c4983f02d01a18128c22 to your computer and use it in GitHub Desktop.
Save derhuerst/f60e49de1190c4983f02d01a18128c22 to your computer and use it in GitHub Desktop.
HAFAS-subscription-based cloud storage ✨

HAFAS's subscription feature has a flaw: It lets you store arbitrary data. Let's make a cloud storage API from this!

'use strict'
const {v4: uuidV4} = require('uuid')
const createStore = require('./3-fs')
;(async () => {
const storeId = process.env.STORE_ID || uuidV4()
console.log('creating HAFAS-backed blob store')
const store = await createStore(storeId)
console.log('storing 10 new random 100b blobs')
const blobs = new Array(10).fill().map(_ => randomBytes(100))
await Promise.all(blobs.map(store.create))
console.log('retrieving IDs of all stored blobs')
const blobIds = await store.list()
console.log('stored:', blobIds)
console.log('reading 1st blob')
console.log('1st blob:', await store.read(blobIds[0]))
console.log('deleting 1st blob')
await store.delete(blobIds[0])
console.log('removing store and all its data')
await store.destroy()
console.log('done!')
})()
.catch((err) => {
console.error(err)
process.exit(1)
})
'use strict'
const https = require('https')
const {ok} = require('assert')
const {randomBytes} = require('crypto')
const ENDPOINT = 'https://reiseauskunft.insa.de/bin/mgate.exe'
const AID = process.env.HAFAS_AUTH_AID
if (!AID) {
console.error('missing HAFAS_AUTH_AID env var')
process.exit(1)
}
// generic HAFAS request helper function
const request = async (method, reqData) => {
const rawBody = await new Promise((resolve, reject) => {
const req = https.request(ENDPOINT, {method: 'POST'}, (res) => {
let body = ''
res.once('error', reject)
res.on('data', d => body += d)
res.once('end', () => resolve(body))
})
req.once('error', reject)
req.setHeader('content-type', 'application/json')
req.setHeader('accept', 'application/json')
const reqBody = JSON.stringify({
lang: 'en',
client: {
type: "IPH",
id: "NASA",
v: "4000200",
name: "nasaPROD"
},
ver: "1.44",
auth: {
type: "AID",
aid: AID,
},
svcReqL: [{
meth: method,
req: reqData,
}],
})
req.end(reqBody)
})
const resBody = JSON.parse(rawBody)
if (resBody.err !== 'OK') {
const err = new Error(resBody.errTxt)
err.res = resBody
throw err
}
const res = resBody.svcResL[0].res
if (res.result && res.result.resultCode !== 'OK') {
const err = new Error(res.result.internalError)
err.res = resBody
throw err
}
return res
}
const findTripReconCtx = async () => {
const res = await request('TripSearch', {
depLocL: [{type: 'S', lid: 'A=1@L=90053@'}],
arrLocL: [{type: 'S', lid: 'A=1@L=90072@'}],
numF: 1,
})
return res.outConL[0].recon.ctx
}
const ensureSubscriptionUser = async (userId) => {
await request('SubscrUserCreate', {userId})
}
const deleteSubscriptionUser = async (userId) => {
await request('SubscrUserDelete', {userId})
}
const listSubscriptions = async (userId) => {
const res = await request('SubscrSearch', {userId, onlySubIds: true})
if (!Array.isArray(res.idSubscrL)) return []
return res.idSubscrL.map(s => s.subscrId)
}
const createSubscription = async (userId, ctxRecon, data) => {
const formatDate = (d) => (
d.getFullYear()
+ ('0' + (d.getMonth() + 1)).slice(-2)
+ ('0' + d.getDate()).slice(-2)
)
const res = await request('SubscrCreate', {
userId,
conSubscr: {
ctxRecon,
serviceDays: {
beginDate: formatDate(new Date()),
endDate: formatDate(new Date(Date.now() + 24 * 60 * 60 * 1000)), // tomorrow
},
data,
},
})
return res.subscrId
}
const readSubscription = async (userId, subscrId) => {
const res = await request('SubscrDetails', {userId, subscrId})
return {
id: res.details.subscrId,
ctxRecon: res.details.conSubscr.ctxRecon,
data: res.details.conSubscr.data,
}
}
const deleteSubscription = async (userId, subscrId) => {
await request('SubscrDelete', {userId, subscrId})
}
const createStore = async (userId) => {
await ensureSubscriptionUser(userId)
const reconCtx = await findTripReconCtx()
const create = async (data) => {
ok(Buffer.isBuffer(data), 'data must be a buffer')
return await createSubscription(userId, reconCtx, data.toString('base64'))
}
const read = async (subId) => {
const sub = await readSubscription(userId, subId)
return Buffer.from(sub.data, 'base64')
}
const del = async (subId) => {
await deleteSubscription(userId, subId)
}
const list = async () => {
return await listSubscriptions(userId)
}
const destroy = async () => {
const subIds = await list()
await Promise.all(subIds.map(del))
await deleteSubscriptionUser(userId)
}
return {create, read, delete: del, list, destroy}
}
module.exports = createStore
{
"name": "hafas-fs",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Jannis R",
"license": "ISC",
"dependencies": {
"uuid": "^8.3.2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment