Home Reference Source

src/index.js

  1. import Encode from './DSBEncoding';
  2. import Decode from './DSBDecode';
  3. import percentage from 'percentage-calc';
  4.  
  5. /**
  6. * Main Library class
  7. */
  8. export default class DSB {
  9. /**
  10. *
  11. * @param {String|Number} username
  12. * @param {String|Number} password
  13. * @param {String} [cookies=""] If you already have session cookies, you can add them here.
  14. * @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.
  15. * @param {Axios} [axios=require('axios')] Pass your custom axios instance if you want.
  16. */
  17. constructor(
  18. username,
  19. password,
  20. cookies = '',
  21. cache = false,
  22. axios = require('axios')
  23. ) {
  24. /**
  25. * @private
  26. */
  27. this.username = username;
  28. /**
  29. * @private
  30. */
  31. this.password = password;
  32. /**
  33. * @private
  34. */
  35. this.axios = axios;
  36. /**
  37. * @private
  38. */
  39. this.urls = {
  40. login: 'https://mobile.dsbcontrol.de/dsbmobilepage.aspx',
  41. main: 'https://www.dsbmobile.de/',
  42. Data: 'http://www.dsbmobile.de/JsonHandlerWeb.ashx/GetData',
  43. default: 'https://www.dsbmobile.de/default.aspx',
  44. loginV1: `https://iphone.dsbcontrol.de/iPhoneService.svc/DSB/authid/${
  45. this.username
  46. }/${this.password}`,
  47. timetables:
  48. 'https://iphone.dsbcontrol.de/iPhoneService.svc/DSB/timetables/',
  49. news: 'https://iphone.dsbcontrol.de/iPhoneService.svc/DSB/news/'
  50. };
  51. /**
  52. * @private
  53. */
  54. this.cookies = cookies;
  55. /**
  56. * @private
  57. */
  58. this.axios.defaults.headers.common['User-Agent'] =
  59. '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';
  60. if (cache) {
  61. /**
  62. * @private
  63. */
  64. this.cache = new DSBSessionStorageManager(cache, this.cookies);
  65. }
  66. }
  67.  
  68. /**
  69. * @callback ProgressCallback
  70. * @param {Number} progress - A number between 0 and 100
  71. */
  72.  
  73. /**
  74. * Fetch data
  75. * @param {ProgressCallback} [progress]
  76. * @returns {Promise.<Object>}
  77. */
  78. async fetch(progress = () => {}) {
  79. const cookies = await this._getSession(progress);
  80. // Progress State: 3
  81. const response = await this.axios({
  82. method: 'POST',
  83. data: {
  84. req: {
  85. Data: Encode({
  86. UserId: '',
  87. UserPw: '',
  88. Abos: [],
  89. AppVersion: '2.3',
  90. Language: 'de',
  91. AppId: '',
  92. Device: 'WebApp',
  93. PushId: '',
  94. BundleId: 'de.heinekingmedia.inhouse.dsbmobile.web',
  95. Date: new Date(),
  96. LastUpdate: new Date(),
  97. OsVersion:
  98. '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'
  99. }),
  100. DataType: 1
  101. }
  102. },
  103. url: this.urls.Data,
  104. headers: {
  105. Bundle_ID: 'de.heinekingmedia.inhouse.dsbmobile.web',
  106. Referer: this.urls.main,
  107. Cookie: cookies,
  108. 'X-Requested-With': 'XMLHttpRequest'
  109. },
  110. onUploadProgress(e) {
  111. console.log(JSON.stringify(e));
  112. },
  113. onDownloadProgress(e) {
  114. console.log(JSON.stringify(e));
  115. }
  116. });
  117. if (!response.data.d) throw new Error('Invalid data.');
  118. progress(percentage.from(4, 5));
  119. const decoded = Decode(response.data.d);
  120. progress(percentage.from(5, 5));
  121. return decoded;
  122. }
  123.  
  124. /**
  125. * Fetch data from the original iphone api (Only news and timetables supported)
  126. * @param {ProgressCallback} [progress]
  127. * @returns {Promise.<Object>}
  128. */
  129. async fetchV1(progress = () => {}) {
  130. let currentProgress = 0;
  131. const loginV1Response = await this.axios({
  132. method: 'GET',
  133. url: this.urls.loginV1
  134. });
  135. if (loginV1Response.data === '00000000-0000-0000-0000-000000000000')
  136. throw new Error('Login failed.');
  137. const id = loginV1Response.data;
  138. currentProgress++;
  139. progress(percentage.from(currentProgress, 5));
  140. const data = await Promise.all([
  141. this.axios(this.urls.timetables + id).then(response => {
  142. currentProgress++;
  143. progress(percentage.from(currentProgress, 5));
  144. return Promise.resolve({ timetables: response.data });
  145. }),
  146. this.axios(this.urls.news + id).then(response => {
  147. currentProgress++;
  148. progress(percentage.from(currentProgress, 5));
  149. return Promise.resolve({ news: response.data });
  150. })
  151. ]);
  152. currentProgress++;
  153. progress(percentage.from(currentProgress, 5));
  154. let newData = {};
  155. for (let fragment of data) {
  156. for (let key in fragment) {
  157. if (fragment.hasOwnProperty(key)) {
  158. newData[key] = fragment[key];
  159. }
  160. }
  161. }
  162. currentProgress++;
  163. progress(percentage.from(currentProgress, 5));
  164. return newData;
  165. }
  166.  
  167. /**
  168. * Login with username and password
  169. * @param {String|Number} [username=this.username]
  170. * @param {String|Number} [password=this.password]
  171. * @returns {Promise.<String>}
  172. * @private
  173. */
  174. async _login(username = this.username, password = this.password) {
  175. const response = await this.axios({
  176. method: 'GET',
  177. url: this.urls.login,
  178. params: {
  179. user: username,
  180. password: password
  181. },
  182. validateStatus(status) {
  183. return status === 200 || status === 302;
  184. },
  185. maxRedirects: 0,
  186. onUploadProgress(e) {
  187. console.log(JSON.stringify(e));
  188. },
  189. onDownloadProgress(e) {
  190. console.log(JSON.stringify(e));
  191. }
  192. });
  193. if (!response.headers['set-cookie'])
  194. throw new Error('Login failed. Returned no cookies.');
  195. this.cookies = response.headers['set-cookie'].join('; ');
  196. return this.cookies;
  197. }
  198.  
  199. /**
  200. * Checks if dsb session cookie is valid
  201. * @param {String} [cookies=this.cookies]
  202. * @returns {Promise.<boolean>}
  203. * @private
  204. */
  205. async _checkCookies(cookies = this.cookies) {
  206. let returnValue = false;
  207. try {
  208. returnValue = !!(await this.axios({
  209. method: 'GET',
  210. url: this.urls.default,
  211. validateStatus(status) {
  212. return status === 200;
  213. },
  214. maxRedirects: 0,
  215. headers: {
  216. Cookie: cookies,
  217. 'Cache-Control': 'no-cache',
  218. Pragma: 'no-cache'
  219. }
  220. }));
  221. } catch (e) {
  222. return false;
  223. } finally {
  224. return returnValue;
  225. }
  226. }
  227.  
  228. /**
  229. * Get a valid session
  230. * @param {Function} [progress]
  231. * @returns {Promise.<String>}
  232. * @private
  233. */
  234. async _getSession(progress = () => {}) {
  235. let returnValue;
  236. try {
  237. const cookies = this.cookies
  238. ? this.cookies
  239. : await this.cache.get();
  240. progress(percentage.from(1, 5));
  241. if (await this._checkCookies(cookies)) {
  242. returnValue = cookies;
  243. progress(percentage.from(3, 5));
  244. } else {
  245. returnValue = await this._login();
  246. progress(percentage.from(2, 5));
  247. this.cache
  248. ? await this.cache.set(returnValue)
  249. : (this.cookies = returnValue);
  250. progress(percentage.from(3, 5));
  251. }
  252. } catch (e) {
  253. returnValue = await this._login();
  254. progress(percentage.from(2, 5));
  255. this.cache
  256. ? await this.cache.set(returnValue)
  257. : (this.cookies = returnValue);
  258. progress(percentage.from(3, 5));
  259. } finally {
  260. return returnValue;
  261. }
  262. }
  263.  
  264. /**
  265. * [Experimental] Try to get just the important data from the data you get back from fetch()
  266. * @param {String} method - The method name to search for (z.B tiles or timetable)
  267. * @param {Object} data - Data returned by fetch()
  268. * @returns {Object}
  269. */
  270. static findMethodInData(method, data) {
  271. for (let key in data) {
  272. if (!data.hasOwnProperty(key)) continue;
  273. if (key === 'MethodName') {
  274. if (data[key] === method) {
  275. if (
  276. typeof data['Root'] === 'object' &&
  277. Array.isArray(data['Root']['Childs'])
  278. ) {
  279. let transformData = [];
  280. for (let o of data['Root']['Childs']) {
  281. let newObject = {};
  282. newObject.title = o.Title;
  283. newObject.id = o.Id;
  284. newObject.date = o.Date;
  285. if (o['Childs'].length === 1) {
  286. newObject.url = o['Childs'][0]['Detail'];
  287. newObject.preview = o['Childs'][0]['Preview'];
  288. newObject.secondTitle = o['Childs'][0]['Title'];
  289. } else {
  290. newObject.objects = [];
  291. for (let objectOfArray of o['Childs']) {
  292. newObject.objects.push({
  293. id: objectOfArray.Id,
  294. url: objectOfArray.Detail,
  295. preview: objectOfArray.Preview,
  296. title: objectOfArray.Title,
  297. date: objectOfArray.Date
  298. });
  299. }
  300. }
  301. transformData.push(newObject);
  302. }
  303. return {
  304. method: method,
  305. data: transformData
  306. };
  307. }
  308. }
  309. }
  310. if (Array.isArray(data[key]) || typeof data[key] === 'object') {
  311. const find = DSB.findMethodInData(method, data[key]);
  312. if (find) return find;
  313. }
  314. }
  315. }
  316. }
  317.  
  318. class DSBSessionStorageManager {
  319. /**
  320. * Class to store the dsb session
  321. * @param [path=""]
  322. * @param [cookies=""]
  323. */
  324. constructor(path = '', cookies = '') {
  325. this.path = path;
  326. this.cookies = cookies;
  327. this.fs = DSBSessionStorageManager.isNode() ? require('fs') : undefined;
  328. }
  329.  
  330. /**
  331. * Retrieves session from cache.
  332. * @returns {Promise.<String>}
  333. */
  334. async get() {
  335. if (DSBSessionStorageManager.isNode()) {
  336. this.cookies = await new Promise((resolve, reject) => {
  337. this.fs.readFile(this.path, (err, data) => {
  338. if (err) return reject(err);
  339. if (typeof data !== 'string') {
  340. let value;
  341. try {
  342. value = data.toString();
  343. } catch (e) {
  344. return reject(e);
  345. } finally {
  346. return resolve(value);
  347. }
  348. } else {
  349. return resolve(data);
  350. }
  351. });
  352. });
  353. return this.cookies;
  354. } else {
  355. if (window.localStorage) {
  356. return window.localStorage.getItem('DSBSession');
  357. } else {
  358. return this.cookies;
  359. }
  360. }
  361. }
  362.  
  363. /**
  364. * Sets the session in the cache.
  365. * @param value
  366. * @returns {Promise.<void>}
  367. */
  368. async set(value = '') {
  369. this.cookies = value;
  370. if (DSBSessionStorageManager.isNode()) {
  371. try {
  372. await new Promise((resolve, reject) => {
  373. this.fs.writeFile(this.path, value, err => {
  374. if (err) return reject(err);
  375. return resolve();
  376. });
  377. });
  378. } catch (e) {}
  379. } else {
  380. if (window.localStorage) {
  381. return window.localStorage.setItem('DSBSession', value);
  382. }
  383. }
  384. }
  385.  
  386. /**
  387. * Checks if this module is running in a browser env or node env.
  388. * @returns {boolean}
  389. */
  390. static isNode() {
  391. return (
  392. Object.prototype.toString.call(global.process) ===
  393. '[object process]'
  394. );
  395. }
  396. }