Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ jobs:
pip install -e "./python[tests]"
pip install flake8 mypy build pytest-cov pip-audit
- name: Lint
run: cd python && python -m flake8 FlightRadar24 tests
run: cd python && python -m flake8 FlightRadarAPI FlightRadar24 tests
- name: Type check
run: cd python && python -m mypy FlightRadar24 --ignore-missing-imports
run: cd python && python -m mypy FlightRadarAPI --ignore-missing-imports
- name: Offline tests (PR gate)
run: cd python && pytest -m "not integration" --cov=FlightRadar24 --cov-report=term --cov-report=xml -v
run: cd python && pytest -m "not integration" --cov=FlightRadarAPI --cov-report=term --cov-report=xml -v
- name: Integration tests (live FR24)
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
uses: nick-fields/retry@v3
Expand All @@ -58,4 +58,4 @@ jobs:
run: |
python -m build ./python
pip install python/dist/*.whl --force-reinstall
python -c "from FlightRadar24 import FlightRadar24API; api = FlightRadar24API(); print('Install OK')"
python -c "from FlightRadarAPI import FlightRadar24API; api = FlightRadar24API(); print('Install OK')"
65 changes: 65 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Contributing to FlightRadarAPI

Thanks for your interest. This repo ships two SDKs in parallel — Python and
Node.js — that must stay behavior-aligned, so most non-trivial changes touch
both sides.

## Development setup

### Python
```bash
cd python
make dev-setup # creates venv, installs package + test extras + tooling
source venv/bin/activate
make test # runs offline + integration
make lint # flake8
make type-check # mypy
```

### Node.js
```bash
cd nodejs
make install
make test # mocha (all tiers)
make lint # eslint
make test-types # tsd
```

## Keeping Python and Node aligned

When you change behavior, change it in both SDKs in the same PR unless there
is a documented reason not to. Common targets that must stay in sync:

- Error taxonomy (`AirportNotFoundError`, `LoginError`, `CloudflareError`,
`FlightRadarError`).
- `RetryPolicy` semantics (which exceptions are transient, backoff math).
- Cloudflare detection rules.
- The public surface — `FlightRadar24API` methods, the `Countries` enum,
`FlightTrackerConfig` fields, and the `Entity` / `Airport` / `Flight`
attributes consumers depend on.

## Style

- Python: flake8 + mypy.
- Node: eslint + tsd.
- Comments must explain **why**, not **what**. The codebase has a few exemplars in
`request.py`/`request.js` — read those before adding new comments.

## Commits and PRs

- Use a descriptive title with a conventional-commits prefix (`fix:`, `feat:`,
`refactor:`, `docs:`, `ci:`, `test:`).
- For new endpoints or behavior tweaks, add a regression test alongside.

## Releases

Before publishing a new release, the version **must be bumped**. The version lives in two places:

- `python/FlightRadarAPI/__init__.py` (`__version__`)
- `nodejs/package.json` (`version`)

## Reporting bugs and asking questions

- Bugs: open a GitHub issue with the bug report template.
- Security: see [`SECURITY.md`](SECURITY.md). **Do not** report via GitHub
issues.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@ npm install flightradarapi
```

## Documentation
Explore the documentation of FlightRadarAPI package, for Python or NodeJS, through [this site](https://JeanExtreme002.github.io/FlightRadarAPI/).
Explore the docs of FlightRadarAPI package, for Python or NodeJS, through [FlightRadarAPI Documentation](https://JeanExtreme002.github.io/FlightRadarAPI/) page.

## Project resources
**Contributing**: [`CONTRIBUTING.md`](CONTRIBUTING.md)<br>
**Security policy**: [`SECURITY.md`](SECURITY.md)<br>

38 changes: 38 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Security Policy

## Supported Versions

Security fixes are applied to the latest minor release on PyPI and npm. Older
versions do not receive backports.

## Reporting a Vulnerability

**Do not open a public GitHub issue for security reports.**

Please email the maintainer with:

- A description of the issue and the affected component (Python SDK, Node SDK,
CI, or documentation).
- The minimum reproducer you have (a short script is ideal).
- The package version (`pip show FlightRadarAPI` / `npm ls flightradarapi`).
- The impact you believe the issue has.

## Scope

This project is an **unofficial SDK** that consumes endpoints from
flightradar24. The following are explicitly **out of scope** for security
reports:

- The FR24 site or upstream API itself — contact FR24 directly.
- TLS impersonation behavior (intentional; see `request.py` / `request.js`).
- The fact that the SDK can be blocked by Cloudflare under heavy use.
- Anything that requires the user to feed the SDK adversarial input *to their
own credentials or filesystem*.

## In Scope

- Code execution, injection, or filesystem traversal triggered by a normal
call surface.
- Credential leakage in logs, error messages, or exception payloads.
- Known-vulnerable transitive dependencies that the SDK actually exercises.
- Improper validation that turns a remote response into local code paths.
2 changes: 1 addition & 1 deletion docs/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pip install FlightRadarAPI
Start by importing the `FlightRadar24API` class and creating an instance of it:

```python
from FlightRadar24 import FlightRadar24API
from FlightRadarAPI import FlightRadar24API
fr_api = FlightRadar24API()
```

Expand Down
37 changes: 26 additions & 11 deletions nodejs/FlightRadar24/api.js → nodejs/FlightRadarAPI/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class FlightRadar24API {
*/
async getAirport(code, details = false) {
if (code.length < 3 || code.length > 4) {
throw new Error("The code '" + code + "' is invalid. It must be the IATA or ICAO of the airport.");
throw new TypeError("The code '" + code + "' is invalid. It must be the IATA or ICAO of the airport.");
}

if (details) {
Expand Down Expand Up @@ -138,7 +138,7 @@ class FlightRadar24API {
*/
async getAirportDetails(code, flightLimit = 100, page = 1) {
if (code.length < 3 || code.length > 4) {
throw new Error("The code '" + code + "' is invalid. It must be the IATA or ICAO of the airport.");
throw new TypeError("The code '" + code + "' is invalid. It must be the IATA or ICAO of the airport.");
}

const params = { "format": "json", "code": code, "limit": flightLimit, "page": page };
Expand All @@ -159,7 +159,7 @@ class FlightRadar24API {
const limit = errors?.["limit"];

if (limit !== undefined) {
throw new Error(limit["notBetween"]);
throw new RangeError(limit["notBetween"]);
}
throw new AirportNotFoundError("Could not find an airport by the code '" + code + "'.", errors);
}
Expand Down Expand Up @@ -198,9 +198,13 @@ class FlightRadar24API {
*/
async getAirports(countries) {
const airports = [];
// Use stateless requests for the fan-out so per-country `Set-Cookie`
// responses do not race onto the shared session jar.
await mapConcurrent(countries, this.maxWorkers, async (countryName) => {
const countryHref = Core.airportsDataUrl + "/" + countryName;
const { content } = await this.__client.request(countryHref, { headers: Core.htmlHeaders, timeout: this.timeout });
const { content } = await this.__client.requestStandalone(
countryHref, { headers: Core.htmlHeaders, timeout: this.timeout },
);
airports.push(...parseAirportsHtml(content, countryHref));
});
return airports;
Expand Down Expand Up @@ -250,30 +254,38 @@ class FlightRadar24API {
const lon = radians(longitude);

const approxEarthRadius = 6371;

// Distance from the centre to a corner of the bounding square.
const hypotenuseDistance = Math.sqrt(2 * (Math.pow(halfSideInKm, 2)));

// Diagonal bearings: 225° (SW corner, min lat/lon) and 45° (NE corner, max lat/lon).
const bearingSw = radians(225);
const bearingNe = radians(45);

// Destination-point formula along the SW bearing → south-west corner.
const latMin = Math.asin(
Math.sin(lat) * Math.cos(hypotenuseDistance / approxEarthRadius) +
Math.cos(lat) *
Math.sin(hypotenuseDistance / approxEarthRadius) *
Math.cos(225 * (Math.PI / 180)),
Math.cos(bearingSw),
);
const lonMin = lon + Math.atan2(
Math.sin(225 * (Math.PI / 180)) *
Math.sin(bearingSw) *
Math.sin(hypotenuseDistance / approxEarthRadius) *
Math.cos(lat),
Math.cos(hypotenuseDistance / approxEarthRadius) -
Math.sin(lat) * Math.sin(latMin),
);

// Same formula along the NE bearing → north-east corner.
const latMax = Math.asin(
Math.sin(lat) * Math.cos(hypotenuseDistance / approxEarthRadius) +
Math.cos(lat) *
Math.sin(hypotenuseDistance / approxEarthRadius) *
Math.cos(45 * (Math.PI / 180)),
Math.cos(bearingNe),
);
const lonMax = lon + Math.atan2(
Math.sin(45 * (Math.PI / 180)) *
Math.sin(bearingNe) *
Math.sin(hypotenuseDistance / approxEarthRadius) *
Math.cos(lat),
Math.cos(hypotenuseDistance / approxEarthRadius) -
Expand All @@ -296,7 +308,8 @@ class FlightRadar24API {
* @return {Promise<[object, string] | null>}
*/
async getCountryFlag(country) {
const flagUrl = Core.countryFlagUrl(country.toLowerCase().replaceAll(" ", "-"));
const slug = country.toLowerCase().replaceAll(" ", "-");
const flagUrl = Core.countryFlagUrl(slug);

const headers = { ...Core.imageHeaders };
delete headers["origin"];
Expand All @@ -321,7 +334,9 @@ class FlightRadar24API {
* @return {Promise<object>}
*/
async getFlightDetails(flight) {
const { content } = await this.__client.request(
// Stateless request so the concurrent fan-out in `getFlights(..., details=true)`
// doesn't interleave cookie writes on the shared session.
const { content } = await this.__client.requestStandalone(
Core.flightDataUrl(flight.id), { headers: Core.jsonHeaders, timeout: this.timeout },
);
return content;
Expand Down Expand Up @@ -399,7 +414,7 @@ class FlightRadar24API {
fileType = fileType.toLowerCase();

if (!["csv", "kml"].includes(fileType)) {
throw new Error("File type '" + fileType + "' is not supported. Only CSV and KML are supported.");
throw new TypeError("File type '" + fileType + "' is not supported. Only CSV and KML are supported.");
}

const headers = { ...Core.jsonHeaders, "accesstoken": this.getLoginData()["accessToken"] };
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Entity {
getDistanceFrom(entity) {
if (this.latitude == null || this.longitude == null ||
entity.latitude == null || entity.longitude == null) {
throw new Error("Cannot calculate distance: one or both entities have no position.");
throw new TypeError("Cannot calculate distance: one or both entities have no position.");
}

const lat1 = radians(this.latitude);
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ const { isNumeric } = require("./util");
const proxyHandler = {
set: function(target, key, value) {
if (!Object.prototype.hasOwnProperty.call(target, key)) {
throw new Error("Unknown option: '" + key + "'");
throw new RangeError("Unknown option: '" + key + "'");
}
if ((typeof value !== "number") && (!isNumeric(value))) {
throw new Error("Value must be a number. Got '" + value + "' for key '" + key + "'");
throw new TypeError("Value must be a number. Got '" + value + "' for key '" + key + "'");
}
target[key] = value.toString();
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export interface ImpersonateOptions {
export class APIClient {
constructor(options?: { impersonate?: ImpersonateOptions; retry?: RetryPolicy });
request(url: string, options?: object): Promise<{content: any; statusCode: number; cookies: Record<string, string>}>;
/**
* Make a stateless request that bypasses the shared cookie jar. Safe to
* call from concurrent fan-outs (e.g. `getAirports`).
*/
requestStandalone(url: string, options?: object): Promise<{content: any; statusCode: number; cookies: Record<string, string>}>;
getCookie(name: string): string | undefined;
clearCookies(): void;
}
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ class Session {
}

/**
* Central HTTP client for the FlightRadar24 package.
* Central HTTP client for the FlightRadarAPI package.
*
* Owns the persistent session (cookie jar, TLS fingerprint, future bypass logic)
* so that the rest of the codebase never has to deal with those concerns directly.
Expand All @@ -333,8 +333,8 @@ class APIClient {
* (`{ciphers, sigalgs, ecdhCurve}`). Falls back to the bundled Chrome 136 profile.
*/
constructor({ impersonate = null, retry = null } = {}) {
const dispatcher = impersonate ? buildImpersonateAgent(impersonate) : defaultAgent;
this.__session = new Session({ dispatcher });
this.__dispatcher = impersonate ? buildImpersonateAgent(impersonate) : defaultAgent;
this.__session = new Session({ dispatcher: this.__dispatcher });
this.__retry = retry;
}

Expand All @@ -349,6 +349,24 @@ class APIClient {
return runWithRetry(() => this.__session.request(url, options), this.__retry);
}

/**
* Make a stateless request that does not touch the shared cookie jar.
*
* Safe to call from concurrent fan-outs (e.g. `getAirports` issuing one
* request per country). The TLS dispatcher is still reused so the
* impersonation profile stays consistent with the session.
*
* @param {string} url
* @param {object} [options={}]
* @return {Promise<{content: *, statusCode: number, cookies: object}>}
*/
async requestStandalone(url, options = {}) {
return runWithRetry(
() => request(url, { dispatcher: this.__dispatcher, ...options }),
this.__retry,
);
}

/**
* Return the value of a stored cookie by name.
*
Expand Down
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions nodejs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions nodejs/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "flightradarapi",
"version": "1.5.0",
"version": "1.5.1",
"description": "SDK for FlightRadar24",
"main": "./FlightRadar24/index.js",
"types": "./FlightRadar24/index.d.ts",
"main": "./FlightRadarAPI/index.js",
"types": "./FlightRadarAPI/index.d.ts",
"scripts": {
"test": "mocha tests --timeout 10000",
"test:offline": "mocha tests/testParsersOffline.js tests/testRequestPolicy.js tests/testRequestTransport.js --timeout 10000",
Expand Down
2 changes: 1 addition & 1 deletion nodejs/tests/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
Airport,
FlightTrackerConfig,
Zone,
} from "../FlightRadar24/index";
} from "../FlightRadarAPI/index";

const api = new FlightRadar24API();
expectType<FlightRadar24API>(new FlightRadar24API({timeout: 5000, maxWorkers: 4}));
Expand Down
2 changes: 1 addition & 1 deletion nodejs/tests/testParsersOffline.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const fs = require("fs");
const path = require("path");
const expect = require("chai").expect;

const { parseAirlinesHtml, parseAirportsHtml } = require("../FlightRadar24/parsers");
const { parseAirlinesHtml, parseAirportsHtml } = require("../FlightRadarAPI/parsers");

const FIXTURES = path.join(__dirname, "fixtures");
const load = (name) => fs.readFileSync(path.join(FIXTURES, name), "utf-8");
Expand Down
4 changes: 2 additions & 2 deletions nodejs/tests/testRequestPolicy.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
const { expect } = require("chai");
const { MockAgent } = require("undici");

const { request, RetryPolicy, APIClient } = require("../FlightRadar24/request");
const { CloudflareError } = require("../FlightRadar24/errors");
const { request, RetryPolicy, APIClient } = require("../FlightRadarAPI/request");
const { CloudflareError } = require("../FlightRadarAPI/errors");
const { clientWithFakeSession } = require("./_requestDoubles");


Expand Down
Loading
Loading