├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── dependabot-auto-merge.yml │ └── wrangler.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── assets │ ├── apple-touch-icon.png │ ├── favicon.svg │ ├── style.css │ └── template.liquid ├── handler.ts ├── index.ts ├── structs │ ├── api-response.ts │ ├── info-data.ts │ └── response-type.ts └── utils │ ├── api-check.ts │ ├── error-response.ts │ ├── html-response.ts │ ├── json-response.ts │ ├── liquid.ts │ ├── mask-ip-filter.ts │ ├── pick-filter.ts │ ├── random-string.ts │ ├── static-router.ts │ ├── text-agents.ts │ ├── text-response.ts │ └── unique-filter.ts ├── tsconfig.json ├── types ├── custom.d.ts └── index.d.ts └── wrangler.toml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: 2 | - "https://github.com/rensatsu/rensatsu/blob/master/Donate.md" 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | open-pull-requests-limit: 1 11 | schedule: 12 | interval: "monthly" 13 | time: "11:00" 14 | ignore: 15 | - dependency-name: "ua-parser-js" 16 | versions: 17 | - ">= 2.0.0" 18 | groups: 19 | dependencies: 20 | patterns: 21 | - "*" 22 | 23 | - package-ecosystem: "github-actions" 24 | directory: "/" 25 | open-pull-requests-limit: 1 26 | schedule: 27 | interval: "monthly" 28 | time: "11:00" 29 | groups: 30 | dependencies: 31 | patterns: 32 | - "*" 33 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Auto Merge 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | automerge: 8 | name: Automerge 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | pull-requests: write 13 | contents: write 14 | 15 | steps: 16 | - uses: fastify/github-action-merge-dependabot@v3.11.1 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | target: minor 20 | -------------------------------------------------------------------------------- /.github/workflows/wrangler.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | paths-ignore: 9 | - ".github/**" 10 | - ".vscode/**" 11 | - "LICENSE" 12 | - "README.md" 13 | 14 | jobs: 15 | deploy: 16 | runs-on: ubuntu-22.04 17 | name: Deploy 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@master 21 | 22 | - name: Prepare node 23 | uses: actions/setup-node@v4.4.0 24 | with: 25 | node-version: "18" 26 | 27 | - name: Install node packages 28 | run: npm ci --ignore-scripts 29 | 30 | - name: Publish 31 | uses: cloudflare/wrangler-action@v3.14.1 32 | with: 33 | apiToken: ${{ secrets.CF_API_TOKEN }} 34 | accountId: ${{ secrets.CF_ACCOUNT_ID }} 35 | command: deploy 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | transpiled 4 | worker -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | transpiled/ 3 | worker/ 4 | node_modules/ 5 | .github/ 6 | package.json 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "semi": true, 4 | "trailingComma": "all", 5 | "tabWidth": 2, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["neilding.language-liquid"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Pavel Tvaladze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # My IP Cloudflare Worker 2 | 3 | Cloudflare Worker which shows your IP and some details about your ISP. 4 | 5 | ## Environment variables 6 | 7 | - `TEXT_API_ENABLED` - Set to `1` to allow text mode (return only an IP). 8 | 9 | ## Formatting 10 | 11 | This project uses [`prettier`][prettier] to format the project. To invoke, run `npm run format`. 12 | 13 | ## Previewing and Publishing 14 | 15 | For information on how to preview and publish your worker, please see the [Wrangler docs][wrangler-publish]. 16 | 17 | [prettier]: https://prettier.io/ 18 | [wrangler-publish]: https://developers.cloudflare.com/workers/tooling/wrangler/commands/#publish 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-ip-worker", 3 | "version": "1.0.0", 4 | "description": "Cloudflare Worker which shows your IP and some details about your ISP", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "NODE_ENV=development wrangler dev --local", 8 | "deploy": "wrangler publish", 9 | "build": "esbuild --bundle --sourcemap --outdir=dist --format=esm --target=esnext ./src/index.ts --loader:.css=text --loader:.liquid=text --loader:.png=binary --loader:.svg=text", 10 | "format": "prettier --write '**/*.{ts,js,css,json,md}'" 11 | }, 12 | "author": "Rensatsu", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@cloudflare/workers-types": "^4.20250224.0", 16 | "@miniflare/tre": "^3.0.0-next.14", 17 | "@types/country-list": "^2.1.4", 18 | "@types/ms": "^2.1.0", 19 | "@types/ua-parser-js": "^0.7.39", 20 | "arraybuffer-to-string": "^1.0.2", 21 | "country-list": "^2.3.0", 22 | "esbuild": "^0.25.0", 23 | "git-revision-webpack-plugin": "^5.0.0", 24 | "http-status-codes": "^2.3.0", 25 | "liquidjs": "^10.21.0", 26 | "ms": "^2.1.3", 27 | "prettier": "^3.5.2", 28 | "ts-loader": "^9.4.2", 29 | "typescript": "^5.8.2", 30 | "ua-parser-js": "^1.0.40", 31 | "wrangler": "^3.111.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rensatsu/my-ip-worker/c664c65c8a0fbb6975735449f2cce49ff29d7281/src/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-text: hsl(200deg 0% 25%); 3 | --color-text-muted: hsl(200deg 0% 65%); 4 | --color-background: hsl(200deg 0% 97%); 5 | --color-link: hsl(230deg 50% 50%); 6 | --color-link-hover: hsl(20deg 100% 45%); 7 | } 8 | 9 | @media (prefers-color-scheme: dark) { 10 | :root { 11 | --color-text: hsl(0deg 0% 90%); 12 | --color-text-muted: hsl(0deg 0% 40%); 13 | --color-background: hsl(0deg 0% 20%); 14 | --color-link: hsl(230deg 50% 70%); 15 | --color-link-hover: hsl(20deg 100% 55%); 16 | } 17 | } 18 | 19 | * { 20 | font-family: system-ui, sans-serif; 21 | } 22 | 23 | body { 24 | margin: 2rem; 25 | max-width: 100ch; 26 | line-height: 1.5; 27 | color: var(--color-text); 28 | background-color: var(--color-background); 29 | } 30 | 31 | a { 32 | color: var(--color-link); 33 | } 34 | 35 | a:hover { 36 | color: var(--color-link-hover); 37 | } 38 | 39 | dl { 40 | display: grid; 41 | grid-template-columns: auto; 42 | grid-gap: 0.5rem 1.5rem; 43 | } 44 | 45 | dt { 46 | grid-column-start: 1; 47 | font-weight: bold; 48 | } 49 | 50 | dd { 51 | grid-column-start: 1; 52 | margin: 0; 53 | } 54 | 55 | @media (min-width: 500px) { 56 | body { 57 | margin: 3rem; 58 | } 59 | 60 | dl { 61 | grid-template-columns: minmax(15ch, max-content) auto; 62 | } 63 | 64 | dd { 65 | grid-column-start: 2; 66 | } 67 | } 68 | 69 | footer { 70 | border-top: 1px solid var(--color-text-muted); 71 | } 72 | 73 | footer > .debug { 74 | color: var(--color-text-muted); 75 | } 76 | -------------------------------------------------------------------------------- /src/assets/template.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IP Information 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

IP Information

14 | 15 |
16 |
IP
17 |
{{ infodata.ip | maskIp }}
18 | 19 | {% if infodata.country or infodata.region or infodata.city %} 20 |
Location
21 |
22 | {% if infodata.country %} 23 |
{{ infodata.country }}
24 | {% endif %} 25 | {% if infodata.region or infodata.city %} 26 | {% assign location = infodata | pick: "region", "city" | unique | join: ", " -%} 27 |
{{ location | escape }}
28 | {% endif %} 29 |
30 | {% endif %} 31 | 32 |
ASN
33 |
{{ infodata.asn }}
34 | 35 | {% if infodata.isp %} 36 |
ISP
37 |
{{ infodata.isp | escape }}
38 | {% endif %} 39 | 40 | {% if infodata.browser %} 41 |
Browser
42 |
{{ infodata.browser.name | append: " " | append: infodata.browser.major | escape }}
43 | {% endif %} 44 | 45 |
User-Agent
46 |
{{ infodata.userAgent | escape }}
47 |
48 | 49 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/handler.ts: -------------------------------------------------------------------------------- 1 | declare const ASNCACHE: KVNamespace; 2 | 3 | import errorResponse from "./utils/error-response"; 4 | import { Infodata } from "./structs/info-data"; 5 | import ResponseType from "./structs/response-type"; 6 | import textResponse from "./utils/text-response"; 7 | import jsonResponse from "./utils/json-response"; 8 | import htmlResponse from "./utils/html-response"; 9 | import { StatusCodes } from "http-status-codes"; 10 | import textAgents from "./utils/text-agents"; 11 | import staticRouter from "./utils/static-router"; 12 | import ms from "ms"; 13 | import UAParser from "ua-parser-js"; 14 | import { canUseApi } from "./utils/api-check"; 15 | 16 | /** 17 | * Collect user info data 18 | * @param {Request} request Incoming request 19 | */ 20 | async function getData(request: Request): Promise { 21 | const asn = request.cf?.asn ?? null; 22 | const isp = request.cf?.asOrganization ?? null; 23 | const userAgent = request.headers?.get("user-agent"); 24 | 25 | const uaParser = new UAParser(userAgent ?? ""); 26 | 27 | const infoData = new Infodata({ 28 | ip: request.headers?.get("cf-connecting-ip"), 29 | countryCode: request.cf?.country ?? null, 30 | region: request.cf?.region ?? null, 31 | city: request.cf?.city ?? null, 32 | asn: asn, 33 | isp: isp, 34 | userAgent: userAgent, 35 | browser: uaParser.getBrowser(), 36 | }); 37 | 38 | if (asn !== null) { 39 | await ASNCACHE.put( 40 | `as${asn}`, 41 | JSON.stringify({ 42 | name: isp, 43 | country: request.cf?.country, 44 | }), 45 | { 46 | expirationTtl: ms("1y") / 1000, 47 | }, 48 | ); 49 | } 50 | 51 | return infoData; 52 | } 53 | 54 | /** 55 | * Detect data request type 56 | * @param {Request} request Incoming request 57 | */ 58 | function detectType(request: Request): ResponseType { 59 | const hAccept = request.headers?.get("accept") ?? ""; 60 | 61 | // Return JSON if requested JSON. 62 | if (hAccept.includes("json")) { 63 | return ResponseType.JSON; 64 | } 65 | 66 | // Return text if requested text-only. 67 | if (hAccept.includes("plain")) { 68 | return ResponseType.TEXT; 69 | } 70 | 71 | // Return plain text for CLI tools. 72 | const userAgent = (request.headers?.get("user-agent") || "").toLowerCase(); 73 | if (textAgents.filter((e) => userAgent.includes(e)).length > 0) { 74 | return ResponseType.TEXT; 75 | } 76 | 77 | // Return HTML otherwise. 78 | return ResponseType.HTML; 79 | } 80 | 81 | /** 82 | * Check if API is enabled and determine data request type 83 | * @param {Request} request Incoming request 84 | */ 85 | function checkType( 86 | request: Request, 87 | forceType: ResponseType | null = null, 88 | ): ResponseType { 89 | if (canUseApi()) return ResponseType.HTML; 90 | return forceType ?? detectType(request); 91 | } 92 | 93 | /** 94 | * Handle IP data request 95 | * @param {Request} request Incoming request 96 | * @param {ResponseType} forceType Force response type 97 | */ 98 | async function handleIpData( 99 | request: Request, 100 | forceType: ResponseType | null = null, 101 | ): Promise { 102 | const data = await getData(request); 103 | const type = checkType(request, forceType); 104 | 105 | switch (type) { 106 | case ResponseType.TEXT: 107 | return textResponse(data); 108 | case ResponseType.JSON: 109 | return jsonResponse(data); 110 | case ResponseType.HTML: 111 | default: 112 | return await htmlResponse(data); 113 | } 114 | } 115 | 116 | /** 117 | * Respond to the request 118 | * @param {Request} request Incoming request 119 | */ 120 | async function handleRequest(request: Request): Promise { 121 | const url = new URL(request.url); 122 | const method = request.method.toLowerCase(); 123 | 124 | if (method !== "get") { 125 | return errorResponse("Bad request", StatusCodes.BAD_REQUEST); 126 | } 127 | 128 | switch (url.pathname) { 129 | case "/favicon.ico": 130 | case "/assets/style.css": 131 | case "/assets/apple-touch-icon.png": 132 | return staticRouter(url.pathname); 133 | case "/ip": 134 | return await handleIpData(request, ResponseType.TEXT); 135 | case "/json": 136 | return await handleIpData(request, ResponseType.JSON); 137 | case "/": 138 | return await handleIpData(request); 139 | default: 140 | return errorResponse("Not found", StatusCodes.NOT_FOUND); 141 | } 142 | } 143 | 144 | export { handleRequest }; 145 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { handleRequest } from "./handler"; 2 | 3 | addEventListener("fetch", (event) => { 4 | event.respondWith(handleRequest(event.request)); 5 | }); 6 | -------------------------------------------------------------------------------- /src/structs/api-response.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from "http-status-codes"; 2 | 3 | /** 4 | * Wrapper for Response to serve formatted JSON. 5 | * 6 | * @class ApiResponse 7 | * @extends {Response} 8 | */ 9 | class ApiResponse extends Response { 10 | /** 11 | * Creates an instance of ApiResponse. 12 | * @param {*} [body] JSON serializable body 13 | * @param {(ResponseInit | undefined)} [init] Init options 14 | * @memberof ApiResponse 15 | */ 16 | constructor(body?: any, init?: ResponseInit | undefined) { 17 | const headers = { 18 | "cache-control": "no-store", 19 | "x-content-type-options": "nosniff", 20 | "content-type": "application/json", 21 | }; 22 | 23 | const modInit = { 24 | ...{ status: StatusCodes.OK }, 25 | ...init, 26 | ...headers, 27 | } as ResponseInit; 28 | 29 | super(JSON.stringify(body, null, 2), modInit); 30 | } 31 | } 32 | 33 | export default ApiResponse; 34 | -------------------------------------------------------------------------------- /src/structs/info-data.ts: -------------------------------------------------------------------------------- 1 | import { getName } from "country-list"; 2 | import UAParser from "ua-parser-js"; 3 | 4 | /** 5 | * IP Infodata interface. 6 | * 7 | * @interface InfodataInterface 8 | */ 9 | interface InfodataInterface { 10 | ip?: string | null; 11 | country?: string | null; 12 | countryCode?: string | null; 13 | region?: string | null; 14 | city?: string | null; 15 | asn?: number | null; 16 | isp?: string | null; 17 | userAgent?: string | null; 18 | browser?: UAParser.IBrowser; 19 | } 20 | 21 | /** 22 | * IP Infodata Class. 23 | * 24 | * @class Infodata 25 | */ 26 | class Infodata { 27 | private data: InfodataInterface; 28 | 29 | /** 30 | * Creates an instance of Infodata. 31 | * @param {InfodataInterface} data 32 | * @memberof Infodata 33 | */ 34 | constructor(data: InfodataInterface) { 35 | this.data = data; 36 | 37 | if (data.countryCode?.length === 2) { 38 | this.data.country = getName(data.countryCode.toUpperCase()); 39 | } 40 | } 41 | 42 | get ip() { 43 | return this.data.ip; 44 | } 45 | 46 | get country() { 47 | return this.data.country; 48 | } 49 | 50 | get countryCode() { 51 | return this.data.countryCode; 52 | } 53 | 54 | get region() { 55 | return this.data.region; 56 | } 57 | 58 | get city() { 59 | return this.data.city; 60 | } 61 | 62 | get asn() { 63 | return this.data.asn; 64 | } 65 | 66 | get isp() { 67 | return this.data.isp; 68 | } 69 | 70 | get userAgent() { 71 | return this.data.userAgent; 72 | } 73 | 74 | get browser() { 75 | return this.data.browser; 76 | } 77 | 78 | /** 79 | * Return JSON data. 80 | * 81 | * @returns {object} 82 | * @memberof Infodata 83 | */ 84 | toJson(): object { 85 | return { 86 | ip: this.data.ip, 87 | country: this.data.country, 88 | countryCode: this.data.countryCode, 89 | region: this.data.region, 90 | city: this.data.city, 91 | asn: this.data.asn, 92 | isp: this.data.isp, 93 | userAgent: this.data.userAgent, 94 | browser: this.data.browser, 95 | }; 96 | } 97 | } 98 | 99 | export { Infodata, InfodataInterface }; 100 | -------------------------------------------------------------------------------- /src/structs/response-type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Available response types. 3 | * 4 | * @enum {number} 5 | */ 6 | enum ResponseType { 7 | JSON, 8 | HTML, 9 | TEXT, 10 | } 11 | 12 | export default ResponseType; 13 | -------------------------------------------------------------------------------- /src/utils/api-check.ts: -------------------------------------------------------------------------------- 1 | declare const TEXT_API_ENABLED: string; 2 | 3 | function canUseApi(): boolean { 4 | return typeof TEXT_API_ENABLED === undefined && TEXT_API_ENABLED !== "1"; 5 | } 6 | 7 | export { canUseApi }; 8 | -------------------------------------------------------------------------------- /src/utils/error-response.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from "http-status-codes"; 2 | 3 | /** 4 | * Create error response. 5 | * 6 | * @param {string} message Error message. 7 | * @param {number} [status=StatusCodes.INTERNAL_SERVER_ERROR] HTTP Status code. 8 | * @returns {Response} 9 | */ 10 | function errorResponse( 11 | message: string, 12 | status: number = StatusCodes.INTERNAL_SERVER_ERROR, 13 | ): Response { 14 | return new Response(message, { 15 | status, 16 | headers: { 17 | "cache-control": "no-store", 18 | "content-type": "text/plain; charset=utf-8", 19 | "x-content-type-options": "nosniff", 20 | }, 21 | }); 22 | } 23 | 24 | export default errorResponse; 25 | -------------------------------------------------------------------------------- /src/utils/html-response.ts: -------------------------------------------------------------------------------- 1 | import { Infodata } from "../structs/info-data"; 2 | import templateContents from "../assets/template.liquid"; 3 | import { StatusCodes } from "http-status-codes"; 4 | import { engine } from "./liquid"; 5 | 6 | /** 7 | * Create HTML response. 8 | * 9 | * @param {Infodata} data User info data. 10 | * @returns {Promise} 11 | */ 12 | async function htmlResponse(data: Infodata): Promise { 13 | const tpl = engine.parse(templateContents); 14 | 15 | const replacements = {} as Record; 16 | 17 | replacements.infodata = data.toJson(); 18 | replacements.timestamp = new Date(); 19 | 20 | const body = await engine.render(tpl, replacements); 21 | 22 | return new Response(body, { 23 | status: StatusCodes.OK, 24 | headers: { 25 | "cache-control": "no-store", 26 | "content-type": "text/html; charset=utf-8", 27 | "x-content-type-options": "nosniff", 28 | "content-security-policy": 29 | "default-src 'none'; img-src 'self'; style-src 'self'", 30 | }, 31 | }); 32 | } 33 | 34 | export default htmlResponse; 35 | -------------------------------------------------------------------------------- /src/utils/json-response.ts: -------------------------------------------------------------------------------- 1 | import ApiResponse from "../structs/api-response"; 2 | import { Infodata } from "../structs/info-data"; 3 | 4 | /** 5 | * Create JSON response. 6 | * 7 | * @param {Infodata} data User info data. 8 | * @returns {ApiResponse} 9 | */ 10 | function jsonResponse(data: Infodata): ApiResponse { 11 | return new ApiResponse(data.toJson()); 12 | } 13 | 14 | export default jsonResponse; 15 | -------------------------------------------------------------------------------- /src/utils/liquid.ts: -------------------------------------------------------------------------------- 1 | import { Liquid } from "liquidjs"; 2 | import { pickFilter } from "./pick-filter"; 3 | import { uniqueFilter } from "./unique-filter"; 4 | import { maskIpFilter } from "./mask-ip-filter"; 5 | 6 | const engine = new Liquid(); 7 | engine.registerFilter("pick", pickFilter); 8 | engine.registerFilter("unique", uniqueFilter); 9 | engine.registerFilter("maskIp", maskIpFilter); 10 | 11 | export { engine }; 12 | -------------------------------------------------------------------------------- /src/utils/mask-ip-filter.ts: -------------------------------------------------------------------------------- 1 | import { randomString } from "./random-string"; 2 | import { canUseApi } from "./api-check"; 3 | 4 | /** 5 | * LiquidJS filter to mask IP address (if required). 6 | * 7 | * @param {string} ip IP address 8 | */ 9 | function maskIpFilter(ip: string) { 10 | if (canUseApi()) return ip; 11 | 12 | const ipParts = [...ip].map((e) => { 13 | const rnd = randomString(5); 14 | return `${e}`; 15 | }); 16 | 17 | return ipParts.join(""); 18 | } 19 | 20 | export { maskIpFilter }; 21 | -------------------------------------------------------------------------------- /src/utils/pick-filter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * LiquidJS filter to pick specified fields from an object and return their 3 | * values as an array. Non-existent fields will be skipped 4 | * 5 | * @param {*} value An object to pick values from 6 | * @param {...any[]} args Field names to pick 7 | */ 8 | function pickFilter(value: any, ...args: any[]) { 9 | return args.reduce((acc, key) => { 10 | if (value[key]) { 11 | acc.push(value[key]); 12 | } 13 | 14 | return acc; 15 | }, []); 16 | } 17 | 18 | export { pickFilter }; 19 | -------------------------------------------------------------------------------- /src/utils/random-string.ts: -------------------------------------------------------------------------------- 1 | import ab2str from "arraybuffer-to-string"; 2 | 3 | function randomString(numBytes: number): string { 4 | const rnd = new Uint32Array(numBytes); 5 | crypto.getRandomValues(rnd); 6 | return ab2str(rnd, "hex"); 7 | } 8 | 9 | export { randomString }; 10 | -------------------------------------------------------------------------------- /src/utils/static-router.ts: -------------------------------------------------------------------------------- 1 | import assetStyle from "../assets/style.css"; 2 | import assetFavicon from "../assets/favicon.svg"; 3 | import assetTouchIcon from "../assets/apple-touch-icon.png"; 4 | import errorResponse from "./error-response"; 5 | import ms from "ms"; 6 | import { StatusCodes } from "http-status-codes"; 7 | 8 | /** 9 | * Create file response object. 10 | * 11 | * @param {ArrayBuffer} file File's ArrayBuffer. 12 | * @param {string} mime MIME type. 13 | * @param {number} [status=StatusCodes.OK] HTTP Status 14 | * @returns {Response} 15 | */ 16 | function fileResponse( 17 | file: BodyInit, 18 | mime: string, 19 | status: number = StatusCodes.OK, 20 | ): Response { 21 | return new Response(file, { 22 | status: status, 23 | headers: { 24 | "cache-control": `max-age=${ms("14d") / 1000}`, 25 | "content-type": mime, 26 | "x-content-type-options": "nosniff", 27 | }, 28 | }); 29 | } 30 | 31 | function jsonBufferResponse( 32 | file: Uint8Array, 33 | mime: string, 34 | status: number = StatusCodes.OK, 35 | ): Response { 36 | return fileResponse(file.buffer, mime, status); 37 | } 38 | 39 | /** 40 | * Create a response object router for static files. 41 | * 42 | * @param {string} path 43 | * @returns {Response} 44 | */ 45 | function staticRouter(path: string): Response { 46 | switch (path) { 47 | case "/favicon.ico": 48 | return fileResponse(assetFavicon, "image/svg+xml"); 49 | case "/assets/style.css": 50 | return fileResponse(assetStyle, "text/css"); 51 | case "/assets/apple-touch-icon.png": 52 | return jsonBufferResponse(assetTouchIcon, "image/png"); 53 | default: 54 | return errorResponse("Not found", 404); 55 | } 56 | } 57 | 58 | export default staticRouter; 59 | -------------------------------------------------------------------------------- /src/utils/text-agents.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Text-only user agents 3 | */ 4 | 5 | const textAgents = [ 6 | "curl", 7 | "httpie", 8 | "wget", 9 | "libfetch", 10 | "go-http-client", 11 | "ddclient", 12 | "mikrotik", 13 | ]; 14 | 15 | export default textAgents; 16 | -------------------------------------------------------------------------------- /src/utils/text-response.ts: -------------------------------------------------------------------------------- 1 | import { Infodata } from "../structs/info-data"; 2 | 3 | /** 4 | * Create plain text response. 5 | * 6 | * @param {Infodata} data 7 | * @returns {Response} User info data. 8 | */ 9 | function textResponse(data: Infodata): Response { 10 | return new Response(`${data.ip}\n`, { 11 | status: 200, 12 | headers: { 13 | "cache-control": "no-store", 14 | "x-content-type-options": "nosniff", 15 | "content-type": "text/plain; charset=utf-8", 16 | }, 17 | }); 18 | } 19 | 20 | export default textResponse; 21 | -------------------------------------------------------------------------------- /src/utils/unique-filter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * LiquidJS filter to filter out repeating array elements 3 | * 4 | * @param {Array<*>} value Array 5 | */ 6 | function uniqueFilter(value: any[]) { 7 | return [...new Set(value)]; 8 | } 9 | 10 | export { uniqueFilter }; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "module": "commonjs", 5 | "target": "ES2018", 6 | "lib": ["ES2018"], 7 | "alwaysStrict": true, 8 | "strict": true, 9 | "preserveConstEnums": true, 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "esModuleInterop": true, 13 | "types": ["@cloudflare/workers-types"] 14 | }, 15 | "include": [ 16 | "./types/*.d.ts", 17 | "./src/*.ts", 18 | "./src/**/*.ts", 19 | "./node_modules/@cloudflare/workers-types/index.d.ts" 20 | ], 21 | "exclude": ["node_modules/", "dist/"] 22 | } 23 | -------------------------------------------------------------------------------- /types/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module "*.liquid" { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module "*.css" { 12 | const content: string; 13 | export default content; 14 | } 15 | 16 | declare module "*.png" { 17 | const content: Uint8Array; 18 | export default content; 19 | } 20 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "arraybuffer-to-string" { 2 | function _exports( 3 | buffer: ArrayBuffer | SharedArrayBuffer, 4 | encoding?: string | null, 5 | ): string; 6 | export = _exports; 7 | } 8 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "ip" 2 | workers_dev = true 3 | main = "dist/index.js" 4 | compatibility_date = "2022-05-14" 5 | send_metrics = false 6 | kv_namespaces = [ 7 | { binding = "ASNCACHE", id = "d5c3236cd1684e259b0ee88f630a9ae0", preview_id = "d5c3236cd1684e259b0ee88f630a9ae0" } 8 | ] 9 | 10 | [build] 11 | command = "npm run build" 12 | --------------------------------------------------------------------------------