├── .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 |
--------------------------------------------------------------------------------