-
Notifications
You must be signed in to change notification settings - Fork 192
fix: bahn.de API 403 errors #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
alexanderroidl
wants to merge
6
commits into
BetterBahn:main
Choose a base branch
from
alexanderroidl:fix/bahn-api-403-errors
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
9f2e432
fix: bahn api 403 errors
alexanderroidl 2aa3ddc
chore: rebase lockfile
alexanderroidl f4c8bcc
fix: linting error
alexanderroidl 6eb65ed
fix: linting error
alexanderroidl 390968d
fix: linting errors
alexanderroidl ee13b1d
chore: re-install dependencies after rebase
alexanderroidl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| import { | ||
| ReconResponse, | ||
| reconResponseSchema, | ||
| VerbindungResponse, | ||
| VerbindungResponseSchema, | ||
| } from "@/utils/schemas"; | ||
| import { validateJson } from "@/utils/validateJson"; | ||
| import UserAgent from "user-agents"; | ||
| import { z, ZodType } from "zod/v4"; | ||
| import { Browser, HTTPResponse, Page } from "puppeteer"; | ||
| import puppeteer from "puppeteer-extra"; | ||
| import StealthPlugin from "puppeteer-extra-plugin-stealth"; | ||
|
|
||
| puppeteer.use(StealthPlugin()); | ||
|
|
||
| /** | ||
| * Sets a random user agent and viewport size for a Puppeteer page | ||
| * @param page - The Puppeteer page instance | ||
| */ | ||
| export const setRandomUserAgent = async (page: Page): Promise<UserAgent> => { | ||
| const randomUserAgent = new UserAgent(); | ||
| const { userAgent, platform, viewportWidth, viewportHeight } = | ||
| randomUserAgent.data; | ||
|
|
||
| await page.setUserAgent({ | ||
| userAgent, | ||
| platform, | ||
| }); | ||
| await page.setViewport({ | ||
| width: viewportWidth, | ||
| height: viewportHeight, | ||
| }); | ||
|
|
||
| return randomUserAgent; | ||
| }; | ||
|
|
||
| /** | ||
| * Sets up a new browser instance and page with random user agent. | ||
| * @returns A tuple containing the browser and page instances. | ||
| */ | ||
| export const setupBrowserAndPage = async (): Promise<[Browser, Page]> => { | ||
| const browser = await puppeteer.launch(); | ||
| const page = await browser.newPage(); | ||
| const randomUserAgent = await setRandomUserAgent(page); | ||
|
|
||
| console.log( | ||
| `🌐 Browser was setup with user agent "${randomUserAgent.data.userAgent}", platform "${randomUserAgent.data.platform}" and viewport ${randomUserAgent.data.viewportWidth}x${randomUserAgent.data.viewportHeight}.` | ||
| ); | ||
|
|
||
| return [browser, page]; | ||
| }; | ||
|
|
||
| /** | ||
| * Intercepts a specific bahn.de HTTP response and validates it against a schema. | ||
| * @param page - The Puppeteer page instance. | ||
| * @param schema - Zod schema to validate the response body against. | ||
| * @param pathname - The URL pathname to wait for. | ||
| * @param timeout - Timeout in milliseconds (default: 15000). | ||
| * @returns The validated response data. | ||
| */ | ||
| export const interceptResponse = <T extends ZodType>( | ||
| page: Page, | ||
| schema: T, | ||
| pathname: string, | ||
| timeout = 15000 | ||
| ): Promise<z.output<typeof schema>> => { | ||
| return new Promise((resolve, reject) => { | ||
| // Set up a timeout to avoid waiting indefinitely | ||
| const failTimer = setTimeout(() => { | ||
| // Clean up the event listener to prevent memory leaks | ||
| page.off("response", onPageResponse); | ||
|
|
||
| reject( | ||
| new Error( | ||
| `Timeout of ${timeout}ms exceeded while awaiting response at "${pathname}".` | ||
| ) | ||
| ); | ||
| }, timeout); | ||
|
|
||
| async function onPageResponse(response: HTTPResponse) { | ||
| const url = new URL(response.url()); | ||
| if (!url.host.includes("bahn.de") || url.pathname !== pathname) { | ||
| return; | ||
| } | ||
|
|
||
| // Clear the timeout and clean up the event listener | ||
| clearTimeout(failTimer); | ||
| page.off("response", onPageResponse); | ||
|
|
||
| try { | ||
| const json = await response.json(); | ||
| resolve(validateJson(schema, json)); | ||
| } catch (error) { | ||
| reject(error); | ||
| } | ||
| } | ||
|
|
||
| // We're registering a function so we can unsubscribe once we got what we want. | ||
| page.on("response", onPageResponse); | ||
| }); | ||
| }; | ||
|
|
||
| export const interceptReconResponse = async ( | ||
| page: Page | ||
| ): Promise<ReconResponse> => { | ||
| const reconResponse = await interceptResponse( | ||
| page, | ||
| reconResponseSchema, | ||
| "/web/api/angebote/recon" | ||
| ); | ||
| console.log(`🌐 Browser has intercepted Recon response.`); | ||
| return reconResponse; | ||
| }; | ||
|
|
||
| export const interceptVerbindungResponse = async ( | ||
| page: Page, | ||
| vbid: string | ||
| ): Promise<VerbindungResponse> => { | ||
| const verbindungResponse = await interceptResponse( | ||
| page, | ||
| VerbindungResponseSchema, | ||
| "/web/api/angebote/verbindung/" + vbid | ||
| ); | ||
| console.log(`🌐 Browser has intercepted Verbindung response.`); | ||
| return verbindungResponse; | ||
| }; | ||
|
|
||
| /** | ||
| * Gets Recon and Verbindung responses by navigating to a bahn.de booking URL with a browser. | ||
| * @param vbid - The journey ID to retrieve data for. | ||
| * @returns A tuple containing the recon and verbindungen responses. | ||
| */ | ||
| export const getReconAndVerbindungenBrowserResponses = async ( | ||
| vbid: string | ||
| ): Promise<[ReconResponse, VerbindungResponse]> => { | ||
| const urlToVisit = `https://www.bahn.de/buchung/start?vbid=${vbid}`; | ||
| const [browser, page] = await setupBrowserAndPage(); | ||
|
|
||
| console.log(`🌐 Browser is visiting "${urlToVisit}".`); | ||
|
|
||
| let reconResponse: ReconResponse; | ||
| let verbindungenResponse: VerbindungResponse; | ||
| try { | ||
| [reconResponse, verbindungenResponse] = await Promise.all([ | ||
| interceptReconResponse(page), | ||
| interceptVerbindungResponse(page, vbid), | ||
| page.goto(urlToVisit, { | ||
| waitUntil: "networkidle0", | ||
| }), | ||
| ]); | ||
| } catch (error) { | ||
| console.error(`❌ Error during browser operations:`, error); | ||
| throw error; | ||
| } finally { | ||
| // Ensure the browser is closed even if an error occurs | ||
| await browser.close(); | ||
| console.log(`🌐 Browser was closed.`); | ||
| } | ||
|
|
||
| return [reconResponse, verbindungenResponse]; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| /// <reference types="next" /> | ||
| /// <reference types="next/image-types/global" /> | ||
| /// <reference path="./.next/types/routes.d.ts" /> | ||
|
|
||
| // NOTE: This file should not be edited | ||
| // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ module.exports = { | |
| // Dies erstellt eine eigenständige Version der App mit allen Abhängigkeiten | ||
| output: "standalone", | ||
| typescript: { | ||
| ignoreBuildErrors: true // temporarily, since some type errors still exists and are ambiguous | ||
| } | ||
| ignoreBuildErrors: true, // temporarily, since some type errors still exists and are ambiguous | ||
| }, | ||
| serverExternalPackages: ["puppeteer-extra", "puppeteer-extra-plugin-stealth"], | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Those must be stated as external to avoid bundling. |
||
| }; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why Next.js automatically added this. 🤷
Somebody please double-check this for correctness.