From ea9f5e82082904523cd963ceb5ba51bdaf3515d4 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Sun, 9 Jun 2024 21:47:57 +0200 Subject: [PATCH] lib/maxmind --- .github/workflows/on_commit.yml | 26 ++++ .gitignore | 4 + Makefile | 20 +++ assets/fonts/index.css | 4 +- lib/maxmind/Makefile | 16 +++ lib/maxmind/index.mts | 8 ++ lib/maxmind/test/maxmind.spec.ts | 210 +++++++++++++++++++++++++++++++ lib/maxmind/test/tsconfig.json | 15 +++ lib/maxmind/tsconfig.json | 15 +++ lib/maxmind/webpack.config.mjs | 28 +++++ package.json | 26 ++++ src/service_worker.ts | 1 + tsconfig.json | 31 +++++ 13 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/on_commit.yml create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 lib/maxmind/Makefile create mode 100644 lib/maxmind/index.mts create mode 100644 lib/maxmind/test/maxmind.spec.ts create mode 100644 lib/maxmind/test/tsconfig.json create mode 100644 lib/maxmind/tsconfig.json create mode 100644 lib/maxmind/webpack.config.mjs create mode 100644 package.json create mode 100644 src/service_worker.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/on_commit.yml b/.github/workflows/on_commit.yml new file mode 100644 index 00000000..abee08ca --- /dev/null +++ b/.github/workflows/on_commit.yml @@ -0,0 +1,26 @@ +--- +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 + run: | + bun install + make fmt test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9190329d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +lib/**/*.mmdb +lib/**/*.d.*ts +lib/**/index.mjs diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..55aa560e --- /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/assets/fonts/index.css b/assets/fonts/index.css index a820a247..720d89fe 100644 --- a/assets/fonts/index.css +++ b/assets/fonts/index.css @@ -2,5 +2,7 @@ font-family: 'Pacifico'; font-style: normal; font-weight: normal; - src: local('Pacifico Regular'), url('Pacifico-Regular.woff') format('woff'); + src: + local('Pacifico Regular'), + url('Pacifico-Regular.woff') format('woff'); } diff --git a/lib/maxmind/Makefile b/lib/maxmind/Makefile new file mode 100644 index 00000000..0337f79d --- /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 + ../../node_modules/.bin/tsc --pretty + ../../node_modules/.bin/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 && ../../../node_modules/.bin/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 00000000..9d5de03e --- /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 00000000..f9dc5317 --- /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 00000000..e1b4cde2 --- /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 00000000..3b3bfccd --- /dev/null +++ b/lib/maxmind/tsconfig.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + + "include": ["*.mts"], + + "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 00000000..51c1cce3 --- /dev/null +++ b/lib/maxmind/webpack.config.mjs @@ -0,0 +1,28 @@ +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 00000000..e1f8d115 --- /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 + } +} diff --git a/src/service_worker.ts b/src/service_worker.ts new file mode 100644 index 00000000..bebf19ed --- /dev/null +++ b/src/service_worker.ts @@ -0,0 +1 @@ +import * as maxmind from '../lib/maxmind/index.mjs'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..9af3b720 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + + "include": ["src/**/*"], + + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": true, + "noUnusedParameters": true, + "noPropertyAccessFromIndexSignature": true + } +}