src/index.js
import Encode from './DSBEncoding';
import Decode from './DSBDecode';
import percentage from 'percentage-calc';
/**
* Main Library class
*/
export default class DSB {
/**
*
* @param {String|Number} username
* @param {String|Number} password
* @param {String} [cookies=""] If you already have session cookies, you can add them here.
* @param {String|Boolean} [cache=false] In the browser just a boolean and in node a path string. If you don't want to use any cache just use undefined, null or false.
* @param {Axios} [axios=require('axios')] Pass your custom axios instance if you want.
*/
constructor(
username,
password,
cookies = '',
cache = false,
axios = require('axios')
) {
/**
* @private
*/
this.username = username;
/**
* @private
*/
this.password = password;
/**
* @private
*/
this.axios = axios;
/**
* @private
*/
this.urls = {
login: 'https://mobile.dsbcontrol.de/dsbmobilepage.aspx',
main: 'https://www.dsbmobile.de/',
Data: 'http://www.dsbmobile.de/JsonHandlerWeb.ashx/GetData',
default: 'https://www.dsbmobile.de/default.aspx',
loginV1: `https://iphone.dsbcontrol.de/iPhoneService.svc/DSB/authid/${
this.username
}/${this.password}`,
timetables:
'https://iphone.dsbcontrol.de/iPhoneService.svc/DSB/timetables/',
news: 'https://iphone.dsbcontrol.de/iPhoneService.svc/DSB/news/'
};
/**
* @private
*/
this.cookies = cookies;
/**
* @private
*/
this.axios.defaults.headers.common['User-Agent'] =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.32 Safari/537.36';
if (cache) {
/**
* @private
*/
this.cache = new DSBSessionStorageManager(cache, this.cookies);
}
}
/**
* @callback ProgressCallback
* @param {Number} progress - A number between 0 and 100
*/
/**
* Fetch data
* @param {ProgressCallback} [progress]
* @returns {Promise.<Object>}
*/
async fetch(progress = () => {}) {
const cookies = await this._getSession(progress);
// Progress State: 3
const response = await this.axios({
method: 'POST',
data: {
req: {
Data: Encode({
UserId: '',
UserPw: '',
Abos: [],
AppVersion: '2.3',
Language: 'de',
AppId: '',
Device: 'WebApp',
PushId: '',
BundleId: 'de.heinekingmedia.inhouse.dsbmobile.web',
Date: new Date(),
LastUpdate: new Date(),
OsVersion:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36'
}),
DataType: 1
}
},
url: this.urls.Data,
headers: {
Bundle_ID: 'de.heinekingmedia.inhouse.dsbmobile.web',
Referer: this.urls.main,
Cookie: cookies,
'X-Requested-With': 'XMLHttpRequest'
},
onUploadProgress(e) {
console.log(JSON.stringify(e));
},
onDownloadProgress(e) {
console.log(JSON.stringify(e));
}
});
if (!response.data.d) throw new Error('Invalid data.');
progress(percentage.from(4, 5));
const decoded = Decode(response.data.d);
progress(percentage.from(5, 5));
return decoded;
}
/**
* Fetch data from the original iphone api (Only news and timetables supported)
* @param {ProgressCallback} [progress]
* @returns {Promise.<Object>}
*/
async fetchV1(progress = () => {}) {
let currentProgress = 0;
const loginV1Response = await this.axios({
method: 'GET',
url: this.urls.loginV1
});
if (loginV1Response.data === '00000000-0000-0000-0000-000000000000')
throw new Error('Login failed.');
const id = loginV1Response.data;
currentProgress++;
progress(percentage.from(currentProgress, 5));
const data = await Promise.all([
this.axios(this.urls.timetables + id).then(response => {
currentProgress++;
progress(percentage.from(currentProgress, 5));
return Promise.resolve({ timetables: response.data });
}),
this.axios(this.urls.news + id).then(response => {
currentProgress++;
progress(percentage.from(currentProgress, 5));
return Promise.resolve({ news: response.data });
})
]);
currentProgress++;
progress(percentage.from(currentProgress, 5));
let newData = {};
for (let fragment of data) {
for (let key in fragment) {
if (fragment.hasOwnProperty(key)) {
newData[key] = fragment[key];
}
}
}
currentProgress++;
progress(percentage.from(currentProgress, 5));
return newData;
}
/**
* Login with username and password
* @param {String|Number} [username=this.username]
* @param {String|Number} [password=this.password]
* @returns {Promise.<String>}
* @private
*/
async _login(username = this.username, password = this.password) {
const response = await this.axios({
method: 'GET',
url: this.urls.login,
params: {
user: username,
password: password
},
validateStatus(status) {
return status === 200 || status === 302;
},
maxRedirects: 0,
onUploadProgress(e) {
console.log(JSON.stringify(e));
},
onDownloadProgress(e) {
console.log(JSON.stringify(e));
}
});
if (!response.headers['set-cookie'])
throw new Error('Login failed. Returned no cookies.');
this.cookies = response.headers['set-cookie'].join('; ');
return this.cookies;
}
/**
* Checks if dsb session cookie is valid
* @param {String} [cookies=this.cookies]
* @returns {Promise.<boolean>}
* @private
*/
async _checkCookies(cookies = this.cookies) {
let returnValue = false;
try {
returnValue = !!(await this.axios({
method: 'GET',
url: this.urls.default,
validateStatus(status) {
return status === 200;
},
maxRedirects: 0,
headers: {
Cookie: cookies,
'Cache-Control': 'no-cache',
Pragma: 'no-cache'
}
}));
} catch (e) {
return false;
} finally {
return returnValue;
}
}
/**
* Get a valid session
* @param {Function} [progress]
* @returns {Promise.<String>}
* @private
*/
async _getSession(progress = () => {}) {
let returnValue;
try {
const cookies = this.cookies
? this.cookies
: await this.cache.get();
progress(percentage.from(1, 5));
if (await this._checkCookies(cookies)) {
returnValue = cookies;
progress(percentage.from(3, 5));
} else {
returnValue = await this._login();
progress(percentage.from(2, 5));
this.cache
? await this.cache.set(returnValue)
: (this.cookies = returnValue);
progress(percentage.from(3, 5));
}
} catch (e) {
returnValue = await this._login();
progress(percentage.from(2, 5));
this.cache
? await this.cache.set(returnValue)
: (this.cookies = returnValue);
progress(percentage.from(3, 5));
} finally {
return returnValue;
}
}
/**
* [Experimental] Try to get just the important data from the data you get back from fetch()
* @param {String} method - The method name to search for (z.B tiles or timetable)
* @param {Object} data - Data returned by fetch()
* @returns {Object}
*/
static findMethodInData(method, data) {
for (let key in data) {
if (!data.hasOwnProperty(key)) continue;
if (key === 'MethodName') {
if (data[key] === method) {
if (
typeof data['Root'] === 'object' &&
Array.isArray(data['Root']['Childs'])
) {
let transformData = [];
for (let o of data['Root']['Childs']) {
let newObject = {};
newObject.title = o.Title;
newObject.id = o.Id;
newObject.date = o.Date;
if (o['Childs'].length === 1) {
newObject.url = o['Childs'][0]['Detail'];
newObject.preview = o['Childs'][0]['Preview'];
newObject.secondTitle = o['Childs'][0]['Title'];
} else {
newObject.objects = [];
for (let objectOfArray of o['Childs']) {
newObject.objects.push({
id: objectOfArray.Id,
url: objectOfArray.Detail,
preview: objectOfArray.Preview,
title: objectOfArray.Title,
date: objectOfArray.Date
});
}
}
transformData.push(newObject);
}
return {
method: method,
data: transformData
};
}
}
}
if (Array.isArray(data[key]) || typeof data[key] === 'object') {
const find = DSB.findMethodInData(method, data[key]);
if (find) return find;
}
}
}
}
class DSBSessionStorageManager {
/**
* Class to store the dsb session
* @param [path=""]
* @param [cookies=""]
*/
constructor(path = '', cookies = '') {
this.path = path;
this.cookies = cookies;
this.fs = DSBSessionStorageManager.isNode() ? require('fs') : undefined;
}
/**
* Retrieves session from cache.
* @returns {Promise.<String>}
*/
async get() {
if (DSBSessionStorageManager.isNode()) {
this.cookies = await new Promise((resolve, reject) => {
this.fs.readFile(this.path, (err, data) => {
if (err) return reject(err);
if (typeof data !== 'string') {
let value;
try {
value = data.toString();
} catch (e) {
return reject(e);
} finally {
return resolve(value);
}
} else {
return resolve(data);
}
});
});
return this.cookies;
} else {
if (window.localStorage) {
return window.localStorage.getItem('DSBSession');
} else {
return this.cookies;
}
}
}
/**
* Sets the session in the cache.
* @param value
* @returns {Promise.<void>}
*/
async set(value = '') {
this.cookies = value;
if (DSBSessionStorageManager.isNode()) {
try {
await new Promise((resolve, reject) => {
this.fs.writeFile(this.path, value, err => {
if (err) return reject(err);
return resolve();
});
});
} catch (e) {}
} else {
if (window.localStorage) {
return window.localStorage.setItem('DSBSession', value);
}
}
}
/**
* Checks if this module is running in a browser env or node env.
* @returns {boolean}
*/
static isNode() {
return (
Object.prototype.toString.call(global.process) ===
'[object process]'
);
}
}