mirror of
https://github.com/MrUnknownDE/utools.git
synced 2026-05-30 16:10:06 +02:00
Compare commits
8 Commits
85ce9106db
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8deca43f20 | |||
| 1cdec3c54a | |||
| 27d9e7b154 | |||
| 014d1704de | |||
| ea0d192365 | |||
| 413810e298 | |||
| 972741b2fd | |||
| 97ccc32832 |
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c80be1dbf2b11aa6b2f0f2d6a9288ddcc590a941ccd94c423093f799d2c3ad00
|
||||
size 11970193
|
||||
oid sha256:be75726475eaa093155243a8743f461633ca1b5cbbfbd3fc2a4707d08636447e
|
||||
size 12178607
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:961bc818cb943a0a858ae61017fbeda9306819360b77ba9ea28fa6832fad1a1d
|
||||
size 65775230
|
||||
oid sha256:06f0aca0b9062cecc3a4f6ebe19028ba321b1b4942534c870fd49874fa292fee
|
||||
size 65982658
|
||||
|
||||
Generated
+61
-118
@@ -21,6 +21,9 @@
|
||||
"pino-pretty": "^13.0.0",
|
||||
"qs": "^6.14.2",
|
||||
"whois-json": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/otel": {
|
||||
@@ -120,9 +123,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/core": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.0.tgz",
|
||||
"integrity": "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==",
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz",
|
||||
"integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
@@ -297,23 +300,6 @@
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/instrumentation-ioredis": {
|
||||
"version": "0.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.62.0.tgz",
|
||||
"integrity": "sha512-ZYt//zcPve8qklaZX+5Z4MkU7UpEkFRrxsf2cnaKYBitqDnsCN69CPAuuMOX6NYdW2rG9sFy7V/QWtBlP5XiNQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/instrumentation": "^0.214.0",
|
||||
"@opentelemetry/redis-common": "^0.38.2",
|
||||
"@opentelemetry/semantic-conventions": "^1.33.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/instrumentation-kafkajs": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.23.0.tgz",
|
||||
@@ -465,23 +451,6 @@
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/instrumentation-redis": {
|
||||
"version": "0.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.62.0.tgz",
|
||||
"integrity": "sha512-y3pPpot7WzR/8JtHcYlTYsyY8g+pbFhAqbwAuG5bLPnR6v6pt1rQc0DpH0OlGP/9CZbWBP+Zhwp9yFoygf/ZXQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/instrumentation": "^0.214.0",
|
||||
"@opentelemetry/redis-common": "^0.38.2",
|
||||
"@opentelemetry/semantic-conventions": "^1.27.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/instrumentation-tedious": {
|
||||
"version": "0.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.33.0.tgz",
|
||||
@@ -499,22 +468,13 @@
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/redis-common": {
|
||||
"version": "0.38.3",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.3.tgz",
|
||||
"integrity": "sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/resources": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.0.tgz",
|
||||
"integrity": "sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A==",
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz",
|
||||
"integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.7.0",
|
||||
"@opentelemetry/core": "2.7.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -525,13 +485,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-trace-base": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.0.tgz",
|
||||
"integrity": "sha512-Yg9zEXJB50DLVLpsKPk7NmNqlPlS+OvqhJGh0A8oawIOTPOwlm4eXs9BMJV7L79lvEwI+dWtAj+YjTyddV336A==",
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz",
|
||||
"integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.7.0",
|
||||
"@opentelemetry/resources": "2.7.0",
|
||||
"@opentelemetry/core": "2.7.1",
|
||||
"@opentelemetry/resources": "2.7.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -542,9 +502,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/semantic-conventions": {
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz",
|
||||
"integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==",
|
||||
"version": "1.41.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz",
|
||||
"integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -625,18 +585,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "10.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.50.0.tgz",
|
||||
"integrity": "sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg==",
|
||||
"version": "10.53.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.53.1.tgz",
|
||||
"integrity": "sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/node": {
|
||||
"version": "10.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.50.0.tgz",
|
||||
"integrity": "sha512-TvwzFQu8MGKzMQ2/tqxcNzFA8UG2kKTB+GDmA4uOzx3+GT849YZRRSJzEXCmYhk1teVd2fbmgqyYY2nyLF5a+Q==",
|
||||
"version": "10.53.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.53.1.tgz",
|
||||
"integrity": "sha512-rxHVil0tJAmz+keFcZCj1LaUdgdkK66E/l0gqh2p1209PNCGoD3lnClFr6vusy1aF3zF8O9JPtuMEJzXOKhs+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/otel": "0.18.0",
|
||||
@@ -651,7 +611,6 @@
|
||||
"@opentelemetry/instrumentation-graphql": "0.62.0",
|
||||
"@opentelemetry/instrumentation-hapi": "0.60.0",
|
||||
"@opentelemetry/instrumentation-http": "0.214.0",
|
||||
"@opentelemetry/instrumentation-ioredis": "0.62.0",
|
||||
"@opentelemetry/instrumentation-kafkajs": "0.23.0",
|
||||
"@opentelemetry/instrumentation-knex": "0.58.0",
|
||||
"@opentelemetry/instrumentation-koa": "0.62.0",
|
||||
@@ -661,14 +620,13 @@
|
||||
"@opentelemetry/instrumentation-mysql": "0.60.0",
|
||||
"@opentelemetry/instrumentation-mysql2": "0.60.0",
|
||||
"@opentelemetry/instrumentation-pg": "0.66.0",
|
||||
"@opentelemetry/instrumentation-redis": "0.62.0",
|
||||
"@opentelemetry/instrumentation-tedious": "0.33.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.6.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.40.0",
|
||||
"@prisma/instrumentation": "7.6.0",
|
||||
"@sentry/core": "10.50.0",
|
||||
"@sentry/node-core": "10.50.0",
|
||||
"@sentry/opentelemetry": "10.50.0",
|
||||
"@sentry/core": "10.53.1",
|
||||
"@sentry/node-core": "10.53.1",
|
||||
"@sentry/opentelemetry": "10.53.1",
|
||||
"import-in-the-middle": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -676,13 +634,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/node-core": {
|
||||
"version": "10.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.50.0.tgz",
|
||||
"integrity": "sha512-Eb1BYf4Lc7ZYmdX3acKP6SgyGikrBA370gbGHaWI5jRu7G7vig8sIu1ghPmY5AlvqBPOetado7GniXr6fAXbTw==",
|
||||
"version": "10.53.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.53.1.tgz",
|
||||
"integrity": "sha512-iH7SMcM/7jPbN+t7+7ussQOiIqI4BMOGt4VYWlV71/z7k0pY+YPaSvlfVkNXfISiDzFAKv0ecCY3BmsLMu1xDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "10.50.0",
|
||||
"@sentry/opentelemetry": "10.50.0",
|
||||
"@sentry/core": "10.53.1",
|
||||
"@sentry/opentelemetry": "10.53.1",
|
||||
"import-in-the-middle": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -718,12 +676,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/opentelemetry": {
|
||||
"version": "10.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.50.0.tgz",
|
||||
"integrity": "sha512-axn3pgDPveGdaMUC0abMCmFN7ux2pA5ebPufCef4lMIsyg7BBQvaEJ+vE19wjstMaBCAJGsdZlL3eeP2rtgRMw==",
|
||||
"version": "10.53.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.53.1.tgz",
|
||||
"integrity": "sha512-Zok6UXla0mFOjd1YnVb1TZtQNOry9v93fXUqx8jmDaygwWM2BwvP8rBQabLz0/OZXo8+35oge+Vmw+QY5aesnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "10.50.0"
|
||||
"@sentry/core": "10.53.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -754,12 +712,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||
"version": "25.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
|
||||
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.19.0"
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
@@ -898,9 +856,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
@@ -1259,14 +1217,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"version": "4.22.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "~1.20.3",
|
||||
"body-parser": "~1.20.5",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "~0.7.1",
|
||||
@@ -1285,7 +1243,7 @@
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "~6.14.0",
|
||||
"qs": "~6.15.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "~0.19.0",
|
||||
@@ -1319,21 +1277,6 @@
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/qs": {
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-copy": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.3.tgz",
|
||||
@@ -1568,9 +1511,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
@@ -2126,9 +2069,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
|
||||
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
|
||||
"version": "6.15.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
@@ -2440,12 +2383,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.7",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
|
||||
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^10.0.1",
|
||||
"ip-address": "^10.1.1",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2585,9 +2528,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.19.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"whois-json": "^2.0.4"
|
||||
},
|
||||
"overrides": {
|
||||
"underscore": "1.13.8"
|
||||
"underscore": ">=1.13.8",
|
||||
"ip-address": ">=10.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getCleanIp } = require('../utils');
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
const ip = getCleanIp(req.ip || req.socket.remoteAddress);
|
||||
res.json({ ip: ip || null });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,75 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const dns = require('dns').promises;
|
||||
const pino = require('pino');
|
||||
const { getMaxMindReaders } = require('../maxmind');
|
||||
|
||||
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
||||
|
||||
// ASN org-name patterns that strongly suggest a VPN service
|
||||
const VPN_PATTERNS = [
|
||||
/\bvpn\b/i, /nordvpn/i, /expressvpn/i, /mullvad/i, /31173\s+services/i,
|
||||
/owl\s+limited/i, /surfshark/i, /protonvpn/i, /cyberghost/i, /ipvanish/i,
|
||||
/purevpn/i, /tunnelbear/i, /private.?internet.?access/i, /\bpia\b/i,
|
||||
/hide\.?my\.?ip/i, /hidemyass/i, /windscribe/i, /perfect.?privacy/i,
|
||||
];
|
||||
|
||||
// ASN org-name patterns that suggest cloud/datacenter/hosting (but not necessarily VPN)
|
||||
const DATACENTER_PATTERNS = [
|
||||
/amazon/i, /\baws\b/i, /google.*cloud/i, /microsoft/i, /\bazure\b/i,
|
||||
/cloudflare/i, /digitalocean/i, /linode/i, /vultr/i, /hetzner/i,
|
||||
/\bovh\b/i, /leaseweb/i, /rackspace/i, /choopa/i, /equinix/i,
|
||||
/hostinger/i, /\bhosting\b/i, /data.?cent(?:er|re)/i, /\bcloud\b/i,
|
||||
/\bcoloc\b/i, /\bvps\b/i, /akamai/i, /fastly/i, /\bcdn\b/i,
|
||||
];
|
||||
|
||||
// Tor DNS exit-node check via torproject.org DNSBL
|
||||
async function isTorExit(ip) {
|
||||
if (!ip || ip.includes(':')) return false; // IPv6 not supported by this DNSBL
|
||||
try {
|
||||
const reversed = ip.split('.').reverse().join('.');
|
||||
await dns.resolve4(`${reversed}.dnsel.torproject.org`);
|
||||
return true; // resolves → known Tor exit node
|
||||
} catch {
|
||||
return false; // NXDOMAIN → not a Tor exit node
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/:ip', async (req, res, next) => {
|
||||
const { ip } = req.params;
|
||||
const flags = [];
|
||||
|
||||
try {
|
||||
// ASN-based classification (synchronous, no network call)
|
||||
try {
|
||||
const { asnReader } = getMaxMindReaders();
|
||||
const asnData = asnReader.asn(ip);
|
||||
const org = asnData?.autonomousSystemOrganization || '';
|
||||
|
||||
if (VPN_PATTERNS.some(p => p.test(org))) {
|
||||
flags.push({ id: 'vpn', label: 'VPN', color: 'yellow' });
|
||||
} else if (DATACENTER_PATTERNS.some(p => p.test(org))) {
|
||||
flags.push({ id: 'datacenter', label: 'Datacenter / Hosting', color: 'orange' });
|
||||
} else if (org) {
|
||||
flags.push({ id: 'residential', label: 'Residential / ISP', color: 'green' });
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug({ ip, err: e.message }, 'ASN lookup skipped for privacy check');
|
||||
}
|
||||
|
||||
// Tor exit-node check (async DNS, ~100–300 ms)
|
||||
const tor = await isTorExit(ip);
|
||||
if (tor) {
|
||||
flags.length = 0; // Tor supersedes all network-type flags — showing "Residential" alongside Tor is misleading
|
||||
flags.push({ id: 'tor', label: 'Tor Exit Node', color: 'red' });
|
||||
}
|
||||
|
||||
logger.info({ ip, flags: flags.map(f => f.id) }, 'Privacy check complete');
|
||||
res.json({ success: true, ip, flags });
|
||||
} catch (err) {
|
||||
logger.error({ ip, err: err.message }, 'Privacy check failed');
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -32,6 +32,8 @@ const versionRoutes = require('./routes/version');
|
||||
const portScanRoutes = require('./routes/portScan');
|
||||
const macLookupRoutes = require('./routes/macLookup');
|
||||
const asnLookupRoutes = require('./routes/asnLookup');
|
||||
const privacyRoutes = require('./routes/privacy');
|
||||
const myipRoutes = require('./routes/myip');
|
||||
|
||||
// --- Logger Initialisierung ---
|
||||
const logger = pino({
|
||||
@@ -88,6 +90,8 @@ app.use('/api/version', versionRoutes);
|
||||
app.use('/api/port-scan', portScanRoutes);
|
||||
app.use('/api/mac-lookup', macLookupRoutes);
|
||||
app.use('/api/asn-lookup', asnLookupRoutes);
|
||||
app.use('/api/privacy', privacyRoutes);
|
||||
app.use('/api/myip', myipRoutes);
|
||||
|
||||
|
||||
// Sentry error handler — must be after routes, before custom error handler
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
# Aktuell nicht nötig, da wir CDN/statische Dateien haben.
|
||||
|
||||
# Stage 2: Production Environment using Nginx
|
||||
FROM nginx:1.27-alpine
|
||||
FROM nginx:1.31.0-alpine
|
||||
|
||||
# Arbeitsverzeichnis im Container (optional, aber gute Praxis)
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
+455
-98
@@ -25,6 +25,27 @@ export const page = {
|
||||
</a>
|
||||
<button id="copy-ip-btn" class="copy-btn hidden">copy</button>
|
||||
</div>
|
||||
<!-- IPv6 row — shown only when dual-stack is detected -->
|
||||
<div id="ipv6-row" class="hidden mt-2 flex items-center gap-2">
|
||||
<span class="text-xs font-bold text-blue-400/60 uppercase tracking-wider w-10 shrink-0">IPv6</span>
|
||||
<span id="ipv6-address" class="font-mono text-blue-300 text-sm break-all"></span>
|
||||
<button id="copy-ipv6-btn" class="copy-btn hidden">copy</button>
|
||||
</div>
|
||||
<!-- Privacy / risk flags — dual-stack: always show both rows -->
|
||||
<div id="privacy-checks" class="mt-3 space-y-1.5">
|
||||
<div class="flex items-center gap-2 min-h-[22px]">
|
||||
<span class="text-xs font-bold text-gray-500 uppercase tracking-wider w-10 shrink-0">IPv4</span>
|
||||
<div id="privacy-flags-v4" class="flex flex-wrap gap-1.5 items-center">
|
||||
<div id="privacy-loader-v4" class="loader" style="width:12px;height:12px;border-width:2px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 min-h-[22px]">
|
||||
<span class="text-xs font-bold text-blue-400/60 uppercase tracking-wider w-10 shrink-0">IPv6</span>
|
||||
<div id="privacy-flags-v6" class="flex flex-wrap gap-1.5 items-center">
|
||||
<span class="text-xs text-gray-600 italic">detecting…</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-card rounded-lg p-5 space-y-4">
|
||||
@@ -68,23 +89,71 @@ export const page = {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right column: map -->
|
||||
<div class="space-y-4 fade-in" style="animation-delay:.2s">
|
||||
<h2 class="text-lg font-semibold text-gray-200 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-purple-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<!-- Right column: map — container is h-[420px] so the Leaflet map can fill it -->
|
||||
<div class="fade-in" style="animation-delay:.2s">
|
||||
<h2 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-purple-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Location Visualization
|
||||
Location
|
||||
</h2>
|
||||
<div id="map-container" class="bg-gray-800/50 rounded-lg min-h-[400px] h-full flex items-center justify-center relative border border-gray-700/50 shadow-inner overflow-hidden">
|
||||
<div id="map-loader" class="loader absolute z-10"></div>
|
||||
<div id="map" class="w-full h-full rounded-lg hidden z-0 opacity-80 hover:opacity-100 transition-opacity duration-700"></div>
|
||||
<p id="map-message" class="text-gray-400 hidden absolute text-sm">Could not load map.</p>
|
||||
<div class="absolute inset-0 pointer-events-none rounded-lg ring-1 ring-inset ring-white/10"></div>
|
||||
<div id="map-container" class="rounded-xl h-[420px] relative border border-gray-700/50 overflow-hidden bg-gray-900/60 shadow-inner">
|
||||
<div id="map-loader" class="loader absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10"></div>
|
||||
<div id="map" class="w-full hidden z-0 transition-opacity duration-700 rounded-xl"></div>
|
||||
<p id="map-message" class="text-gray-400 hidden absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-sm">Could not load map.</p>
|
||||
<div class="absolute inset-0 pointer-events-none rounded-xl ring-1 ring-inset ring-white/10"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Browser Fingerprint -->
|
||||
<div class="mt-6 glass-card rounded-xl p-5 fade-in" style="animation-delay:.3s">
|
||||
<h2 class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-700 pb-2 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Browser Fingerprint
|
||||
</h2>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-x-8 gap-y-4 text-sm">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Browser</p>
|
||||
<p id="fp-browser" class="text-gray-200 font-medium font-mono">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Operating System</p>
|
||||
<p id="fp-os" class="text-gray-200 font-medium">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Screen</p>
|
||||
<p id="fp-screen" class="text-gray-200 font-medium font-mono">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Viewport</p>
|
||||
<p id="fp-viewport" class="text-gray-200 font-medium font-mono">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Language</p>
|
||||
<p id="fp-language" class="text-gray-200 font-medium">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Timezone</p>
|
||||
<p id="fp-timezone" class="text-gray-200 font-medium">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Color Depth</p>
|
||||
<p id="fp-color" class="text-gray-200 font-medium">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Privacy</p>
|
||||
<p id="fp-privacy" class="text-gray-200 font-medium text-xs leading-relaxed">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<details class="mt-4 border-t border-gray-700/50 pt-3">
|
||||
<summary class="text-xs text-gray-500 cursor-pointer hover:text-gray-300 select-none list-none transition-colors">User Agent string</summary>
|
||||
<p id="fp-ua" class="font-mono text-xs text-gray-400 break-all mt-2 leading-relaxed"></p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- IP Lookup -->
|
||||
<div class="mt-8 p-6 glass-card rounded-xl">
|
||||
<h2 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-pink-500 mb-4 flex items-center gap-2">
|
||||
@@ -104,12 +173,14 @@ export const page = {
|
||||
<div id="lookup-error" class="text-red-400 mb-4 hidden p-3 bg-red-900/20 border border-red-500/30 rounded text-sm"></div>
|
||||
|
||||
<div id="lookup-results-section" class="hidden grid grid-cols-1 md:grid-cols-2 gap-8 mt-6 border-t border-gray-700/50 pt-6 fade-in">
|
||||
<div class="space-y-6">
|
||||
<!-- Left: info -->
|
||||
<div class="space-y-5">
|
||||
<h3 class="text-lg font-semibold text-gray-200">Result for: <span id="lookup-ip-address" class="font-mono text-purple-400 bg-purple-500/10 px-2 py-0.5 rounded"></span>
|
||||
<button id="copy-lookup-ip-btn" class="copy-btn ml-2">copy</button>
|
||||
</h3>
|
||||
<div id="lookup-result-loader" class="loader hidden"></div>
|
||||
<div id="lookup-geo-info" class="space-y-1 text-sm text-gray-300">
|
||||
|
||||
<div id="lookup-geo-info" class="text-sm text-gray-300">
|
||||
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">Geolocation</h4>
|
||||
<div class="grid grid-cols-2 gap-x-2 gap-y-1">
|
||||
<p><span class="text-gray-500">Country:</span> <span id="lookup-country" class="text-white">-</span></p>
|
||||
@@ -118,69 +189,145 @@ export const page = {
|
||||
<p><span class="text-gray-500">Zip:</span> <span id="lookup-postal" class="text-white">-</span></p>
|
||||
<p class="col-span-2"><span class="text-gray-500">Coords:</span> <span id="lookup-coords" class="font-mono text-purple-300">-</span></p>
|
||||
<p class="col-span-2"><span class="text-gray-500">Time:</span> <span id="lookup-timezone" class="text-white">-</span></p>
|
||||
<p id="lookup-geo-error" class="text-red-400 col-span-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="lookup-asn-info" class="space-y-1 text-sm text-gray-300">
|
||||
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2 mt-4">ASN</h4>
|
||||
<p><span class="text-gray-500">Number:</span> <span id="lookup-asn-number" class="text-white font-mono">-</span></p>
|
||||
<p><span class="text-gray-500">Org:</span> <span id="lookup-asn-org" class="text-white">-</span></p>
|
||||
<p id="lookup-asn-error" class="text-red-400"></p>
|
||||
</div>
|
||||
<div id="lookup-rdns-info" class="space-y-1 text-sm text-gray-300">
|
||||
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2 mt-4">Reverse DNS</h4>
|
||||
<ul id="lookup-rdns-list" class="list-none space-y-1 font-mono text-xs text-green-400"><li>-</li></ul>
|
||||
<p id="lookup-rdns-error" class="text-red-400"></p>
|
||||
<p id="lookup-geo-error" class="text-red-400 col-span-2 text-xs"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">Location Map</h4>
|
||||
<div id="lookup-map-container" class="glass-panel rounded-lg min-h-[250px] flex items-center justify-center relative overflow-hidden">
|
||||
<div id="lookup-map-loader" class="loader hidden absolute z-10"></div>
|
||||
<div id="lookup-map" class="w-full rounded hidden opacity-90"></div>
|
||||
<p id="lookup-map-message" class="text-gray-400 hidden absolute">Could not load map.</p>
|
||||
<div class="absolute inset-0 pointer-events-none ring-1 ring-inset ring-white/10 rounded-lg"></div>
|
||||
<div id="lookup-asn-info" class="text-sm text-gray-300">
|
||||
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">ASN</h4>
|
||||
<p><span class="text-gray-500">Number:</span> <span id="lookup-asn-number" class="text-white font-mono">-</span></p>
|
||||
<p><span class="text-gray-500">Org:</span> <span id="lookup-asn-org" class="text-white">-</span></p>
|
||||
<p id="lookup-asn-error" class="text-red-400 text-xs"></p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 pt-2">
|
||||
<button id="lookup-ping-button" disabled class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md">Ping</button>
|
||||
<button id="lookup-trace-button" disabled class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md">Trace</button>
|
||||
<button id="lookup-scan-button" disabled class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md">Port Scan</button>
|
||||
|
||||
<div id="lookup-rdns-info" class="text-sm text-gray-300">
|
||||
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">Reverse DNS</h4>
|
||||
<ul id="lookup-rdns-list" class="list-none space-y-1 font-mono text-xs text-green-400"><li>-</li></ul>
|
||||
<p id="lookup-rdns-error" class="text-red-400 text-xs"></p>
|
||||
</div>
|
||||
<div id="lookup-ping-results" class="mt-4 text-sm hidden fade-in">
|
||||
<h4 class="font-bold text-purple-400 mb-2 flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> Ping Results
|
||||
</h4>
|
||||
<div id="lookup-ping-loader" class="loader hidden"></div>
|
||||
<pre id="lookup-ping-output" class="result-pre mt-1"></pre>
|
||||
<p id="lookup-ping-error" class="text-red-400 mt-2"></p>
|
||||
</div>
|
||||
|
||||
<!-- Right: map + action buttons -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider">Location Map</h4>
|
||||
<div id="lookup-map-container" class="rounded-xl h-[260px] relative overflow-hidden bg-gray-900/60 border border-gray-700/50">
|
||||
<div id="lookup-map-loader" class="loader hidden absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10"></div>
|
||||
<div id="lookup-map" class="w-full hidden rounded-xl"></div>
|
||||
<p id="lookup-map-message" class="text-gray-400 hidden absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-sm text-center px-4">Could not load map.</p>
|
||||
<div class="absolute inset-0 pointer-events-none ring-1 ring-inset ring-white/10 rounded-xl"></div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button id="lookup-ping-button" disabled title="Send ICMP ping"
|
||||
class="action-tool-btn flex flex-col items-center gap-1.5 py-3 px-2 rounded-lg transition-all disabled:opacity-40 disabled:cursor-not-allowed border border-gray-700/50 bg-gray-800/50 hover:bg-purple-900/30 hover:border-purple-500/40 text-gray-400 hover:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
||||
</svg>
|
||||
<span class="text-xs font-semibold">Ping</span>
|
||||
</button>
|
||||
<button id="lookup-trace-button" disabled title="Run traceroute"
|
||||
class="action-tool-btn flex flex-col items-center gap-1.5 py-3 px-2 rounded-lg transition-all disabled:opacity-40 disabled:cursor-not-allowed border border-gray-700/50 bg-gray-800/50 hover:bg-purple-900/30 hover:border-purple-500/40 text-gray-400 hover:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
|
||||
</svg>
|
||||
<span class="text-xs font-semibold">Traceroute</span>
|
||||
</button>
|
||||
<button id="lookup-scan-button" disabled title="Scan common ports"
|
||||
class="action-tool-btn flex flex-col items-center gap-1.5 py-3 px-2 rounded-lg transition-all disabled:opacity-40 disabled:cursor-not-allowed border border-gray-700/50 bg-gray-800/50 hover:bg-purple-900/30 hover:border-purple-500/40 text-gray-400 hover:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18"/>
|
||||
</svg>
|
||||
<span class="text-xs font-semibold">Port Scan</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ping Results — dedicated section, consistent with traceroute/port-scan -->
|
||||
<div id="ping-section" class="mt-8 p-6 glass-card rounded-xl hidden fade-in">
|
||||
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
||||
</svg>
|
||||
Ping — <span id="ping-target" class="font-mono text-white text-base font-normal ml-1"></span>
|
||||
</h2>
|
||||
<div class="flex items-center gap-3 mb-4 text-sm">
|
||||
<div id="ping-section-loader" class="loader hidden"></div>
|
||||
<span id="ping-section-message" class="text-gray-400"></span>
|
||||
<span id="ping-section-error" class="text-red-400"></span>
|
||||
</div>
|
||||
<!-- Stat cards -->
|
||||
<div id="ping-stats-grid" class="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4 hidden">
|
||||
<div class="bg-gray-900/50 rounded-lg p-4 text-center border border-gray-700/30">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-1">Sent</p>
|
||||
<p id="ping-stat-sent" class="text-3xl font-bold font-mono text-white">-</p>
|
||||
</div>
|
||||
<div class="bg-gray-900/50 rounded-lg p-4 text-center border border-gray-700/30">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-1">Received</p>
|
||||
<p id="ping-stat-recv" class="text-3xl font-bold font-mono text-green-400">-</p>
|
||||
</div>
|
||||
<div class="bg-gray-900/50 rounded-lg p-4 text-center border border-gray-700/30">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-1">Packet Loss</p>
|
||||
<p id="ping-stat-loss" class="text-3xl font-bold font-mono text-red-400">-</p>
|
||||
</div>
|
||||
<div class="bg-gray-900/50 rounded-lg p-4 text-center border border-gray-700/30">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-1">Avg RTT</p>
|
||||
<p id="ping-stat-rtt" class="text-3xl font-bold font-mono text-blue-300">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- RTT range bar -->
|
||||
<div id="ping-rtt-range" class="hidden mb-4 px-4 py-3 bg-gray-900/40 rounded-lg border border-gray-700/30 flex flex-wrap gap-6 text-xs font-mono text-gray-500">
|
||||
<span>min <span id="ping-rtt-min" class="text-gray-200 font-semibold">-</span> ms</span>
|
||||
<span>avg <span id="ping-rtt-avg" class="text-gray-200 font-semibold">-</span> ms</span>
|
||||
<span>max <span id="ping-rtt-max" class="text-gray-200 font-semibold">-</span> ms</span>
|
||||
</div>
|
||||
<!-- Raw output -->
|
||||
<details>
|
||||
<summary class="text-xs text-gray-500 hover:text-gray-300 cursor-pointer select-none list-none transition-colors">Raw output</summary>
|
||||
<pre id="ping-raw-output" class="result-pre mt-2 text-xs"></pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Traceroute -->
|
||||
<div id="traceroute-section" class="mt-8 p-6 glass-card rounded-xl hidden fade-in">
|
||||
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4">Traceroute Results</h2>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
|
||||
</svg>
|
||||
Traceroute — <span id="traceroute-target" class="font-mono text-white text-base font-normal ml-1"></span>
|
||||
</h2>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<div id="traceroute-loader" class="loader hidden"></div>
|
||||
<span id="traceroute-message" class="text-gray-300"></span>
|
||||
<span id="traceroute-message" class="text-gray-400"></span>
|
||||
</div>
|
||||
<button id="traceroute-stop-btn" class="stop-btn hidden">■ Stop</button>
|
||||
<button id="traceroute-stop-btn" class="stop-btn hidden">■ Stop</button>
|
||||
</div>
|
||||
<div id="traceroute-output" class="rounded-lg overflow-hidden"><pre class="m-0"></pre></div>
|
||||
<!-- Hop table header -->
|
||||
<div class="hidden sm:grid traceroute-header text-xs text-gray-600 uppercase tracking-wider px-2 mb-1" style="grid-template-columns:2rem 1fr auto">
|
||||
<span class="text-right pr-3">#</span>
|
||||
<span>IP / Hostname</span>
|
||||
<span class="text-right">RTT</span>
|
||||
</div>
|
||||
<div id="traceroute-output" class="font-mono text-sm rounded-lg border border-gray-700/30 bg-black/20 overflow-y-auto max-h-[420px] p-2 space-y-0.5"></div>
|
||||
</div>
|
||||
|
||||
<!-- Port Scan -->
|
||||
<div id="port-scan-section" class="mt-8 p-6 glass-card rounded-xl hidden fade-in">
|
||||
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4">Port Scan Results</h2>
|
||||
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18"/>
|
||||
</svg>
|
||||
Port Scan Results
|
||||
</h2>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<div id="port-scan-loader" class="loader hidden"></div>
|
||||
<span id="port-scan-message" class="text-gray-300"></span>
|
||||
</div>
|
||||
<button id="port-scan-stop-btn" class="stop-btn hidden">■ Stop</button>
|
||||
<button id="port-scan-stop-btn" class="stop-btn hidden">■ Stop</button>
|
||||
</div>
|
||||
<div id="port-scan-output" class="text-sm font-mono bg-gray-900/50 p-4 rounded-lg border border-gray-700/50 max-h-[300px] overflow-y-auto"></div>
|
||||
</div>
|
||||
@@ -215,13 +362,20 @@ export const page = {
|
||||
const lookupPingBtn = document.getElementById('lookup-ping-button');
|
||||
const lookupTraceBtn = document.getElementById('lookup-trace-button');
|
||||
const lookupScanBtn = document.getElementById('lookup-scan-button');
|
||||
const lookupPingRes = document.getElementById('lookup-ping-results');
|
||||
const lookupPingLoader= document.getElementById('lookup-ping-loader');
|
||||
const lookupPingOutput= document.getElementById('lookup-ping-output');
|
||||
const lookupPingError = document.getElementById('lookup-ping-error');
|
||||
|
||||
// Ping section
|
||||
const pingSect = document.getElementById('ping-section');
|
||||
const pingTarget = document.getElementById('ping-target');
|
||||
const pingSectLoader = document.getElementById('ping-section-loader');
|
||||
const pingSectMsg = document.getElementById('ping-section-message');
|
||||
const pingSectErr = document.getElementById('ping-section-error');
|
||||
const pingStatsGrid = document.getElementById('ping-stats-grid');
|
||||
const pingRttRange = document.getElementById('ping-rtt-range');
|
||||
const pingRawOutput = document.getElementById('ping-raw-output');
|
||||
|
||||
const tracerouteSection = document.getElementById('traceroute-section');
|
||||
const tracerouteOutput = document.querySelector('#traceroute-output pre');
|
||||
const tracerouteTarget = document.getElementById('traceroute-target');
|
||||
const tracerouteOutput = document.getElementById('traceroute-output');
|
||||
const tracerouteLoader = document.getElementById('traceroute-loader');
|
||||
const tracerouteMessage = document.getElementById('traceroute-message');
|
||||
const tracerouteStopBtn = document.getElementById('traceroute-stop-btn');
|
||||
@@ -233,7 +387,7 @@ export const page = {
|
||||
const portScanStopBtn = document.getElementById('port-scan-stop-btn');
|
||||
|
||||
// ── State ────────────────────────────────────────────────────
|
||||
let map = null, lookupMap = null, currentIp = null, currentLookupIp = null;
|
||||
let currentIp = null, currentLookupIp = null;
|
||||
let eventSource = null, portScanEventSource = null;
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────
|
||||
@@ -323,11 +477,88 @@ export const page = {
|
||||
// ── Copy buttons ─────────────────────────────────────────────
|
||||
setupCopyBtn(copyIpBtn, () => ipAddressSpan.textContent);
|
||||
setupCopyBtn(copyLookupIpBtn, () => lookupIpEl.textContent);
|
||||
const copyIpv6Btn = document.getElementById('copy-ipv6-btn');
|
||||
setupCopyBtn(copyIpv6Btn, () => document.getElementById('ipv6-address').textContent);
|
||||
|
||||
// ── Lookup button enable/disable ─────────────────────────────
|
||||
const syncLookupBtn = () => { lookupBtn.disabled = !lookupInput.value.trim(); };
|
||||
lookupInput.addEventListener('input', syncLookupBtn);
|
||||
|
||||
// ── Privacy / risk flags ──────────────────────────────────────
|
||||
const FLAG_COLORS = {
|
||||
green: 'bg-green-900/40 text-green-300 border border-green-700/50',
|
||||
orange: 'bg-orange-900/40 text-orange-300 border border-orange-700/50',
|
||||
yellow: 'bg-yellow-900/40 text-yellow-300 border border-yellow-700/50',
|
||||
red: 'bg-red-900/40 text-red-300 border border-red-700/50',
|
||||
};
|
||||
|
||||
async function fetchPrivacyFlags(ip, version = 4) {
|
||||
const container = document.getElementById(`privacy-flags-v${version}`);
|
||||
const loader = document.getElementById(`privacy-loader-v${version}`);
|
||||
if (!container) return;
|
||||
try {
|
||||
const r = await fetch(`${API}/privacy/${encodeURIComponent(ip)}`);
|
||||
const data = await r.json();
|
||||
loader?.remove();
|
||||
if (!data.flags?.length) {
|
||||
container.innerHTML = '<span class="text-xs text-gray-600">—</span>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = data.flags.map(f =>
|
||||
`<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold ${FLAG_COLORS[f.color] || FLAG_COLORS.green}">${f.label}</span>`
|
||||
).join('');
|
||||
} catch {
|
||||
loader?.remove();
|
||||
container.innerHTML = '<span class="text-xs text-gray-600">N/A</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Browser fingerprint ───────────────────────────────────────
|
||||
function populateBrowserFingerprint() {
|
||||
const ua = navigator.userAgent;
|
||||
|
||||
function getBrowser() {
|
||||
if (/Edg\/(\d+)/.test(ua)) return `Edge ${RegExp.$1}`;
|
||||
if (/OPR\/(\d+)/.test(ua)) return `Opera ${RegExp.$1}`;
|
||||
if (/Chrome\/(\d+)/.test(ua)) return `Chrome ${RegExp.$1}`;
|
||||
if (/Firefox\/(\d+)/.test(ua)) return `Firefox ${RegExp.$1}`;
|
||||
if (/Version\/([\d.]+).*Safari/.test(ua)) return `Safari ${RegExp.$1}`;
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
function getOS() {
|
||||
if (/Windows NT 10\.0/.test(ua)) return 'Windows 10 / 11';
|
||||
if (/Windows NT 6\.3/.test(ua)) return 'Windows 8.1';
|
||||
if (/Windows NT 6\.1/.test(ua)) return 'Windows 7';
|
||||
if (/Mac OS X ([\d_]+)/.test(ua)) return `macOS ${RegExp.$1.replace(/_/g, '.')}`;
|
||||
if (/Android ([\d.]+)/.test(ua)) return `Android ${RegExp.$1}`;
|
||||
if (/iPhone OS ([\d_]+)/.test(ua)) return `iOS ${RegExp.$1.replace(/_/g, '.')}`;
|
||||
if (/iPad.*OS ([\d_]+)/.test(ua)) return `iPadOS ${RegExp.$1.replace(/_/g, '.')}`;
|
||||
if (/Linux/.test(ua)) return 'Linux';
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
document.getElementById('fp-browser').textContent = getBrowser();
|
||||
document.getElementById('fp-os').textContent = getOS();
|
||||
document.getElementById('fp-screen').textContent =
|
||||
`${screen.width} × ${screen.height}` + (window.devicePixelRatio !== 1 ? ` @ ${window.devicePixelRatio}×` : '');
|
||||
document.getElementById('fp-viewport').textContent = `${window.innerWidth} × ${window.innerHeight} px`;
|
||||
document.getElementById('fp-language').textContent =
|
||||
navigator.language + (navigator.languages?.length > 1 ? ` (+${navigator.languages.length - 1} more)` : '');
|
||||
document.getElementById('fp-timezone').textContent = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
document.getElementById('fp-color').textContent = `${screen.colorDepth}-bit`;
|
||||
|
||||
const bits = [];
|
||||
if (navigator.cookieEnabled) bits.push('Cookies on');
|
||||
else bits.push('Cookies off');
|
||||
if (navigator.doNotTrack === '1') bits.push('DNT enabled');
|
||||
const conn = navigator.connection;
|
||||
if (conn?.effectiveType) bits.push(conn.effectiveType.toUpperCase());
|
||||
document.getElementById('fp-privacy').textContent = bits.join(' · ');
|
||||
|
||||
document.getElementById('fp-ua').textContent = ua;
|
||||
}
|
||||
|
||||
// ── Own IP fetch ─────────────────────────────────────────────
|
||||
async function fetchIpInfo() {
|
||||
globalError.classList.add('hidden');
|
||||
@@ -337,21 +568,93 @@ export const page = {
|
||||
mapEl.classList.add('hidden');
|
||||
mapMessage.classList.add('hidden');
|
||||
|
||||
// Force-detect via protocol-specific subdomains (ipv4./ipv6. have A-only / AAAA-only DNS)
|
||||
async function forceDetect(url) {
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 4000);
|
||||
try {
|
||||
const r = await fetch(url, { signal: ctrl.signal });
|
||||
if (!r.ok) return null;
|
||||
const d = await r.json();
|
||||
return d.ip || null;
|
||||
} catch { return null; }
|
||||
finally { clearTimeout(timer); }
|
||||
}
|
||||
|
||||
function setPrivacyNA(version) {
|
||||
const el = document.getElementById(`privacy-flags-v${version}`);
|
||||
if (el) { document.getElementById(`privacy-loader-v${version}`)?.remove(); el.innerHTML = '<span class="text-xs text-gray-600">N/A</span>'; }
|
||||
}
|
||||
|
||||
// Use current hostname so it works for any deployment (utools.mrunk.de → ipv4.utools.mrunk.de)
|
||||
const host = location.hostname;
|
||||
const [ipv4, ipv6] = await Promise.all([
|
||||
forceDetect(`https://ipv4.${host}/api/myip`),
|
||||
forceDetect(`https://ipv6.${host}/api/myip`),
|
||||
]);
|
||||
|
||||
const hasSeperateIPv6 = !!(ipv4 && ipv6 && ipv4 !== ipv6);
|
||||
const primaryIp = ipv4 || ipv6;
|
||||
|
||||
// Fetch geo / ASN / rDNS — use /api/lookup for detected IP, /api/ipinfo as fallback
|
||||
let data;
|
||||
try {
|
||||
if (primaryIp) {
|
||||
const r = await fetch(`${API}/lookup?targetIp=${encodeURIComponent(primaryIp)}`);
|
||||
if (!r.ok) throw new Error(`${r.statusText} (${r.status})`);
|
||||
data = await r.json();
|
||||
data.ip = primaryIp;
|
||||
} else {
|
||||
// Local dev / subdomains not reachable — fall back to auto-detect
|
||||
const r = await fetch(`${API}/ipinfo`);
|
||||
if (!r.ok) throw new Error(`${r.statusText} (${r.status})`);
|
||||
const data = await r.json();
|
||||
data = await r.json();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch IP info:', err);
|
||||
showGlobalErr(`Could not load IP information. ${err.message}`);
|
||||
[ipLoader, geoLoader, asnLoader, rdnsLoader, mapLoader].forEach(l => l?.classList.add('hidden'));
|
||||
return;
|
||||
}
|
||||
|
||||
currentIp = data.ip;
|
||||
|
||||
// Show primary IP
|
||||
ipAddressSpan.textContent = data.ip;
|
||||
ipAddressLink.classList.remove('hidden');
|
||||
copyIpBtn.classList.remove('hidden');
|
||||
ipLoader.classList.add('hidden');
|
||||
ipAddressLink.addEventListener('click', e => {
|
||||
ipAddressLink.onclick = e => {
|
||||
e.preventDefault();
|
||||
if (currentIp) window._router.navigate('/whois', { query: currentIp });
|
||||
});
|
||||
window._router.navigate('/whois', { query: currentIp });
|
||||
};
|
||||
|
||||
// IPv6 row — only when a separate IPv6 was detected alongside IPv4
|
||||
if (hasSeperateIPv6) {
|
||||
document.getElementById('ipv6-address').textContent = ipv6;
|
||||
document.getElementById('ipv6-row').classList.remove('hidden');
|
||||
copyIpv6Btn.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Privacy checks — each slot gets the correct IP or N/A
|
||||
const v4ForPrivacy = ipv4 || (!data.ip.includes(':') ? data.ip : null);
|
||||
const v6ForPrivacy = hasSeperateIPv6 ? ipv6 : (data.ip.includes(':') ? data.ip : null);
|
||||
|
||||
if (v4ForPrivacy) {
|
||||
fetchPrivacyFlags(v4ForPrivacy, 4);
|
||||
} else {
|
||||
setPrivacyNA(4);
|
||||
}
|
||||
|
||||
if (v6ForPrivacy) {
|
||||
const v6El = document.getElementById('privacy-flags-v6');
|
||||
if (v6El) v6El.innerHTML = '<div id="privacy-loader-v6" class="loader" style="width:12px;height:12px;border-width:2px"></div>';
|
||||
fetchPrivacyFlags(v6ForPrivacy, 6);
|
||||
} else {
|
||||
setPrivacyNA(6);
|
||||
}
|
||||
|
||||
// Populate geo / ASN / rDNS / map
|
||||
updateField(document.getElementById('country'), data.geo?.countryName ? `${data.geo.countryName} (${data.geo.country})` : null, null, document.getElementById('geo-error'));
|
||||
updateField(document.getElementById('region'), data.geo?.region);
|
||||
updateField(document.getElementById('city'), data.geo?.city);
|
||||
@@ -370,13 +673,7 @@ export const page = {
|
||||
}
|
||||
updateField(document.getElementById('asn-org'), data.asn?.organization, asnLoader);
|
||||
updateRdns(document.getElementById('rdns-list'), data.rdns, rdnsLoader, document.getElementById('rdns-error'));
|
||||
map = initOrUpdateMap('map', data.geo?.latitude, data.geo?.longitude, mapEl, mapLoader, mapMessage);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch IP info:', err);
|
||||
showGlobalErr(`Could not load IP information. ${err.message}`);
|
||||
[ipLoader, geoLoader, asnLoader, rdnsLoader, mapLoader].forEach(l => l?.classList.add('hidden'));
|
||||
}
|
||||
initOrUpdateMap('map', data.geo?.latitude, data.geo?.longitude, mapEl, mapLoader, mapMessage);
|
||||
}
|
||||
|
||||
// ── Lookup ───────────────────────────────────────────────────
|
||||
@@ -386,8 +683,7 @@ export const page = {
|
||||
lookupMapLoader.classList.add('hidden');
|
||||
lookupMapEl.classList.add('hidden');
|
||||
lookupMapMsg.classList.add('hidden');
|
||||
lookupPingRes.classList.add('hidden');
|
||||
lookupPingLoader.classList.add('hidden');
|
||||
pingSect.classList.add('hidden');
|
||||
portScanSection.classList.add('hidden');
|
||||
portScanOutput.innerHTML = '';
|
||||
[lookupIpEl, document.getElementById('lookup-country'), document.getElementById('lookup-region'),
|
||||
@@ -397,8 +693,6 @@ export const page = {
|
||||
document.getElementById('lookup-geo-error'), document.getElementById('lookup-asn-error'),
|
||||
document.getElementById('lookup-rdns-error')].forEach(el => { if (el) el.textContent = ''; });
|
||||
document.getElementById('lookup-rdns-list').innerHTML = '<li>-</li>';
|
||||
lookupPingOutput.textContent = '';
|
||||
lookupPingError.textContent = '';
|
||||
[lookupPingBtn, lookupTraceBtn, lookupScanBtn].forEach(b => { if (b) b.disabled = true; });
|
||||
currentLookupIp = null;
|
||||
if (window['lookup-map_instance']) { window['lookup-map_instance'].remove(); window['lookup-map_instance'] = null; }
|
||||
@@ -460,7 +754,7 @@ export const page = {
|
||||
}
|
||||
updateField(document.getElementById('lookup-asn-org'), data.asn?.organization);
|
||||
updateRdns(document.getElementById('lookup-rdns-list'), data.rdns, null, document.getElementById('lookup-rdns-error'));
|
||||
lookupMap = initOrUpdateMap('lookup-map', data.geo?.latitude, data.geo?.longitude, lookupMapEl, lookupMapLoader, lookupMapMsg);
|
||||
initOrUpdateMap('lookup-map', data.geo?.latitude, data.geo?.longitude, lookupMapEl, lookupMapLoader, lookupMapMsg);
|
||||
[lookupPingBtn, lookupTraceBtn, lookupScanBtn].forEach(b => { if (b) b.disabled = false; });
|
||||
|
||||
} catch (err) {
|
||||
@@ -478,25 +772,44 @@ export const page = {
|
||||
|
||||
// ── Ping ─────────────────────────────────────────────────────
|
||||
async function runPing(ip) {
|
||||
lookupPingRes.classList.remove('hidden');
|
||||
lookupPingLoader.classList.remove('hidden');
|
||||
lookupPingOutput.textContent = '';
|
||||
lookupPingError.textContent = '';
|
||||
pingSect.classList.remove('hidden');
|
||||
pingTarget.textContent = ip;
|
||||
pingSectLoader.classList.remove('hidden');
|
||||
pingSectMsg.textContent = `Pinging ${ip}…`;
|
||||
pingSectErr.textContent = '';
|
||||
pingStatsGrid.classList.add('hidden');
|
||||
pingRttRange.classList.add('hidden');
|
||||
pingRawOutput.textContent = '';
|
||||
pingSect.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
try {
|
||||
const r = await fetch(`${API}/ping?targetIp=${encodeURIComponent(ip)}`);
|
||||
const data = await r.json();
|
||||
if (!r.ok || !data.success) throw new Error(data.error || `HTTP ${r.status}`);
|
||||
let out = `--- Ping Statistics for ${ip} ---\n`;
|
||||
if (data.stats) {
|
||||
out += `Packets: ${data.stats.packets.transmitted} sent, ${data.stats.packets.received} received, ${data.stats.packets.lossPercent}% loss\n`;
|
||||
if (data.stats.rtt) out += `RTT (ms): min=${data.stats.rtt.min} avg=${data.stats.rtt.avg} max=${data.stats.rtt.max}\n`;
|
||||
|
||||
if (data.stats?.packets) {
|
||||
const loss = data.stats.packets.lossPercent;
|
||||
document.getElementById('ping-stat-sent').textContent = data.stats.packets.transmitted;
|
||||
document.getElementById('ping-stat-recv').textContent = data.stats.packets.received;
|
||||
document.getElementById('ping-stat-loss').textContent = `${loss}%`;
|
||||
document.getElementById('ping-stat-loss').className =
|
||||
`text-3xl font-bold font-mono ${loss === 0 || loss === '0' ? 'text-green-400' : loss >= 50 ? 'text-red-400' : 'text-yellow-400'}`;
|
||||
document.getElementById('ping-stat-rtt').textContent = data.stats.rtt ? `${data.stats.rtt.avg} ms` : '-';
|
||||
pingStatsGrid.classList.remove('hidden');
|
||||
}
|
||||
out += `\n--- Raw Output ---\n${data.rawOutput || ''}`;
|
||||
lookupPingOutput.textContent = out;
|
||||
if (data.stats?.rtt) {
|
||||
document.getElementById('ping-rtt-min').textContent = data.stats.rtt.min;
|
||||
document.getElementById('ping-rtt-avg').textContent = data.stats.rtt.avg;
|
||||
document.getElementById('ping-rtt-max').textContent = data.stats.rtt.max;
|
||||
pingRttRange.classList.remove('hidden');
|
||||
}
|
||||
pingRawOutput.textContent = data.rawOutput || '';
|
||||
pingSectMsg.textContent = `Ping to ${ip} complete.`;
|
||||
} catch (err) {
|
||||
lookupPingError.textContent = `Ping Error: ${err.message}`;
|
||||
pingSectErr.textContent = `Ping failed: ${err.message}`;
|
||||
pingSectMsg.textContent = '';
|
||||
} finally {
|
||||
lookupPingLoader.classList.add('hidden');
|
||||
pingSectLoader.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,11 +817,13 @@ export const page = {
|
||||
function startTraceroute(ip) {
|
||||
if (eventSource) { eventSource.close(); eventSource = null; }
|
||||
tracerouteSection.classList.remove('hidden');
|
||||
tracerouteOutput.textContent = '';
|
||||
if (tracerouteTarget) tracerouteTarget.textContent = ip;
|
||||
tracerouteOutput.innerHTML = '';
|
||||
tracerouteLoader.classList.remove('hidden');
|
||||
tracerouteStopBtn.classList.remove('hidden');
|
||||
tracerouteMessage.textContent = `Starting traceroute to ${ip}…`;
|
||||
globalError.classList.add('hidden');
|
||||
tracerouteSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
eventSource = new EventSource(`${API}/traceroute?targetIp=${encodeURIComponent(ip)}`);
|
||||
|
||||
@@ -552,34 +867,74 @@ export const page = {
|
||||
|
||||
function displayTraceLine(text, cls = '') {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('px-2', 'py-0.5', 'fade-in');
|
||||
if (cls) div.classList.add(cls);
|
||||
div.classList.add('fade-in');
|
||||
div.textContent = text;
|
||||
tracerouteOutput.appendChild(div);
|
||||
tracerouteOutput.scrollTop = tracerouteOutput.scrollHeight;
|
||||
}
|
||||
|
||||
function displayHop(hop) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('hop-line', 'fade-in');
|
||||
const num = document.createElement('span'); num.classList.add('hop-number'); num.textContent = hop.hop || '?'; div.appendChild(num);
|
||||
const row = document.createElement('div');
|
||||
row.classList.add('hop-row', 'fade-in');
|
||||
|
||||
// Hop number
|
||||
const numEl = document.createElement('span');
|
||||
numEl.classList.add('hop-number');
|
||||
numEl.textContent = hop.hop ?? '?';
|
||||
row.appendChild(numEl);
|
||||
|
||||
// Body: IP line + optional RDNS line below
|
||||
const body = document.createElement('div');
|
||||
body.classList.add('hop-body');
|
||||
|
||||
if (hop.ip) {
|
||||
const ip = document.createElement('span'); ip.classList.add('hop-ip'); ip.textContent = hop.ip; div.appendChild(ip);
|
||||
if (hop.hostname) { const h = document.createElement('span'); h.classList.add('hop-hostname'); h.textContent = ` (${hop.hostname})`; div.appendChild(h); }
|
||||
} else if (hop.rtt?.every(r => r === '*')) {
|
||||
const t = document.createElement('span'); t.classList.add('hop-timeout'); t.textContent = '* * *'; div.appendChild(t);
|
||||
} else {
|
||||
div.appendChild(document.createTextNode(hop.rawLine || 'Unknown hop'));
|
||||
}
|
||||
const ipLine = document.createElement('div');
|
||||
ipLine.classList.add('hop-ip-line');
|
||||
|
||||
const ipEl = document.createElement('span');
|
||||
ipEl.classList.add('hop-ip');
|
||||
ipEl.textContent = hop.ip;
|
||||
ipLine.appendChild(ipEl);
|
||||
|
||||
// RTTs — right-aligned via flex margin-left auto on the rtt group
|
||||
if (Array.isArray(hop.rtt)) {
|
||||
const rttsEl = document.createElement('span');
|
||||
rttsEl.classList.add('hop-rtts');
|
||||
hop.rtt.forEach(r => {
|
||||
const s = document.createElement('span');
|
||||
s.classList.add(r === '*' ? 'hop-timeout' : 'hop-rtt');
|
||||
s.textContent = r === '*' ? '*' : `${r} ms`;
|
||||
div.appendChild(s);
|
||||
rttsEl.appendChild(s);
|
||||
});
|
||||
ipLine.appendChild(rttsEl);
|
||||
}
|
||||
tracerouteOutput.appendChild(div);
|
||||
body.appendChild(ipLine);
|
||||
|
||||
// RDNS — own line, only when it differs from the IP
|
||||
if (hop.hostname && hop.hostname !== hop.ip) {
|
||||
const rdnsEl = document.createElement('div');
|
||||
rdnsEl.classList.add('hop-rdns');
|
||||
rdnsEl.textContent = hop.hostname;
|
||||
body.appendChild(rdnsEl);
|
||||
}
|
||||
} else if (hop.rtt?.every(r => r === '*')) {
|
||||
const line = document.createElement('div');
|
||||
line.classList.add('hop-ip-line');
|
||||
const t = document.createElement('span');
|
||||
t.classList.add('hop-timeout');
|
||||
t.textContent = '* * *';
|
||||
line.appendChild(t);
|
||||
body.appendChild(line);
|
||||
} else {
|
||||
const line = document.createElement('div');
|
||||
line.classList.add('hop-ip-line', 'text-gray-400');
|
||||
line.textContent = hop.rawLine || 'Unknown hop';
|
||||
body.appendChild(line);
|
||||
}
|
||||
|
||||
row.appendChild(body);
|
||||
tracerouteOutput.appendChild(row);
|
||||
tracerouteOutput.scrollTop = tracerouteOutput.scrollHeight;
|
||||
}
|
||||
|
||||
@@ -591,6 +946,7 @@ export const page = {
|
||||
portScanLoader.classList.remove('hidden');
|
||||
portScanStopBtn.classList.remove('hidden');
|
||||
portScanMessage.textContent = `Starting port scan for ${ip}…`;
|
||||
portScanSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
portScanEventSource = new EventSource(`${API}/port-scan?targetIp=${encodeURIComponent(ip)}`);
|
||||
portScanEventSource.onopen = () => {};
|
||||
@@ -650,6 +1006,7 @@ export const page = {
|
||||
portScanStopBtn.addEventListener('click', stopPortScan);
|
||||
|
||||
// ── Bootstrap ────────────────────────────────────────────────
|
||||
populateBrowserFingerprint();
|
||||
fetchIpInfo();
|
||||
|
||||
const params = new URLSearchParams(search);
|
||||
|
||||
+452
-92
@@ -1,27 +1,249 @@
|
||||
export const page = {
|
||||
title: 'Subnetz Rechner',
|
||||
title: 'Subnet Calculator',
|
||||
|
||||
template: () => `
|
||||
<div class="container mx-auto max-w-5xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
|
||||
<h2 class="text-3xl font-bold mb-8 text-center text-gradient">IP Subnetz Rechner</h2>
|
||||
<h2 class="text-3xl font-bold mb-4 text-center text-gradient">IP Subnet Calculator</h2>
|
||||
|
||||
<!-- Mode Toggle -->
|
||||
<div class="flex justify-center mb-8">
|
||||
<div class="flex bg-gray-900/70 border border-gray-700/50 rounded-xl p-1 gap-1">
|
||||
<button id="btn-beginner" class="px-6 py-2 rounded-lg text-sm font-semibold transition-all duration-200 bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg">
|
||||
Beginner
|
||||
</button>
|
||||
<button id="btn-pro" class="px-6 py-2 rounded-lg text-sm font-semibold transition-all duration-200 text-gray-400 hover:text-gray-200">
|
||||
Expert
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BEGINNER MODE -->
|
||||
<div id="beginner-mode">
|
||||
<div class="glass-card p-6 rounded-xl mb-6">
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">IP Address</label>
|
||||
<input type="text" id="beg-ip" value="192.168.1.0" placeholder="e.g. 192.168.1.0"
|
||||
class="w-full px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 font-mono transition-all">
|
||||
<p class="text-xs text-gray-500 mt-1">Enter an IPv4 address, e.g. 192.168.1.0</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<label class="text-gray-400 text-sm font-bold uppercase tracking-wide">Network Size</label>
|
||||
<span id="beg-cidr-label" class="text-3xl font-mono font-bold text-purple-300">/24</span>
|
||||
</div>
|
||||
<input type="range" id="beg-slider" min="1" max="30" value="24" list="cidr-marks"
|
||||
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-purple-500">
|
||||
<datalist id="cidr-marks">
|
||||
<option value="8"></option>
|
||||
<option value="16"></option>
|
||||
<option value="24"></option>
|
||||
</datalist>
|
||||
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>/1 — huge</span>
|
||||
<span>/8</span>
|
||||
<span>/16</span>
|
||||
<span>/24</span>
|
||||
<span>/30 — tiny</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Host count highlight -->
|
||||
<div class="mb-4 p-4 bg-gray-900/60 rounded-xl border border-purple-500/20 flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-1">Usable Hosts</p>
|
||||
<p id="beg-hosts-count" class="text-4xl font-bold font-mono text-green-400">254</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-1">Subnet Mask</p>
|
||||
<p id="beg-mask-display" class="text-sm font-mono text-gray-300">255.255.255.0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visual bar -->
|
||||
<div class="mb-6">
|
||||
<span class="text-xs text-gray-500 uppercase tracking-wider">Relative Network Size (logarithmic)</span>
|
||||
<div class="h-3 bg-gray-800 rounded-full overflow-hidden border border-gray-700/50 mt-1">
|
||||
<div id="beg-bar" class="h-full bg-gradient-to-r from-purple-600 to-pink-500 transition-all duration-500 rounded-full" style="width:26%"></div>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs text-gray-600 mt-1">
|
||||
<span>2 hosts (/30)</span>
|
||||
<span>2 billion hosts (/1)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results grid -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<div class="bg-gray-900/50 rounded-lg p-3 text-center border border-gray-700/30">
|
||||
<p class="text-xs text-gray-500 mb-1">Network ID</p>
|
||||
<p id="beg-network" class="font-mono text-white text-sm font-bold">-</p>
|
||||
<p class="text-xs text-gray-600 mt-1">network address</p>
|
||||
</div>
|
||||
<div class="bg-gray-900/50 rounded-lg p-3 text-center border border-gray-700/30">
|
||||
<p class="text-xs text-gray-500 mb-1">First Host</p>
|
||||
<p id="beg-first" class="font-mono text-blue-300 text-sm font-bold">-</p>
|
||||
<p class="text-xs text-gray-600 mt-1">1st usable address</p>
|
||||
</div>
|
||||
<div class="bg-gray-900/50 rounded-lg p-3 text-center border border-gray-700/30">
|
||||
<p class="text-xs text-gray-500 mb-1">Last Host</p>
|
||||
<p id="beg-last" class="font-mono text-blue-300 text-sm font-bold">-</p>
|
||||
<p class="text-xs text-gray-600 mt-1">last usable address</p>
|
||||
</div>
|
||||
<div class="bg-gray-900/50 rounded-lg p-3 text-center border border-gray-700/30">
|
||||
<p class="text-xs text-gray-500 mb-1">Broadcast</p>
|
||||
<p id="beg-broadcast" class="font-mono text-purple-400 text-sm font-bold">-</p>
|
||||
<p class="text-xs text-gray-600 mt-1">send to all devices</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Explanation card -->
|
||||
<div class="glass-card p-6 rounded-xl border border-purple-500/20 mb-6">
|
||||
<h3 class="text-base font-bold text-purple-300 mb-3 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
What does this mean?
|
||||
</h3>
|
||||
<p id="beg-explain-text" class="text-sm text-gray-300 leading-relaxed mb-4"></p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 text-xs">
|
||||
<div class="p-3 bg-gray-900/40 rounded-lg border border-gray-700/30">
|
||||
<p class="text-white font-semibold mb-1">Network Address</p>
|
||||
<p class="text-gray-400">The first address — identifies the network itself. No device can use this address.</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-900/40 rounded-lg border border-gray-700/30">
|
||||
<p class="text-white font-semibold mb-1">Host Addresses</p>
|
||||
<p class="text-gray-400">All addresses in between — assignable to devices like PCs, servers, or printers.</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-900/40 rounded-lg border border-gray-700/30">
|
||||
<p class="text-white font-semibold mb-1">Broadcast Address</p>
|
||||
<p class="text-gray-400">The last address — packets sent here are delivered to every device in the network.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick examples -->
|
||||
<div class="glass-card p-4 rounded-xl mb-6">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider mb-3 font-bold">Typical Networks — click to try</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button class="beg-example px-3 py-1.5 text-xs bg-gray-800 hover:bg-purple-900/50 border border-gray-700/50 hover:border-purple-500/50 rounded-lg font-mono text-gray-300 hover:text-white transition-all" data-ip="192.168.1.0" data-cidr="24">192.168.1.0/24 — home network (254 hosts)</button>
|
||||
<button class="beg-example px-3 py-1.5 text-xs bg-gray-800 hover:bg-purple-900/50 border border-gray-700/50 hover:border-purple-500/50 rounded-lg font-mono text-gray-300 hover:text-white transition-all" data-ip="192.168.0.0" data-cidr="16">192.168.0.0/16 — large (65k hosts)</button>
|
||||
<button class="beg-example px-3 py-1.5 text-xs bg-gray-800 hover:bg-purple-900/50 border border-gray-700/50 hover:border-purple-500/50 rounded-lg font-mono text-gray-300 hover:text-white transition-all" data-ip="10.0.0.0" data-cidr="8">10.0.0.0/8 — huge (16M hosts)</button>
|
||||
<button class="beg-example px-3 py-1.5 text-xs bg-gray-800 hover:bg-purple-900/50 border border-gray-700/50 hover:border-purple-500/50 rounded-lg font-mono text-gray-300 hover:text-white transition-all" data-ip="192.168.1.0" data-cidr="28">192.168.1.0/28 — small (14 hosts)</button>
|
||||
<button class="beg-example px-3 py-1.5 text-xs bg-gray-800 hover:bg-purple-900/50 border border-gray-700/50 hover:border-purple-500/50 rounded-lg font-mono text-gray-300 hover:text-white transition-all" data-ip="10.0.0.0" data-cidr="30">10.0.0.0/30 — P2P link (2 hosts)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subnet explainer -->
|
||||
<details class="glass-card rounded-xl border border-gray-700/30 group">
|
||||
<summary class="flex items-center justify-between p-5 cursor-pointer select-none list-none">
|
||||
<span class="text-sm font-bold text-gray-300 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
|
||||
</svg>
|
||||
What is a subnet, exactly?
|
||||
</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-500 transition-transform duration-200 group-open:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</summary>
|
||||
|
||||
<div class="px-5 pb-6 space-y-6 border-t border-gray-700/30 pt-5">
|
||||
|
||||
<!-- Analogy -->
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-purple-300 mb-2">The neighbourhood analogy</h4>
|
||||
<p class="text-sm text-gray-400 leading-relaxed mb-3">
|
||||
Think of a city. Every house has a full address: <span class="font-mono text-gray-200">district + house number</span>.
|
||||
A subnet works the same way — every IP address is split into two parts.
|
||||
</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div class="p-3 bg-purple-900/20 border border-purple-500/30 rounded-lg">
|
||||
<p class="text-xs font-bold text-purple-300 uppercase tracking-wider mb-1">Network part (district)</p>
|
||||
<p class="font-mono text-white text-sm mb-1">192.168.1.<span class="text-gray-500">___</span></p>
|
||||
<p class="text-xs text-gray-400">All devices in the same subnet share this part — like neighbours on the same street.</p>
|
||||
</div>
|
||||
<div class="p-3 bg-blue-900/20 border border-blue-500/30 rounded-lg">
|
||||
<p class="text-xs font-bold text-blue-300 uppercase tracking-wider mb-1">Host part (house number)</p>
|
||||
<p class="font-mono text-white text-sm mb-1"><span class="text-gray-500">___</span>.42</p>
|
||||
<p class="text-xs text-gray-400">Each device gets a unique number within the network.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- What does /24 mean -->
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-purple-300 mb-2">What does the slash mean?</h4>
|
||||
<p class="text-sm text-gray-400 leading-relaxed mb-3">
|
||||
An IP address is made up of exactly <strong class="text-white">32 bits</strong> (ones and zeros) under the hood.
|
||||
The <span class="font-mono text-purple-300">/24</span> says: “the first 24 bits belong to the network, the remaining 8 bits are the host number.”
|
||||
</p>
|
||||
<div class="p-3 bg-gray-900/60 rounded-lg border border-gray-700/30 font-mono text-xs overflow-x-auto">
|
||||
<div class="flex items-center gap-2 mb-1 min-w-max">
|
||||
<span class="text-gray-500 w-20 shrink-0">192.168.1.42</span>
|
||||
<span class="text-gray-600">=</span>
|
||||
<span class="text-purple-300">11000000.10101000.00000001</span><span class="text-gray-600">.</span><span class="text-blue-300">00101010</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 min-w-max">
|
||||
<span class="text-gray-500 w-20 shrink-0">/24 mask</span>
|
||||
<span class="text-gray-600">=</span>
|
||||
<span class="text-purple-300">11111111.11111111.11111111</span><span class="text-gray-600">.</span><span class="text-blue-300">00000000</span>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-2 min-w-max">
|
||||
<span class="w-20 shrink-0"></span>
|
||||
<span class="text-gray-600 ml-1"> </span>
|
||||
<span class="text-purple-400 text-xs">←——— network (24 bits) ———→</span>
|
||||
<span class="text-blue-400 text-xs">← host (8 bits) →</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">8 host bits = 2<sup>8</sup> = 256 addresses, 254 usable (minus network ID and broadcast).</p>
|
||||
</div>
|
||||
|
||||
<!-- Why subnets -->
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-purple-300 mb-2">Why do subnets exist?</h4>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 text-xs">
|
||||
<div class="p-3 bg-gray-900/40 rounded-lg border border-gray-700/30">
|
||||
<p class="font-semibold text-white mb-1">Organisation</p>
|
||||
<p class="text-gray-400">Group devices logically — e.g. keep office PCs separate from servers or guest Wi-Fi.</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-900/40 rounded-lg border border-gray-700/30">
|
||||
<p class="font-semibold text-white mb-1">Security</p>
|
||||
<p class="text-gray-400">Isolate networks from each other — malware on the guest network can't reach corporate systems.</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-900/40 rounded-lg border border-gray-700/30">
|
||||
<p class="font-semibold text-white mb-1">Efficiency</p>
|
||||
<p class="text-gray-400">Broadcast traffic stays within the subnet — no unnecessary noise for the rest of the network.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- EXPERT MODE -->
|
||||
<div id="pro-mode" class="hidden">
|
||||
<form id="subnet-form" class="mb-8 glass-card p-6 rounded-xl">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
|
||||
<div>
|
||||
<label for="ip-address" class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">IP Adresse:</label>
|
||||
<input type="text" id="ip-address" name="ip-address" placeholder="z.B. 192.168.1.1" required
|
||||
<label for="ip-address" class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">IP Address:</label>
|
||||
<input type="text" id="ip-address" name="ip-address" placeholder="e.g. 192.168.1.1" required
|
||||
class="w-full px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all">
|
||||
<p class="text-xs text-gray-500 mt-1">IPv4 in dotted-decimal notation</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="cidr" class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">CIDR / Maske:</label>
|
||||
<input type="text" id="cidr" name="cidr" placeholder="z.B. 24 oder 255.255.255.0" required
|
||||
<label for="cidr" class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">CIDR / Mask:</label>
|
||||
<input type="text" id="cidr" name="cidr" placeholder="e.g. 24 or 255.255.255.0" required
|
||||
class="w-full px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all">
|
||||
<p class="text-xs text-gray-500 mt-1">CIDR (0–32) or subnet mask (e.g. 255.255.255.0)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="subnet-error" class="hidden mb-4 p-3 bg-red-900/20 border border-red-500/30 rounded text-red-400 text-sm"></div>
|
||||
<button type="submit"
|
||||
class="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white font-bold py-3 px-6 rounded-lg shadow-lg hover:shadow-purple-500/25 transition-all duration-200 ease-in-out transform hover:-translate-y-0.5">
|
||||
Berechnen
|
||||
Calculate
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -30,32 +252,64 @@ export const page = {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Ergebnisse:
|
||||
Results:
|
||||
</h3>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
|
||||
<span class="text-gray-400">Netzwerkadresse:</span>
|
||||
|
||||
<div class="space-y-2 text-sm mb-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 border-b border-gray-700/50 pb-2 items-start">
|
||||
<span class="text-gray-400 font-semibold">Network Address:</span>
|
||||
<span id="network-address" class="font-mono text-white font-semibold">-</span>
|
||||
<span class="text-xs text-gray-500 italic">First address of the network — not assignable to hosts</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
|
||||
<span class="text-gray-400">Broadcast-Adresse:</span>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 border-b border-gray-700/50 pb-2 items-start">
|
||||
<span class="text-gray-400 font-semibold">Broadcast Address:</span>
|
||||
<span id="broadcast-address" class="font-mono text-purple-400 font-semibold">-</span>
|
||||
<span class="text-xs text-gray-500 italic">Last address — delivers packets to all hosts simultaneously</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
|
||||
<span class="text-gray-400">Subnetzmaske:</span>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 border-b border-gray-700/50 pb-2 items-start">
|
||||
<span class="text-gray-400 font-semibold">Subnet Mask:</span>
|
||||
<span id="subnet-mask" class="font-mono text-gray-300">-</span>
|
||||
<span class="text-xs text-gray-500 italic">Separates the network and host portions of the IP address</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
|
||||
<span class="text-gray-400">Anzahl der Hosts:</span>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 border-b border-gray-700/50 pb-2 items-start">
|
||||
<span class="text-gray-400 font-semibold">Usable Hosts:</span>
|
||||
<span id="host-count" class="font-mono text-green-400 font-bold">-</span>
|
||||
<span class="text-xs text-gray-500 italic">Usable IPs = 2<sup>host bits</sup> − 2</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
|
||||
<span class="text-gray-400">Erste Host-Adresse:</span>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 border-b border-gray-700/50 pb-2 items-start">
|
||||
<span class="text-gray-400 font-semibold">First Host:</span>
|
||||
<span id="first-host" class="font-mono text-blue-300">-</span>
|
||||
<span class="text-xs text-gray-500 italic">Network address + 1</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<span class="text-gray-400">Letzte Host-Adresse:</span>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 items-start">
|
||||
<span class="text-gray-400 font-semibold">Last Host:</span>
|
||||
<span id="last-host" class="font-mono text-blue-300">-</span>
|
||||
<span class="text-xs text-gray-500 italic">Broadcast − 1</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Binary visualization -->
|
||||
<div class="mt-4 p-4 bg-gray-900/60 rounded-lg border border-gray-700/30">
|
||||
<h4 class="text-xs text-gray-400 uppercase tracking-wider font-bold mb-3">Binary Representation</h4>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="font-mono text-xs space-y-2 min-w-max">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-gray-500 w-16 text-right shrink-0 text-xs">IP:</span>
|
||||
<span id="bin-ip" class="tracking-wide"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-gray-500 w-16 text-right shrink-0 text-xs">Mask:</span>
|
||||
<span id="bin-mask" class="tracking-wide"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-gray-500 w-16 text-right shrink-0 text-xs">Network:</span>
|
||||
<span id="bin-net" class="tracking-wide"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 pt-1 border-t border-gray-700/30">
|
||||
<span class="w-16 shrink-0"></span>
|
||||
<span id="bin-legend" class="text-xs text-gray-500"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,49 +317,177 @@ export const page = {
|
||||
<!-- Example subnets -->
|
||||
<div class="glass-card rounded-xl p-6 mt-8">
|
||||
<h3 class="text-lg font-bold text-gray-400 uppercase tracking-wider border-b border-gray-700/50 pb-2 mb-4">
|
||||
Beispiel-Subnetze (Private Adressbereiche)
|
||||
Example Subnets (Private Address Ranges)
|
||||
</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm text-left text-gray-400">
|
||||
<thead class="text-xs uppercase bg-gray-800/50 text-gray-200">
|
||||
<tr>
|
||||
<th class="px-6 py-3">Bereich</th>
|
||||
<th class="px-6 py-3">Range</th>
|
||||
<th class="px-6 py-3">CIDR</th>
|
||||
<th class="px-6 py-3">Subnetzmaske</th>
|
||||
<th class="px-6 py-3">Beschreibung</th>
|
||||
<th class="px-6 py-3">Aktion</th>
|
||||
<th class="px-6 py-3">Subnet Mask</th>
|
||||
<th class="px-6 py-3">Description</th>
|
||||
<th class="px-6 py-3">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-700/50">
|
||||
<tr class="hover:bg-gray-700/30 transition-colors">
|
||||
<td class="px-6 py-4 font-mono text-white">192.168.0.0 – 192.168.255.255</td>
|
||||
<td class="px-6 py-4 font-mono">/16 (Gesamt)</td>
|
||||
<td class="px-6 py-4 font-mono text-white">192.168.0.0 – 192.168.255.255</td>
|
||||
<td class="px-6 py-4 font-mono">/16 (total)</td>
|
||||
<td class="px-6 py-4 font-mono">255.255.0.0</td>
|
||||
<td class="px-6 py-4">Klasse C (oft als /24 genutzt)</td>
|
||||
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="192.168.1.1" data-cidr="24">Beispiel /24</span></td>
|
||||
<td class="px-6 py-4">Class C (commonly used as /24)</td>
|
||||
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="192.168.1.1" data-cidr="24">Example /24</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-700/30 transition-colors">
|
||||
<td class="px-6 py-4 font-mono text-white">172.16.0.0 – 172.31.255.255</td>
|
||||
<td class="px-6 py-4 font-mono">/12 (Gesamt)</td>
|
||||
<td class="px-6 py-4 font-mono text-white">172.16.0.0 – 172.31.255.255</td>
|
||||
<td class="px-6 py-4 font-mono">/12 (total)</td>
|
||||
<td class="px-6 py-4 font-mono">255.240.0.0</td>
|
||||
<td class="px-6 py-4">Klasse B</td>
|
||||
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="172.16.10.5" data-cidr="16">Beispiel /16</span></td>
|
||||
<td class="px-6 py-4">Class B</td>
|
||||
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="172.16.10.5" data-cidr="16">Example /16</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-700/30 transition-colors">
|
||||
<td class="px-6 py-4 font-mono text-white">10.0.0.0 – 10.255.255.255</td>
|
||||
<td class="px-6 py-4 font-mono">/8 (Gesamt)</td>
|
||||
<td class="px-6 py-4 font-mono text-white">10.0.0.0 – 10.255.255.255</td>
|
||||
<td class="px-6 py-4 font-mono">/8 (total)</td>
|
||||
<td class="px-6 py-4 font-mono">255.0.0.0</td>
|
||||
<td class="px-6 py-4">Klasse A</td>
|
||||
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="10.0.50.100" data-cidr="8">Beispiel /8</span></td>
|
||||
<td class="px-6 py-4">Class A</td>
|
||||
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="10.0.50.100" data-cidr="8">Example /8</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="mt-4 text-xs text-gray-500 italic">Klicken Sie auf "Beispiel", um die Felder auszufüllen und die Berechnung zu starten.</p>
|
||||
<p class="mt-4 text-xs text-gray-500 italic">Click an example to populate the fields and run the calculation.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
|
||||
init() {
|
||||
// ─── Shared helpers ────────────────────────────────────────────────────────
|
||||
function ipToBinary(ip) {
|
||||
return ip.split('.').map(o => parseInt(o, 10).toString(2).padStart(8, '0')).join('');
|
||||
}
|
||||
function binaryToIp(b) {
|
||||
const parts = [];
|
||||
for (let i = 0; i < 32; i += 8) parts.push(parseInt(b.slice(i, i + 8), 2));
|
||||
return parts.join('.');
|
||||
}
|
||||
function cidrToMask(cidr) {
|
||||
return binaryToIp('1'.repeat(cidr) + '0'.repeat(32 - cidr));
|
||||
}
|
||||
function maskToCidr(mask) {
|
||||
const b = ipToBinary(mask);
|
||||
if (/^1*0*$/.test(b)) return b.replace(/0+$/, '').length;
|
||||
return null;
|
||||
}
|
||||
function isValidIP(ip) {
|
||||
return /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(ip);
|
||||
}
|
||||
function calcSubnet(ip, cidr) {
|
||||
const ipBin = ipToBinary(ip);
|
||||
const maskBin = '1'.repeat(cidr) + '0'.repeat(32 - cidr);
|
||||
let netBin = '';
|
||||
for (let i = 0; i < 32; i++) netBin += (parseInt(ipBin[i]) & parseInt(maskBin[i])).toString();
|
||||
const hostBits = 32 - cidr;
|
||||
const bcBin = netBin.slice(0, cidr) + '1'.repeat(hostBits);
|
||||
const netNum = parseInt(netBin, 2);
|
||||
const bcNum = parseInt(bcBin, 2);
|
||||
let hosts, first, last;
|
||||
if (hostBits >= 2) {
|
||||
hosts = Math.pow(2, hostBits) - 2;
|
||||
first = binaryToIp((netNum + 1).toString(2).padStart(32, '0'));
|
||||
last = binaryToIp((bcNum - 1).toString(2).padStart(32, '0'));
|
||||
} else if (cidr === 31) {
|
||||
hosts = 2; first = binaryToIp(netBin); last = binaryToIp(bcBin);
|
||||
} else {
|
||||
hosts = 1; first = binaryToIp(netBin); last = binaryToIp(netBin);
|
||||
}
|
||||
return { network: binaryToIp(netBin), broadcast: binaryToIp(bcBin), mask: cidrToMask(cidr), hosts, first, last, netBin, ipBin, maskBin, cidr, hostBits };
|
||||
}
|
||||
|
||||
// ─── Mode toggle ──────────────────────────────────────────────────────────
|
||||
const beginnerEl = document.getElementById('beginner-mode');
|
||||
const proEl = document.getElementById('pro-mode');
|
||||
const btnBeg = document.getElementById('btn-beginner');
|
||||
const btnPro = document.getElementById('btn-pro');
|
||||
const activeClass = 'px-6 py-2 rounded-lg text-sm font-semibold transition-all duration-200 bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg';
|
||||
const inactiveClass = 'px-6 py-2 rounded-lg text-sm font-semibold transition-all duration-200 text-gray-400 hover:text-gray-200';
|
||||
|
||||
function setMode(mode) {
|
||||
const isBeg = mode === 'beginner';
|
||||
beginnerEl.classList.toggle('hidden', !isBeg);
|
||||
proEl.classList.toggle('hidden', isBeg);
|
||||
btnBeg.className = isBeg ? activeClass : inactiveClass;
|
||||
btnPro.className = isBeg ? inactiveClass : activeClass;
|
||||
}
|
||||
btnBeg.addEventListener('click', () => setMode('beginner'));
|
||||
btnPro.addEventListener('click', () => setMode('pro'));
|
||||
|
||||
// ─── Beginner mode ────────────────────────────────────────────────────────
|
||||
const begIpInput = document.getElementById('beg-ip');
|
||||
const begSlider = document.getElementById('beg-slider');
|
||||
const begCidrLabel = document.getElementById('beg-cidr-label');
|
||||
const begBar = document.getElementById('beg-bar');
|
||||
const begHostsCount = document.getElementById('beg-hosts-count');
|
||||
const begMaskDisplay = document.getElementById('beg-mask-display');
|
||||
const begNetwork = document.getElementById('beg-network');
|
||||
const begFirst = document.getElementById('beg-first');
|
||||
const begLast = document.getElementById('beg-last');
|
||||
const begBroadcast = document.getElementById('beg-broadcast');
|
||||
const begExplain = document.getElementById('beg-explain-text');
|
||||
|
||||
function updateBeginner() {
|
||||
const ip = begIpInput.value.trim();
|
||||
const cidr = parseInt(begSlider.value, 10);
|
||||
|
||||
begCidrLabel.textContent = `/${cidr}`;
|
||||
|
||||
// bar: log scale via host-bit count
|
||||
const barPct = Math.max(3, ((32 - cidr) / 31) * 100);
|
||||
begBar.style.width = barPct + '%';
|
||||
|
||||
if (!isValidIP(ip)) return;
|
||||
|
||||
const r = calcSubnet(ip, cidr);
|
||||
|
||||
begHostsCount.textContent = r.hosts.toLocaleString('en');
|
||||
begMaskDisplay.textContent = r.mask;
|
||||
begNetwork.textContent = r.network;
|
||||
begFirst.textContent = r.first;
|
||||
begLast.textContent = r.last;
|
||||
begBroadcast.textContent = r.broadcast;
|
||||
|
||||
let sizeDesc;
|
||||
if (cidr <= 8) sizeDesc = 'This is a massive network — only found in large data centres or at internet service providers.';
|
||||
else if (cidr <= 12) sizeDesc = 'This is a very large network, typical for big enterprises or campus environments.';
|
||||
else if (cidr <= 16) sizeDesc = 'This is a large network, common in mid-sized companies or universities.';
|
||||
else if (cidr <= 20) sizeDesc = 'This is a medium-sized network, e.g. for a large office building or campus.';
|
||||
else if (cidr <= 24) sizeDesc = 'This is a typical home or small office network — the default on most routers.';
|
||||
else if (cidr <= 27) sizeDesc = 'This is a small network, e.g. for a single department or server cluster.';
|
||||
else sizeDesc = 'This is a very small network, usually used for direct point-to-point links.';
|
||||
|
||||
begExplain.innerHTML = `
|
||||
A <strong class="text-purple-300">/${cidr}</strong> network reserves
|
||||
<strong class="text-purple-300">${cidr} bits for the network address</strong> and leaves
|
||||
<strong class="text-purple-300">${r.hostBits} bits for hosts</strong>.
|
||||
That gives <strong class="text-green-400">${r.hosts.toLocaleString('en')} usable IP addresses</strong>
|
||||
(2<sup>${r.hostBits}</sup>−2, since the network ID and broadcast are reserved).
|
||||
<br><br>${sizeDesc}
|
||||
`;
|
||||
}
|
||||
|
||||
begSlider.addEventListener('input', updateBeginner);
|
||||
begIpInput.addEventListener('input', updateBeginner);
|
||||
|
||||
document.querySelectorAll('.beg-example').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
begIpInput.value = btn.dataset.ip;
|
||||
begSlider.value = btn.dataset.cidr;
|
||||
updateBeginner();
|
||||
});
|
||||
});
|
||||
|
||||
updateBeginner();
|
||||
|
||||
// ─── Expert mode ──────────────────────────────────────────────────────────
|
||||
const form = document.getElementById('subnet-form');
|
||||
const ipInput = document.getElementById('ip-address');
|
||||
const cidrInput = document.getElementById('cidr');
|
||||
@@ -117,28 +499,26 @@ export const page = {
|
||||
errorEl.classList.toggle('hidden', !msg);
|
||||
}
|
||||
|
||||
function isValidIP(ip) {
|
||||
return /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(ip);
|
||||
function coloredBits(bits, cidr, hostChar) {
|
||||
let html = '';
|
||||
for (let i = 0; i < 32; i++) {
|
||||
if (i > 0 && i % 8 === 0) html += '<span class="text-gray-600 select-none">.</span>';
|
||||
const isNet = i < cidr;
|
||||
const ch = hostChar && !isNet ? hostChar : bits[i];
|
||||
html += `<span class="${isNet ? 'text-purple-300' : 'text-gray-500'}">${ch}</span>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function ipToBinary(ip) {
|
||||
return ip.split('.').map(o => parseInt(o, 10).toString(2).padStart(8, '0')).join('');
|
||||
}
|
||||
|
||||
function binaryToIp(b) {
|
||||
const parts = [];
|
||||
for (let i = 0; i < 32; i += 8) parts.push(parseInt(b.slice(i, i + 8), 2));
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
function cidrToMask(cidr) {
|
||||
return binaryToIp('1'.repeat(cidr) + '0'.repeat(32 - cidr));
|
||||
}
|
||||
|
||||
function maskToCidr(mask) {
|
||||
const b = ipToBinary(mask);
|
||||
if (/^1*0*$/.test(b)) return b.replace(/0+$/, '').length;
|
||||
return null;
|
||||
function renderBinary(r) {
|
||||
const maskBits = '1'.repeat(r.cidr) + '0'.repeat(r.hostBits);
|
||||
document.getElementById('bin-ip').innerHTML = coloredBits(r.ipBin, r.cidr, null);
|
||||
document.getElementById('bin-mask').innerHTML = coloredBits(maskBits, r.cidr, null);
|
||||
document.getElementById('bin-net').innerHTML = coloredBits(r.netBin, r.cidr, 'x');
|
||||
document.getElementById('bin-legend').innerHTML =
|
||||
`<span class="text-purple-400">■</span> network bit (${r.cidr}) ` +
|
||||
`<span class="text-gray-600">□</span> host bit (${r.hostBits}) — ` +
|
||||
`x = any host address within this network`;
|
||||
}
|
||||
|
||||
function calculate() {
|
||||
@@ -146,48 +526,28 @@ export const page = {
|
||||
const ip = ipInput.value.trim();
|
||||
const cidrRaw = cidrInput.value.trim();
|
||||
|
||||
if (!isValidIP(ip)) { showInlineError('Bitte eine gültige IPv4-Adresse eingeben.'); return; }
|
||||
if (!isValidIP(ip)) { showInlineError('Please enter a valid IPv4 address.'); return; }
|
||||
|
||||
let cidr, mask;
|
||||
let cidr;
|
||||
if (cidrRaw.includes('.')) {
|
||||
if (!isValidIP(cidrRaw)) { showInlineError('Bitte eine gültige Subnetzmaske eingeben.'); return; }
|
||||
if (!isValidIP(cidrRaw)) { showInlineError('Please enter a valid subnet mask.'); return; }
|
||||
cidr = maskToCidr(cidrRaw);
|
||||
if (cidr === null) { showInlineError('Ungültige Subnetzmaske — muss eine kontinuierliche Folge von Einsen sein (z.B. 255.255.255.0).'); return; }
|
||||
mask = cidrRaw;
|
||||
if (cidr === null) { showInlineError('Invalid subnet mask — must be a contiguous sequence of ones (e.g. 255.255.255.0).'); return; }
|
||||
} else {
|
||||
cidr = parseInt(cidrRaw, 10);
|
||||
if (isNaN(cidr) || cidr < 0 || cidr > 32) { showInlineError('Bitte einen gültigen CIDR-Wert (0–32) eingeben.'); return; }
|
||||
mask = cidrToMask(cidr);
|
||||
if (isNaN(cidr) || cidr < 0 || cidr > 32) { showInlineError('Please enter a valid CIDR value (0–32).'); return; }
|
||||
}
|
||||
|
||||
const ipBin = ipToBinary(ip);
|
||||
const maskBin = '1'.repeat(cidr) + '0'.repeat(32 - cidr);
|
||||
let netBin = '';
|
||||
for (let i = 0; i < 32; i++) netBin += (parseInt(ipBin[i]) & parseInt(maskBin[i])).toString();
|
||||
const r = calcSubnet(ip, cidr);
|
||||
|
||||
const hostBits = 32 - cidr;
|
||||
const bcBin = netBin.slice(0, cidr) + '1'.repeat(hostBits);
|
||||
const netNum = parseInt(netBin, 2);
|
||||
const bcNum = parseInt(bcBin, 2);
|
||||
|
||||
let hosts, first, last;
|
||||
if (hostBits >= 2) {
|
||||
hosts = Math.pow(2, hostBits) - 2;
|
||||
first = binaryToIp((netNum + 1).toString(2).padStart(32, '0'));
|
||||
last = binaryToIp((bcNum - 1).toString(2).padStart(32, '0'));
|
||||
} else if (cidr === 31) {
|
||||
hosts = 2; first = binaryToIp(netBin); last = binaryToIp(bcBin);
|
||||
} else {
|
||||
hosts = 1; first = binaryToIp(netBin); last = binaryToIp(netBin);
|
||||
}
|
||||
|
||||
document.getElementById('network-address').textContent = binaryToIp(netBin);
|
||||
document.getElementById('broadcast-address').textContent = binaryToIp(bcBin);
|
||||
document.getElementById('subnet-mask').textContent = mask;
|
||||
document.getElementById('host-count').textContent = hosts.toLocaleString();
|
||||
document.getElementById('first-host').textContent = first;
|
||||
document.getElementById('last-host').textContent = last;
|
||||
document.getElementById('network-address').textContent = r.network;
|
||||
document.getElementById('broadcast-address').textContent = r.broadcast;
|
||||
document.getElementById('subnet-mask').textContent = r.mask;
|
||||
document.getElementById('host-count').textContent = r.hosts.toLocaleString('en');
|
||||
document.getElementById('first-host').textContent = r.first;
|
||||
document.getElementById('last-host').textContent = r.last;
|
||||
|
||||
renderBinary(r);
|
||||
resultsEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ async function navigate(path, { push = true, search = '' } = {}) {
|
||||
|
||||
// ── Intercept same-origin link clicks ───────────────────────────
|
||||
document.addEventListener('click', e => {
|
||||
if (e.defaultPrevented) return;
|
||||
const a = e.target.closest('a[href]');
|
||||
if (!a) return;
|
||||
let url;
|
||||
|
||||
+41
-27
@@ -62,13 +62,16 @@
|
||||
color: #e5e7eb;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
max-height: 500px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* ── Tool action buttons (Ping / Traceroute / Port Scan) ──────── */
|
||||
.action-tool-btn { text-align: center; }
|
||||
|
||||
/* ── Scrollbar ─────────────────────────────────────────────────── */
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-track { background: rgba(31, 41, 55, 0.5); }
|
||||
@@ -230,34 +233,45 @@ header.nav-open #main-nav { display: block; }
|
||||
#ip-address-link:hover::after { transform: scaleX(1); transform-origin: bottom left; }
|
||||
|
||||
/* ── Home page — Traceroute output ─────────────────────────────── */
|
||||
#traceroute-output pre, .result-pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
background-color: rgba(0,0,0,.3);
|
||||
color: #e5e7eb;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.875rem;
|
||||
border: 1px solid rgba(255,255,255,.05);
|
||||
box-shadow: inset 0 2px 4px 0 rgba(0,0,0,.3);
|
||||
/* Hop rows — structured grid layout with RDNS on its own line */
|
||||
#traceroute-output .hop-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2rem 1fr;
|
||||
align-items: start;
|
||||
gap: 0 0.5rem;
|
||||
padding: 3px 4px;
|
||||
border-radius: 4px;
|
||||
border-left: 2px solid transparent;
|
||||
transition: border-left-color .2s, background .2s;
|
||||
}
|
||||
#traceroute-output .hop-line { margin-bottom: .25rem; padding-left: .5rem; border-left: 2px solid transparent; transition: border-left-color .3s; }
|
||||
#traceroute-output .hop-line:hover { border-left-color: #a855f7; background: rgba(255,255,255,.02); }
|
||||
#traceroute-output .hop-number { display: inline-block; width: 30px; text-align: right; margin-right: 15px; color: #6b7280; font-weight: bold; }
|
||||
#traceroute-output .hop-ip { color: #60a5fa; font-weight: 500; }
|
||||
#traceroute-output .hop-hostname { color: #c084fc; }
|
||||
#traceroute-output .hop-rtt { color: #34d399; margin-left: 8px; font-size: .85em; }
|
||||
#traceroute-output .hop-timeout { color: #f87171; }
|
||||
#traceroute-output .info-line { color: #fbbf24; font-style: italic; }
|
||||
#traceroute-output .error-line { color: #f87171; font-weight: bold; border-left: 3px solid #f87171; padding-left: 10px; }
|
||||
#traceroute-output .end-line { color: #d8b4fe; font-weight: bold; margin-top: 15px; text-transform: uppercase; letter-spacing: .05em; border-top: 1px solid rgba(255,255,255,.1); padding-top: 10px; }
|
||||
#traceroute-output .hop-row:hover { border-left-color: #a855f7; background: rgba(255,255,255,.025); }
|
||||
#traceroute-output .hop-number { text-align: right; color: #4b5563; font-weight: 700; font-size: .8em; padding-top: 2px; }
|
||||
#traceroute-output .hop-body { min-width: 0; }
|
||||
#traceroute-output .hop-ip-line { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
|
||||
#traceroute-output .hop-ip { color: #60a5fa; font-weight: 500; flex-shrink: 0; }
|
||||
#traceroute-output .hop-rtts { display: flex; gap: 0.4rem; margin-left: auto; }
|
||||
#traceroute-output .hop-rtt { color: #34d399; font-size: .8em; }
|
||||
#traceroute-output .hop-timeout { color: #f87171; font-size: .85em; }
|
||||
/* RDNS hostname on its own line — clearly distinguishable from the IP */
|
||||
#traceroute-output .hop-rdns {
|
||||
font-size: .75em;
|
||||
color: #c084fc;
|
||||
opacity: .85;
|
||||
margin-top: 1px;
|
||||
padding-left: 1px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
/* Non-hop lines (info, error, end) */
|
||||
#traceroute-output .info-line { color: #fbbf24; font-style: italic; font-size: .85em; }
|
||||
#traceroute-output .error-line { color: #f87171; font-weight: bold; border-left: 3px solid #f87171; padding-left: 8px; }
|
||||
#traceroute-output .end-line { color: #d8b4fe; font-weight: bold; margin-top: 8px; text-transform: uppercase; letter-spacing: .05em; border-top: 1px solid rgba(255,255,255,.08); padding-top: 8px; }
|
||||
|
||||
/* ── Home page — Maps ───────────────────────────────────────────── */
|
||||
#map { height: 300px; }
|
||||
#lookup-map { height: 250px; }
|
||||
/* Containers use h-[420px] / h-[260px] in HTML; maps fill 100% via ID selector (higher specificity than Tailwind) */
|
||||
#map { height: 420px; }
|
||||
#lookup-map { height: 260px; }
|
||||
|
||||
/* ── ASN page — Graph ───────────────────────────────────────────── */
|
||||
#graph-container { width: 100%; height: 600px; background: rgba(0,0,0,.3); border-radius: .75rem; border: 1px solid rgba(255,255,255,.06); overflow: hidden; position: relative; }
|
||||
|
||||
Reference in New Issue
Block a user