diff --git a/.github/workflows/on_commit.yml b/.github/workflows/on_commit.yml new file mode 100644 index 0000000..2786694 --- /dev/null +++ b/.github/workflows/on_commit.yml @@ -0,0 +1,29 @@ +--- +name: CI + +on: + workflow_dispatch: + + pull_request: + push: + branches: [main] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - name: 🛎️ Checkout + uses: actions/checkout@v4 + + - name: ⚙️ Setup Bun + uses: oven-sh/setup-bun@v1 + + - name: 📦 Build + env: + NODE_ENV: production + FORCE_COLOR: 1 + run: | + bun install + make --output-sync --jobs fmt test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..940b884 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +**/*.mmdb +lib/**/*.d.*ts +lib/**/index.mjs diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..55aa560 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +.PHONY : all test +all : + +.PHONY : lib/maxmind +lib/maxmind : + @ $(MAKE) --directory=lib/maxmind + +.PHONY : fmt +fmt : + bun x prettier --write . + @ git diff-index --quiet HEAD + +.PHONY : test +test : + @ $(MAKE) --directory=lib/maxmind test + +.PHONY : distclean +distclean : + -rm -r node_modules + -@ $(MAKE) --directory=lib/maxmind clean diff --git a/lib/maxmind/Makefile b/lib/maxmind/Makefile new file mode 100644 index 0000000..692c0b6 --- /dev/null +++ b/lib/maxmind/Makefile @@ -0,0 +1,16 @@ +.PHONY : all test clean +all : test/GeoLite2-Country-Test.mmdb index.mjs + +index.mjs : index.mts + bun x tsc --pretty + bun x webpack + +test/GeoLite2-Country-Test.mmdb : + cd test && wget 'https://cdn.jsdelivr.net/gh/maxmind/MaxMind-DB/test-data/GeoLite2-Country-Test.mmdb' + +test : + cd test && bun x tsc + cd test && bun test + +clean : + - rm test/*.mmdb *.d.mts index.*js diff --git a/lib/maxmind/index.mts b/lib/maxmind/index.mts new file mode 100644 index 0000000..9d5de03 --- /dev/null +++ b/lib/maxmind/index.mts @@ -0,0 +1,8 @@ +import type { CountryResponse } from 'mmdb-lib'; +import { Reader } from 'mmdb-lib'; + +export async function init(response: Pick) { + const db = Buffer.from(await response.arrayBuffer()); + + return new Reader(db); +} diff --git a/lib/maxmind/test/maxmind.spec.ts b/lib/maxmind/test/maxmind.spec.ts new file mode 100644 index 0000000..f9dc531 --- /dev/null +++ b/lib/maxmind/test/maxmind.spec.ts @@ -0,0 +1,210 @@ +import { describe, expect, it } from 'bun:test'; +import fs from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +import * as maxmind from '../index.mjs'; + +describe('lib / maxmind', () => { + const arrayBuffer = fs + .readFile( + fileURLToPath(import.meta.resolve('./GeoLite2-Country-Test.mmdb')), + ) + .then(({ buffer }) => buffer as ArrayBuffer); + const mockDatabase = { arrayBuffer: () => arrayBuffer }; + + it('should resolve a country for IPv4', async () => { + const reader = await maxmind.init(mockDatabase); + + expect(reader.get('89.160.20.122')).toStrictEqual({ + continent: { + code: 'EU', + geoname_id: 6255148, + names: { + de: 'Europa', + en: 'Europe', + es: 'Europa', + fr: 'Europe', + ja: 'ヨーロッパ', + 'pt-BR': 'Europa', + ru: 'Европа', + 'zh-CN': '欧洲', + }, + }, + country: { + geoname_id: 2661886, + is_in_european_union: true, + iso_code: 'SE', + names: { + de: 'Schweden', + en: 'Sweden', + es: 'Suecia', + fr: 'Suède', + ja: 'スウェーデン王国', + 'pt-BR': 'Suécia', + ru: 'Швеция', + 'zh-CN': '瑞典', + }, + }, + registered_country: { + geoname_id: 2921044, + is_in_european_union: true, + iso_code: 'DE', + names: { + de: 'Deutschland', + en: 'Germany', + es: 'Alemania', + fr: 'Allemagne', + ja: 'ドイツ連邦共和国', + 'pt-BR': 'Alemanha', + ru: 'Германия', + 'zh-CN': '德国', + }, + }, + }); + + expect(reader.get('67.43.156.156')).toStrictEqual({ + continent: { + code: 'AS', + geoname_id: 6255147, + names: { + de: 'Asien', + en: 'Asia', + es: 'Asia', + fr: 'Asie', + ja: 'アジア', + 'pt-BR': 'Ásia', + ru: 'Азия', + 'zh-CN': '亚洲', + }, + }, + country: { + geoname_id: 1252634, + iso_code: 'BT', + names: { + de: 'Bhutan', + en: 'Bhutan', + es: 'Bután', + fr: 'Bhutan', + ja: 'ブータン王国', + 'pt-BR': 'Butão', + ru: 'Бутан', + 'zh-CN': '不丹', + }, + }, + registered_country: { + geoname_id: 798549, + is_in_european_union: true, + iso_code: 'RO', + names: { + de: 'Rumänien', + en: 'Romania', + es: 'Rumanía', + fr: 'Roumanie', + ja: 'ルーマニア', + 'pt-BR': 'Romênia', + ru: 'Румыния', + 'zh-CN': '罗马尼亚', + }, + }, + traits: { + is_anonymous_proxy: true, + }, + }); + }); + + it('should resolve a country for IPv6', async () => { + const reader = await maxmind.init(mockDatabase); + + expect(reader.get('2a02:fc40::1')).toStrictEqual({ + continent: { + code: 'EU', + geoname_id: 6255148, + names: { + de: 'Europa', + en: 'Europe', + es: 'Europa', + fr: 'Europe', + ja: 'ヨーロッパ', + 'pt-BR': 'Europa', + ru: 'Европа', + 'zh-CN': '欧洲', + }, + }, + country: { + geoname_id: 2623032, + is_in_european_union: true, + iso_code: 'DK', + names: { + de: 'Dänemark', + en: 'Denmark', + es: 'Dinamarca', + fr: 'Danemark', + ja: 'デンマーク王国', + 'pt-BR': 'Dinamarca', + ru: 'Дания', + 'zh-CN': '丹麦', + }, + }, + registered_country: { + geoname_id: 2623032, + is_in_european_union: true, + iso_code: 'DK', + names: { + de: 'Dänemark', + en: 'Denmark', + es: 'Dinamarca', + fr: 'Danemark', + ja: 'デンマーク王国', + 'pt-BR': 'Dinamarca', + ru: 'Дания', + 'zh-CN': '丹麦', + }, + }, + }); + + expect(reader.get('2a02:d300::1')).toStrictEqual({ + continent: { + code: 'EU', + geoname_id: 6255148, + names: { + de: 'Europa', + en: 'Europe', + es: 'Europa', + fr: 'Europe', + ja: 'ヨーロッパ', + 'pt-BR': 'Europa', + ru: 'Европа', + 'zh-CN': '欧洲', + }, + }, + country: { + geoname_id: 690791, + iso_code: 'UA', + names: { + de: 'Ukraine', + en: 'Ukraine', + es: 'Ucrania', + fr: 'Ukraine', + ja: 'ウクライナ共和国', + 'pt-BR': 'Ucrânia', + ru: 'Украина', + 'zh-CN': '乌克兰', + }, + }, + registered_country: { + geoname_id: 690791, + iso_code: 'UA', + names: { + de: 'Ukraine', + en: 'Ukraine', + es: 'Ucrania', + fr: 'Ukraine', + ja: 'ウクライナ共和国', + 'pt-BR': 'Ucrânia', + ru: 'Украина', + 'zh-CN': '乌克兰', + }, + }, + }); + }); +}); diff --git a/lib/maxmind/test/tsconfig.json b/lib/maxmind/test/tsconfig.json new file mode 100644 index 0000000..e1b4cde --- /dev/null +++ b/lib/maxmind/test/tsconfig.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + + "include": ["*.ts"], + + "compilerOptions": { + "target": "ESNext", + "module": "Preserve", + "skipLibCheck": true, + "noEmit": true, + + "strict": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/lib/maxmind/tsconfig.json b/lib/maxmind/tsconfig.json new file mode 100644 index 0000000..03b1762 --- /dev/null +++ b/lib/maxmind/tsconfig.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + + "include": ["*.mts", "webpack.config.mjs"], + + "compilerOptions": { + "target": "ESNext", + "module": "Preserve", + "types": [], + "declaration": true, + + "strict": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/lib/maxmind/webpack.config.mjs b/lib/maxmind/webpack.config.mjs new file mode 100644 index 0000000..54625d8 --- /dev/null +++ b/lib/maxmind/webpack.config.mjs @@ -0,0 +1,30 @@ +// @ts-check + +import { fileURLToPath } from 'node:url'; + +import webpack from 'webpack'; + +/** @type webpack.Configuration */ +const config = { + mode: 'none', + experiments: { outputModule: true }, + + entry: { index: fileURLToPath(import.meta.resolve('./index.mjs')) }, + output: { + path: fileURLToPath(import.meta.resolve('.')), + libraryTarget: 'module', + }, + + resolve: { + fallback: { + net: false, + }, + }, + plugins: [ + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + }), + ], +}; + +export default config; diff --git a/package.json b/package.json new file mode 100644 index 0000000..e1f8d11 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "private": true, + "name": "ctf", + "version": "0.1.0", + "author": "nilfalse.com", + "license": "MPL-2.0", + "type": "module", + "scripts": { + "postinstall": "make lib/maxmind" + }, + "dependencies": { + "buffer": "^6.0.3", + "mmdb-lib": "^2.1.1" + }, + "devDependencies": { + "@types/bun": "latest", + "prettier": "^3.3.1", + "webpack-cli": "^5.1.4" + }, + "peerDependencies": { + "typescript": "*" + }, + "prettier": { + "singleQuote": true + } +}