Compare commits

154 Commits

Author SHA1 Message Date
github-actions[bot] 07bc5ffd9f Update MaxMind GeoLite2 databases (LFS) (2026-04-01) 2026-04-01 01:26:00 +00:00
MrUnknownDE b78b19b58b add asn info grid 2026-03-05 21:04:57 +01:00
MrUnknownDE 636cd9a1e4 fix limit on neighbours 2026-03-05 21:00:24 +01:00
MrUnknownDE 8bcc6270ca fix: Null Issue on response 2026-03-05 20:52:35 +01:00
MrUnknownDE 828103be06 add Sentry Logs 2026-03-05 20:52:15 +01:00
MrUnknownDE a606d8e649 update sentry plugin 2026-03-05 20:38:04 +01:00
MrUnknownDE 9fdd7d58a9 fix: display issue 2026-03-05 20:33:27 +01:00
MrUnknownDE b226b81775 set cache to 7 days 2026-03-05 20:20:31 +01:00
MrUnknownDE f21b06e6ad fix: add asn-cache on filesystem 2026-03-05 20:09:53 +01:00
MrUnknownDE 6552d198cf Merge branch 'main' of github.com:MrUnknownDE/utools 2026-03-05 19:57:17 +01:00
MrUnknownDE 564596e06a feat: add ASN Function 2026-03-05 19:57:14 +01:00
MrUnknownDE 2d25dfc262 update qs and underscore 2026-03-05 19:44:54 +01:00
github-actions[bot] 49708b7b58 Update MaxMind GeoLite2 databases (LFS) (2026-03-05) 2026-03-05 18:35:15 +00:00
MrUnknownDE d119ecf4a2 fix CodeQL-Alerts 2026-03-05 19:32:01 +01:00
github-actions[bot] b3c7a7bef3 Update MaxMind GeoLite2 databases (LFS) (2026-03-01) 2026-03-01 01:16:33 +00:00
github-actions[bot] aed28b982a Update MaxMind GeoLite2 databases (LFS) (2026-02-01) 2026-02-01 01:17:59 +00:00
MrUnknownDE 080fed1008 fix: remove .html links 2026-01-14 17:12:31 +01:00
MrUnknownDE 33b7d5dffc Merge branch 'main' of github.com:MrUnknownDE/utools 2026-01-14 17:10:57 +01:00
github-actions[bot] 6a47880288 Update MaxMind GeoLite2 databases (LFS) (2026-01-14) 2026-01-14 16:09:59 +00:00
MrUnknownDE d584e11453 feat: add shareable links 2026-01-14 17:08:15 +01:00
MrUnknownDE cde424e881 feat: Add MAC address vendor lookup API endpoint and dependencies 2026-01-02 18:14:53 +01:00
MrUnknownDE eec2ed1adb feat: Add new API endpoint for MAC address vendor lookup. 2026-01-02 18:08:07 +01:00
MrUnknownDE 7f3566888d chore: update README.md 2026-01-02 18:03:10 +01:00
MrUnknownDE a0ea88b2dd feat: add MAC address lookup API endpoint and backend dependencies 2026-01-02 18:00:00 +01:00
MrUnknownDE a7d8654d3c feat: Add initial frontend logic for network utilities (IP info, lookup, traceroute, port scan) and a backend route for MAC lookup. 2026-01-02 17:56:31 +01:00
MrUnknownDE ff0fd1098b fix: switch to nodejs 24 2026-01-02 17:51:56 +01:00
MrUnknownDE 29fd909340 Fix SSRF vulnerability in utils.js 2026-01-02 17:49:19 +01:00
MrUnknownDE a7d189d89d feat: update style 2026-01-02 17:45:41 +01:00
MrUnknownDE fdc753b32f fix: update dependencies 2026-01-02 17:38:36 +01:00
MrUnknownDE 7a3b159105 feat: add example environment variables for backend configuration 2026-01-02 17:28:22 +01:00
MrUnknownDE 652010a92f feat: Add initial backend server with various utility APIs, Sentry, logging, rate limiting, and a multi-arch Docker build workflow. 2026-01-02 17:26:09 +01:00
github-actions[bot] e5902e9747 Update MaxMind GeoLite2 databases (LFS) (2026-01-01) 2026-01-01 01:02:49 +00:00
github-actions[bot] 068a8cd472 Update MaxMind GeoLite2 databases (LFS) (2025-12-18) 2025-12-18 17:03:22 +00:00
MrUnknownDE eb3f43953a chore: add github lfs 2025-12-18 18:02:34 +01:00
MrUnknownDE ac6ec7e535 chore: change to github lfs 2025-12-18 18:01:58 +01:00
github-actions[bot] db806fd06c Update MaxMind GeoLite2 databases (2025-12-18) 2025-12-18 16:58:36 +00:00
MrUnknownDE f7fad027db change to monthly updates 2025-12-03 20:56:13 +01:00
github-actions[bot] b74dcbff38 Update MaxMind GeoLite2 databases (2025-12-02) 2025-12-02 05:18:57 +00:00
github-actions[bot] 0ce8eec6e5 Update MaxMind GeoLite2 databases (2025-11-25) 2025-11-25 05:19:34 +00:00
github-actions[bot] 2ce5916fb3 Update MaxMind GeoLite2 databases (2025-11-22) 2025-11-22 19:49:44 +00:00
github-actions[bot] fab48185c8 Update MaxMind GeoLite2 databases (2025-11-18) 2025-11-18 05:18:28 +00:00
github-actions[bot] 9597644607 Update MaxMind GeoLite2 databases (2025-11-11) 2025-11-11 05:18:51 +00:00
github-actions[bot] 6daa4ef4bc Update MaxMind GeoLite2 databases (2025-11-04) 2025-11-04 05:18:32 +00:00
MrUnknownDE 26c22f907b Verbesserung der Docker-Workflow-Konfiguration und Aktualisierung der Image-Referenzen 2025-11-01 18:17:33 +01:00
MrUnknownDE e354d8aabd edit footer 2025-11-01 18:14:31 +01:00
MrUnknownDE 26137c4ed0 [Workflow] Verbesserung der Tag-Definition für Multi-Arch-Images 2025-11-01 18:13:12 +01:00
MrUnknownDE e3ae926043 move to DockerHUB + manuell trigger 2025-11-01 18:10:15 +01:00
github-actions[bot] dce367ce78 Update MaxMind GeoLite2 databases (2025-10-28) 2025-10-28 05:18:41 +00:00
github-actions[bot] 9b4e2552dd Update MaxMind GeoLite2 databases (2025-10-21) 2025-10-21 05:17:48 +00:00
github-actions[bot] 2f3241e36a Update MaxMind GeoLite2 databases (2025-10-14) 2025-10-14 05:16:50 +00:00
github-actions[bot] dff70e2b07 Update MaxMind GeoLite2 databases (2025-10-07) 2025-10-07 05:16:45 +00:00
github-actions[bot] 21e9c62441 Update MaxMind GeoLite2 databases (2025-09-30) 2025-09-30 05:17:12 +00:00
MrUnknownDE 8962cfadd9 update nav bar 2025-09-23 20:06:54 +02:00
MrUnknownDE bb7fa35496 fix sentry error and mac-vendor utils issue 2025-09-23 20:01:19 +02:00
MrUnknownDE dfdfbbdf68 try again mac-lookup 2025-09-23 19:54:27 +02:00
MrUnknownDE f21da6b888 add Port-Scanner 2025-09-23 19:47:51 +02:00
MrUnknownDE eabd59e945 add token secret 2025-09-23 19:38:20 +02:00
MrUnknownDE 93132c256d try to push a docker image :D 2025-09-23 19:33:38 +02:00
MrUnknownDE 39a6c3dd8b fix docker image lowcase naming 2025-09-23 19:29:41 +02:00
MrUnknownDE b93b91d352 seperate docker-build compose 2025-09-23 19:19:28 +02:00
MrUnknownDE 9693238eb0 Update index.html 2025-09-23 19:10:51 +02:00
github-actions[bot] e465887edc Update MaxMind GeoLite2 databases (2025-09-23) 2025-09-23 05:17:13 +00:00
github-actions[bot] ac63f5e5bb Update MaxMind GeoLite2 databases (2025-09-16) 2025-09-16 05:16:58 +00:00
github-actions[bot] 8ad92755ed Update MaxMind GeoLite2 databases (2025-09-09) 2025-09-09 05:16:42 +00:00
github-actions[bot] 6d3d7f4efb Update MaxMind GeoLite2 databases (2025-09-02) 2025-09-02 05:18:09 +00:00
github-actions[bot] 9804a68ea5 Update MaxMind GeoLite2 databases (2025-08-26) 2025-08-26 05:18:15 +00:00
github-actions[bot] 2730ebb174 Update MaxMind GeoLite2 databases (2025-08-19) 2025-08-19 05:18:06 +00:00
github-actions[bot] e4221f2e8f Update MaxMind GeoLite2 databases (2025-08-12) 2025-08-12 05:18:56 +00:00
github-actions[bot] 5c9df4e6b1 Update MaxMind GeoLite2 databases (2025-08-05) 2025-08-05 05:28:01 +00:00
github-actions[bot] a1753f4e85 Update MaxMind GeoLite2 databases (2025-07-29) 2025-07-29 05:27:53 +00:00
github-actions[bot] c9846b0d88 Update MaxMind GeoLite2 databases (2025-07-22) 2025-07-22 05:23:59 +00:00
github-actions[bot] b7c98e7f4d Update MaxMind GeoLite2 databases (2025-07-15) 2025-07-15 05:23:24 +00:00
github-actions[bot] ea4c74747d Update MaxMind GeoLite2 databases (2025-07-08) 2025-07-08 05:19:31 +00:00
github-actions[bot] aad232e6f4 Update MaxMind GeoLite2 databases (2025-07-01) 2025-07-01 05:22:02 +00:00
github-actions[bot] c6cd5c59ea Update MaxMind GeoLite2 databases (2025-06-24) 2025-06-24 05:20:00 +00:00
github-actions[bot] 9c493cc01e Update MaxMind GeoLite2 databases (2025-06-17) 2025-06-17 05:18:21 +00:00
github-actions[bot] 104ebd4f83 Update MaxMind GeoLite2 databases (2025-06-10) 2025-06-10 05:18:06 +00:00
github-actions[bot] d444b0f46b Update MaxMind GeoLite2 databases (2025-06-03) 2025-06-03 05:18:49 +00:00
github-actions[bot] 7d382bf18e Update MaxMind GeoLite2 databases (2025-05-27) 2025-05-27 05:17:59 +00:00
github-actions[bot] 6ed691e465 Update MaxMind GeoLite2 databases (2025-05-20) 2025-05-20 05:18:04 +00:00
github-actions[bot] 2e7cc5f2c9 Update MaxMind GeoLite2 databases (2025-05-13) 2025-05-13 05:17:52 +00:00
github-actions[bot] 940f5ff8fb Update MaxMind GeoLite2 databases (2025-05-06) 2025-05-06 05:18:22 +00:00
github-actions[bot] 44d09e09c1 Update MaxMind GeoLite2 databases (2025-04-29) 2025-04-29 05:17:57 +00:00
MrUnknownDE 8c3e72a632 change live-preview url 2025-04-25 20:44:15 +02:00
github-actions[bot] 0a9d43da13 Update MaxMind GeoLite2 databases (2025-04-22) 2025-04-22 05:17:44 +00:00
github-actions[bot] bc27677967 Update MaxMind GeoLite2 databases (2025-04-15) 2025-04-15 05:17:40 +00:00
github-actions[bot] 9e969b5745 Update MaxMind GeoLite2 databases (2025-04-08) 2025-04-08 05:17:16 +00:00
github-actions[bot] 5241de94b6 Update MaxMind GeoLite2 databases (2025-04-01) 2025-04-01 05:17:29 +00:00
MrUnknownDE 6b931cf99c Create CODE_OF_CONDUCT.md 2025-03-29 19:36:36 +01:00
MrUnknownDE ac5b1abd7b Create CONTRIBUTING.md 2025-03-29 19:35:56 +01:00
MrUnknownDE e95770bdce edit some sentry in traceroute 2025-03-29 18:45:34 +01:00
MrUnknownDE bab3b59750 Sentry.startTransaction 2025-03-29 18:42:41 +01:00
MrUnknownDE 1c6802995f fix traceroute module 2025-03-29 18:40:08 +01:00
MrUnknownDE 7f11612aa6 edit title 2025-03-29 18:39:59 +01:00
MrUnknownDE aa38eddc07 fix geoip on ip-adress-lookup 2025-03-29 18:38:12 +01:00
MrUnknownDE 2498d10f6d edit proxy 2025-03-29 18:36:27 +01:00
MrUnknownDE b9cfe43986 seperate server.js 2025-03-29 18:34:12 +01:00
MrUnknownDE 0da70547aa fix Broadcast-Adresse and last ip-adresse 2025-03-29 18:16:41 +01:00
MrUnknownDE 6dfc86ce48 add debug-mode 2025-03-29 18:13:07 +01:00
MrUnknownDE be3aba6d86 fix: subnetz-calculator doesn't work 2025-03-29 18:09:53 +01:00
MrUnknownDE 3c1b5156a9 update README.md 2025-03-29 18:04:42 +01:00
MrUnknownDE 12ad5a86be remove mac-lookup from the menu 2025-03-29 18:02:45 +01:00
MrUnknownDE 8e2caf1e73 .. 2025-03-29 18:00:19 +01:00
MrUnknownDE 85c03bc483 . 2025-03-29 17:57:50 +01:00
MrUnknownDE 3cd55bb7a9 add sentry/profiling-node 2025-03-29 17:56:44 +01:00
MrUnknownDE 75614e7cd2 ProfilingIntegration fix 2025-03-29 17:46:20 +01:00
MrUnknownDE c0ddd1cf7d fix Sentry.Init 2025-03-29 17:43:51 +01:00
MrUnknownDE 93a4574553 fix sentry url 2025-03-29 17:41:09 +01:00
MrUnknownDE 0fa3e6ac7c enable sentry logging 2025-03-29 17:38:52 +01:00
MrUnknownDE 5fd7d49602 remove mac-lookup 2025-03-29 17:32:23 +01:00
MrUnknownDE cb4adabbc2 ai try oui install ^^ 2025-03-29 17:22:13 +01:00
MrUnknownDE 47efadd6d6 add oui install on dockerfile 2025-03-29 17:19:10 +01:00
MrUnknownDE 0e153db2c4 edit build-"pipeline" 2025-03-29 17:18:02 +01:00
MrUnknownDE 95b83f060b try again oui ^^ 2025-03-29 17:17:35 +01:00
MrUnknownDE 33e8400d51 fix ismacvalid func 2025-03-29 17:12:44 +01:00
MrUnknownDE 98796edcbb ip-lookup: resolv dns 2025-03-29 17:07:49 +01:00
MrUnknownDE 320606ca3f add clickable ips 2025-03-29 17:03:13 +01:00
MrUnknownDE 4d00fd02cf edit path in dockerfile from frontend 2025-03-29 16:55:39 +01:00
MrUnknownDE 168618c4fb back to macaddress xd 2025-03-29 16:52:50 +01:00
MrUnknownDE e6ee946cc3 switch mac-lookup lib to oui 2025-03-29 16:46:21 +01:00
MrUnknownDE 2207a0d325 add dns, mac and domain whois lookup 2025-03-29 16:40:31 +01:00
MrUnknownDE 12370ea173 fix examples 2025-03-29 16:20:16 +01:00
MrUnknownDE 06e4971f1f add adress examples 2025-03-29 16:16:07 +01:00
MrUnknownDE 84611f36f5 start subnetz-calculator 2025-03-29 16:14:12 +01:00
MrUnknownDE f36e52a2fe add build.sh 2025-03-29 15:38:29 +01:00
MrUnknownDE 12bb9a157e add version on footer 2025-03-29 15:32:34 +01:00
MrUnknownDE d5b18295c0 set trust proxy to 2, because my env need this ^^ 2025-03-29 15:25:15 +01:00
MrUnknownDE 039fda5797 fix geo-check on frontend 2025-03-29 15:19:49 +01:00
MrUnknownDE 4f8718c18a edit docker network 2025-03-29 13:46:42 +01:00
MrUnknownDE 5d415a597b activate trust proxy 2025-03-29 13:42:04 +01:00
MrUnknownDE b98f9047ef Update README.md 2025-03-29 13:39:36 +01:00
MrUnknownDE 57c671e17b add livedemo-url and MaxMind Geo Pipeline Badge 2025-03-29 13:19:24 +01:00
MrUnknownDE 815cc48e9b add weekly maxmind update for repo 2025-03-29 13:14:41 +01:00
MrUnknownDE 2f070acaf4 beautify ReadME.md 2025-03-29 13:10:18 +01:00
MrUnknownDE c59341552f remove public port of backend 2025-03-29 13:05:09 +01:00
MrUnknownDE 7c38595984 remove docker push on every push 2025-03-29 13:02:19 +01:00
MrUnknownDE e560c8358c add compose.yml an README.md 2025-03-29 13:01:57 +01:00
MrUnknownDE 6dae7c356f add docker image build pipeline 2025-03-29 12:54:14 +01:00
MrUnknownDE bee37d3eda add dns to backend-container 2025-03-29 12:46:47 +01:00
MrUnknownDE 02079ff639 remove notes 2025-03-29 12:43:52 +01:00
MrUnknownDE c399ea9d26 remove volumes 2025-03-29 12:42:56 +01:00
MrUnknownDE 45e012c817 frontend2docker :P 2025-03-29 12:41:58 +01:00
MrUnknownDE 99eb24f665 start with frontend 2025-03-29 12:35:37 +01:00
MrUnknownDE c1d9e41d43 add lookup endpoint 2025-03-29 12:31:38 +01:00
MrUnknownDE 4535631e9f make docker grate again ^^ 2025-03-29 12:01:58 +01:00
MrUnknownDE 654df54fa7 add private ip check 2025-03-29 11:56:16 +01:00
MrUnknownDE 06862e2023 remove ip-adress lib 2025-03-29 11:54:25 +01:00
MrUnknownDE f9daddc122 replace ip-adress lib with net. 2025-03-29 11:50:35 +01:00
MrUnknownDE 0a2163ff38 add more debugging things 2025-03-29 11:41:49 +01:00
MrUnknownDE b5c1908b04 add debug in traceroute endpoint 2025-03-29 11:35:01 +01:00
MrUnknownDE 0e52a0cd06 add debug in ping endpoint 2025-03-29 11:34:28 +01:00
MrUnknownDE 5266b05ce3 fix "Incorrect number of groups found" in func isValidIp 2025-03-29 11:33:25 +01:00
MrUnknownDE b001ca6d29 start with a backend 2025-03-29 11:26:48 +01:00
MrUnknownDE 7eac8b00ae repo is started from the beginning 2025-03-29 11:20:20 +01:00
53 changed files with 9372 additions and 359 deletions
+1
View File
@@ -0,0 +1 @@
backend/data/*.mmdb filter=lfs diff=lfs merge=lfs -text
+89
View File
@@ -0,0 +1,89 @@
name: Docker Build and Push (Docker Hub, Multi-Arch)
on:
push:
branches:
- main
workflow_dispatch:
inputs:
extra_tag:
description: "Optionaler Zusatz-Tag (z.B. v1.2.3). Kommt zusätzlich zu :latest und :<sha>."
required: false
default: ""
jobs:
build-and-push:
runs-on: ubuntu-latest
env:
REGISTRY: docker.io
DOCKERHUB_USER_LC: ${{ secrets.DOCKERHUB_USERNAME }}
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true # WICHTIG: Lädt die echten LFS-Dateien (MaxMind DBs) herunter
- name: Get short SHA
id: vars
run: echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
# -------- BACKEND --------
- name: Build & Push backend (multi-arch)
uses: docker/build-push-action@v6
with:
context: ./backend
file: ./backend/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-backend:latest
${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-backend:${{ steps.vars.outputs.sha }}
build-args: |
GIT_COMMIT_SHA=${{ steps.vars.outputs.sha }}
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
- name: Tag backend with extra_tag (manifest retag)
if: ${{ github.event.inputs.extra_tag && github.event.inputs.extra_tag != '' }}
run: |
docker buildx imagetools create \
-t ${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-backend:${{ github.event.inputs.extra_tag }} \
${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-backend:${{ steps.vars.outputs.sha }}
# -------- FRONTEND --------
- name: Build & Push frontend (multi-arch)
uses: docker/build-push-action@v6
with:
context: ./frontend
file: ./frontend/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-frontend:latest
${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-frontend:${{ steps.vars.outputs.sha }}
build-args: |
GIT_COMMIT_SHA=${{ steps.vars.outputs.sha }}
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
- name: Tag frontend with extra_tag (manifest retag)
if: ${{ github.event.inputs.extra_tag && github.event.inputs.extra_tag != '' }}
run: |
docker buildx imagetools create \
-t ${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-frontend:${{ github.event.inputs.extra_tag }} \
${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-frontend:${{ steps.vars.outputs.sha }}
+86
View File
@@ -0,0 +1,86 @@
name: Update MaxMind GeoLite2 DBs
on:
workflow_dispatch: # Ermöglicht manuelles Starten
schedule:
- cron: '0 0 1 * *'
jobs:
update-db:
runs-on: ubuntu-latest
# Berechtigung, um Änderungen zurück ins Repo zu pushen
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: true # Wichtig: LFS-Dateien beim Checkout herunterladen
- name: Download geoipupdate tool
run: |
# Lade eine spezifische Version oder die neueste herunter
GEOIPUPDATE_VERSION="4.11.1"
wget "https://github.com/maxmind/geoipupdate/releases/download/v${GEOIPUPDATE_VERSION}/geoipupdate_${GEOIPUPDATE_VERSION}_linux_amd64.tar.gz"
tar -zxvf "geoipupdate_${GEOIPUPDATE_VERSION}_linux_amd64.tar.gz"
# Verschiebe das Binary in einen bekannten Pfad und mache es ausführbar
sudo mv "geoipupdate_${GEOIPUPDATE_VERSION}_linux_amd64/geoipupdate" /usr/local/bin/
sudo chmod +x /usr/local/bin/geoipupdate
# Überprüfe die Version
geoipupdate -V
- name: Create GeoIP.conf
# Erstellt die Konfigurationsdatei für geoipupdate mit den Secrets
run: |
echo "Creating GeoIP.conf..."
cat << EOF > GeoIP.conf
# GeoIP.conf file for geoipupdate
AccountID ${{ secrets.MAXMIND_ACCOUNT_ID }}
LicenseKey ${{ secrets.MAXMIND_LICENSE_KEY }}
# Specify the editions to download
EditionIDs GeoLite2-ASN GeoLite2-City
EOF
echo "GeoIP.conf created."
env:
MAXMIND_ACCOUNT_ID: ${{ secrets.MAXMIND_ACCOUNT_ID }}
MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }}
- name: Run geoipupdate
run: |
echo "Running geoipupdate..."
# Lädt die Datenbanken nach ./backend/data herunter
geoipupdate -f GeoIP.conf -d ./backend/data -v
echo "geoipupdate finished."
- name: Configure Git and LFS
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
# Installiere Git LFS Hooks für diesen Run
git lfs install
- name: Track and Commit changes (LFS)
run: |
# Sage Git, dass .mmdb Dateien mit LFS verwaltet werden sollen
git lfs track "backend/data/*.mmdb"
# Füge .gitattributes (hier steht die LFS-Konfiguration drin) hinzu
git add .gitattributes
# Füge die eigentlichen Datenbank-Dateien hinzu
git add ./backend/data/*.mmdb
# Prüfe, ob Änderungen zum Committen vorliegen (staged changes)
if git diff --staged --quiet; then
echo "No changes detected in MaxMind databases."
else
echo "Changes detected. Committing..."
COMMIT_DATE=$(date -u +"%Y-%m-%d")
git commit -m "Update MaxMind GeoLite2 databases (LFS) (${COMMIT_DATE})"
git push
echo "Changes pushed via LFS."
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+144
View File
@@ -0,0 +1,144 @@
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
# End of https://www.toptal.com/developers/gitignore/api/node
+69
View File
@@ -0,0 +1,69 @@
# Code of Conduct - uTools
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behaviour that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologising to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behaviour include:
* The use of sexualised language or imagery, and sexual attention or advances
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying and enforcing our standards of
acceptable behaviour and will take appropriate and fair corrective action in
response to any instances of unacceptable behaviour.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, or to ban
temporarily or permanently any contributor for other behaviours that they deem
inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behaviour may be
reported to the community leaders responsible for enforcement at .
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org/), version
[1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.md) and
[2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct/code_of_conduct.md),
and was generated by [contributing.md](https://contributing.md/generator).
+159
View File
@@ -0,0 +1,159 @@
<!-- omit in toc -->
# Contributing to uTools
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> - Star the project
> - Tweet about it
> - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues
<!-- omit in toc -->
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
- [Improving The Documentation](#improving-the-documentation)
- [Styleguides](#styleguides)
- [Commit Messages](#commit-messages)
- [Join The Project Team](#join-the-project-team)
## Code of Conduct
This project and everyone participating in it is governed by the
[uTools Code of Conduct](https://github.com/MrUnknownDE/utools/blob/main/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable behavior
to .
## I Have a Question
> If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/MrUnknownDE/utools).
Before you ask a question, it is best to search for existing [Issues](https://github.com/MrUnknownDE/utools/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an [Issue](https://github.com/MrUnknownDE/utools/issues/new).
- Provide as much context as you can about what you're running into.
- Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant.
We will then take care of the issue as soon as possible.
<!--
You might want to create a separate issue tag for questions and include it in this description. People should then tag their issues accordingly.
Depending on how large the project is, you may want to outsource the questioning, e.g. to Stack Overflow or Gitter. You may add additional contact and information possibilities:
- IRC
- Slack
- Gitter
- Stack Overflow tag
- Blog
- FAQ
- Roadmap
- E-Mail List
- Forum
-->
## I Want To Contribute
> ### Legal Notice <!-- omit in toc -->
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project licence.
### Reporting Bugs
<!-- omit in toc -->
#### Before Submitting a Bug Report
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/MrUnknownDE/utools). If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/MrUnknownDE/utools/issues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
<!-- omit in toc -->
#### How Do I Submit a Good Bug Report?
> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to .
<!-- You may add a PGP key to allow the messages to be sent encrypted as well. -->
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](https://github.com/MrUnknownDE/utools/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
- Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section.
Once it's filed:
- The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced.
- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution).
<!-- You might want to create an issue template for bugs and errors that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion for uTools, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
<!-- omit in toc -->
#### Before Submitting an Enhancement
- Make sure that you are using the latest version.
- Read the [documentation](https://github.com/MrUnknownDE/utools) carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](https://github.com/MrUnknownDE/utools/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
<!-- omit in toc -->
#### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](https://github.com/MrUnknownDE/utools/issues).
- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- You may want to **include screenshots or screen recordings** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [LICEcap](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and the built-in [screen recorder in GNOME](https://help.gnome.org/users/gnome-help/stable/screen-shot-record.html.en) or [SimpleScreenRecorder](https://github.com/MaartenBaert/ssr) on Linux. <!-- this should only be included if the project has a GUI -->
- **Explain why this enhancement would be useful** to most uTools users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
<!-- You might want to create an issue template for enhancement suggestions that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->
### Your First Code Contribution
<!-- TODO
include Setup of env, IDE and typical getting started instructions?
-->
### Improving The Documentation
<!-- TODO
Updating, improving and correcting the documentation
-->
## Styleguides
### Commit Messages
<!-- TODO
-->
## Join The Project Team
<!-- TODO -->
<!-- omit in toc -->
## Attribution
This guide is based on the [contributing.md](https://contributing.md/generator)!
+95 -2
View File
@@ -1,3 +1,96 @@
# utools
# utools - IP Information & Diagnostics Webapp ✨
Ich versuche mal was :)
[![Build Status](https://github.com/mrunknownde/utools/actions/workflows/docker-build-push.yml/badge.svg)](https://github.com/mrunknownde/utools/actions/workflows/docker-build-push.yml)
[![Update MaxMind GeoLite2 DBs](https://github.com/MrUnknownDE/utools/actions/workflows/maxmind-update.yml/badge.svg)](https://github.com/MrUnknownDE/utools/actions/workflows/maxmind-update.yml)
A modern web application that displays detailed information about a client's IP address, including geolocation, ASN, rDNS, and provides network diagnostic tools like Ping, Traceroute, DNS Lookup, Subnet Calculation, and WHOIS Lookup. It also allows looking up information for any public IP address.
### Preview: https://utools.mrunk.de
## 🚀 Features
* **Client IP Info:** Automatically detects and displays the visitor's public IP address.
* **Geolocation:** Shows Country, Region, City, Postal Code, Coordinates, and Timezone based on the IP.
* **ASN Information:** Displays the Autonomous System Number (ASN) and organization name.
* **Reverse DNS (rDNS):** Performs a reverse DNS lookup for the IP address.
* **Interactive Dark Mode Map:** Visualizes the geolocation on a dark-themed OpenStreetMap/CartoDB map.
* **Glassmorphism UI:** Features a premium, modern transparent design with animated gradients.
* **IP Lookup:** Allows users to enter any public IP address to retrieve its Geo, ASN, and rDNS information.
* **Traceroute:** Initiates a server-side traceroute (via SSE stream) to the client's IP or a looked-up IP.
* **Ping:** Performs a server-side ping test to a looked-up IP.
* **DNS Lookup:** Performs various DNS record lookups (A, AAAA, MX, NS, TXT, SOA) for a given domain.
* **Subnet Calculator:** Calculates network details (address ranges, usable hosts) for IPv4 subnets.
* **WHOIS Lookup:** Retrieves WHOIS information for a given domain or IP address.
* **MAC Address Lookup:** Identifies the vendor/manufacturer of a network interface using OUI data.
* **Port Scan:** Scans common ports of a target IP (via SSE stream).
* **Dockerized:** both frontend and backend are containerized for easy deployment.
## 📚 API Usage
The backend exposes several RESTful endpoints that return JSON data.
| Endpoint | Method | Params | Description |
| :--- | :--- | :--- | :--- |
| `/api/ipinfo` | `GET` | None | Returns info for the requestor's IP. |
| `/api/lookup` | `GET` | `targetIp` | Returns Geo/ASN info for a specific IP. |
| `/api/dns-lookup` | `GET` | `domain`, `type` | Resolves DNS records (A, AAAA, MX, etc.). |
| `/api/whois-lookup` | `GET` | `query` | Performs a WHOIS lookup for a domain or IP. |
| `/api/mac-lookup` | `GET` | `mac` | Returns the vendor for a MAC address. |
| `/api/ping` | `GET` | `targetIp` | Pings an IP address (returns 4 packets). |
| `/api/traceroute` | `GET` | `targetIp` | Streams traceroute hops via Server-Sent Events (SSE). |
| `/api/port-scan` | `GET` | `targetIp` | Streams port scan results via Server-Sent Events (SSE). |
### Examples
**Lookup an IP:**
```bash
curl "http://localhost:3000/api/lookup?targetIp=8.8.8.8"
```
**DNS Lookup:**
```bash
curl "http://localhost:3000/api/dns-lookup?domain=google.com&type=A"
```
**MAC Vendor Lookup:**
```bash
curl "http://localhost:3000/api/mac-lookup?mac=00:50:56:C0:00:08"
```
## 🛠️ Tech Stack
* **Backend:** Node.js, Express.js, MaxMind GeoLite2, `oui`, `whois-json`, `@sentry/node`.
* **Frontend:** Vanilla JS, Tailwind CSS, Leaflet.js.
* **Deployment:** Docker, GitHub Actions.
## 🏁 Getting Started
### Using Pre-built Images (Recommended)
1. **Create `compose.yml`:**
(See provided `compose.yml` in repository)
2. **Start:**
```bash
docker compose up -d
```
3. **Access:** `http://localhost:8080`
## ⚙️ Configuration
Key environment variables for the backend:
* `NODE_ENV`: `production` or `development`.
* `PORT`: Internal port (default 3000).
* `RATE_LIMIT_MAX`: Requests per window (e.g., 50).
* `SENTRY_DSN`: Optional Sentry integration.
## 🌐 Data Sources
* **Geolocation:** [MaxMind GeoLite2](https://www.maxmind.com).
* **Map Tiles:** [OpenStreetMap](https://www.openstreetmap.org) & [CartoDB](https://carto.com).
* **MAC Data:** [IEEE OUI](https://standards.ieee.org/products-services/regauth/oui/index.html).
## 📜 License
MIT License.
+7
View File
@@ -0,0 +1,7 @@
.git
.env
node_modules
npm-debug.log
Dockerfile
.dockerignore
*.md
+77
View File
@@ -0,0 +1,77 @@
# Stage 1: Build Dependencies
# Use an official Node.js runtime as a parent image
FROM node:24-alpine AS builder
WORKDIR /app
# Install OS dependencies needed for ping/traceroute
# Using apk add --no-cache reduces layer size
RUN apk add --no-cache iputils-ping traceroute
# Copy package.json and package-lock.json (or yarn.lock)
# Ensure these files include 'oui' as a dependency before building!
COPY package*.json ./
# Install app dependencies using npm ci for faster, reliable builds
# --only=production installs only production dependencies (including 'oui')
RUN npm ci --only=production
# REMOVED: RUN npm i oui (should be installed by npm ci now)
# Stage 2: Production Image
FROM node:24-alpine
WORKDIR /app
# Install only necessary OS dependencies again for the final image
RUN apk add --no-cache iputils-ping traceroute
# Copy dependencies from the builder stage
COPY --from=builder /app/node_modules ./node_modules
# Copy application code
COPY . .
# Copy MaxMind data (assuming it's in ./data)
# Ensure the 'data' directory exists in your project root
COPY ./data ./data
# Create a non-root user and group
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Create ASN cache directory and set correct ownership BEFORE switching user
# This ensures the Docker volume mount is writable by appuser
RUN mkdir -p /app/asn-cache && chown -R appuser:appgroup /app/asn-cache
# Change ownership of all app files to the new user
RUN chown -R appuser:appgroup /app
# Switch to the non-root user
USER appuser
# Make port specified in environment variable available to the world outside this container
# Default to 3000 if not specified
ARG PORT=3000
ENV PORT=${PORT}
EXPOSE ${PORT}
# Define environment variable for Node environment (important for Pino, Express etc.)
ENV NODE_ENV=production
# Define default Log Level if not set externally
ENV LOG_LEVEL=info
# Define default Ping Count if not set externally
ENV PING_COUNT=4
# Define paths to GeoIP DBs (can be overridden by external .env or docker run -e)
ENV GEOIP_CITY_DB=./data/GeoLite2-City.mmdb
ENV GEOIP_ASN_DB=./data/GeoLite2-ASN.mmdb
# Define build argument and environment variable for Git commit SHA
ARG GIT_COMMIT_SHA=unknown
ENV GIT_COMMIT_SHA=${GIT_COMMIT_SHA}
# Define build argument and environment variable for Sentry DSN
ARG SENTRY_DSN
ENV SENTRY_DSN=${SENTRY_DSN}
# Run the app when the container launches
CMD ["node", "server.js"]
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7453ef019d43d779c31ec0f4a3c6e7d75dcc6d55b656df16d8fb8865158fac56
size 11740902
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:afe884f63215e1b3e5b52d61204f9f2ffc8960a4e82b6b6af733c32ebeb0cfee
size 64840769
+9
View File
@@ -0,0 +1,9 @@
# .env
GEOIP_CITY_DB=./data/GeoLite2-City.mmdb
GEOIP_ASN_DB=./data/GeoLite2-ASN.mmdb
PORT=3000
LOG_LEVEL=debug # z.B. für mehr Details im Development
PING_COUNT=4
# NODE_ENV=development # Setze dies ggf. für pino-pretty
RATE_LIMIT_MAX=200
RATE_LIMIT_WINDOW_MS=300000 # 5 Minuten
+56
View File
@@ -0,0 +1,56 @@
// backend/maxmind.js
const geoip = require('@maxmind/geoip2-node');
const pino = require('pino');
const Sentry = require("@sentry/node");
// Minimaler Logger für dieses Modul
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
let cityReaderInstance = null;
let asnReaderInstance = null;
async function initializeMaxMind() {
if (cityReaderInstance && asnReaderInstance) {
logger.debug('MaxMind databases already loaded.');
return { cityReader: cityReaderInstance, asnReader: asnReaderInstance };
}
try {
logger.info('Loading MaxMind databases...');
const cityDbPath = process.env.GEOIP_CITY_DB || './data/GeoLite2-City.mmdb';
const asnDbPath = process.env.GEOIP_ASN_DB || './data/GeoLite2-ASN.mmdb';
logger.info({ cityDbPath, asnDbPath }, 'Database paths');
// Verwende Promise.all für paralleles Laden
const [cityReader, asnReader] = await Promise.all([
geoip.Reader.open(cityDbPath),
geoip.Reader.open(asnDbPath)
]);
cityReaderInstance = cityReader;
asnReaderInstance = asnReader;
logger.info('MaxMind databases loaded successfully.');
return { cityReader: cityReaderInstance, asnReader: asnReaderInstance };
} catch (error) {
logger.fatal({ error: error.message, stack: error.stack }, 'Could not initialize MaxMind databases.');
Sentry.captureException(error);
// Wirf den Fehler weiter, damit der Serverstart fehlschlägt
throw error;
}
}
// Funktion zum Abrufen der Reader (stellt sicher, dass sie initialisiert wurden)
function getMaxMindReaders() {
if (!cityReaderInstance || !asnReaderInstance) {
// Dieser Fall sollte im normalen Betrieb nicht auftreten, da initialize() beim Serverstart aufgerufen wird.
logger.error('MaxMind readers accessed before initialization!');
throw new Error('MaxMind readers not initialized. Call initializeMaxMind() first.');
}
return { cityReader: cityReaderInstance, asnReader: asnReaderInstance };
}
module.exports = {
initializeMaxMind,
getMaxMindReaders,
};
+2697
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
{
"name": "utrools-backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@maxmind/geoip2-node": "^6.0.0",
"@sentry/node": "^10.42.0",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"mac-oui-lookup": "^1.1.4",
"macaddress": "^0.5.3",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"qs": "^6.14.2",
"whois-json": "^2.0.4"
},
"overrides": {
"underscore": "1.13.8"
}
}
+275
View File
@@ -0,0 +1,275 @@
// backend/routes/asnLookup.js
const express = require('express');
const https = require('https');
const fs = require('fs');
const path = require('path');
const pino = require('pino');
const Sentry = require('@sentry/node');
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router();
// ─── Filesystem Cache (24h TTL) ───────────────────────────────────────────────
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
const CACHE_DIR = process.env.ASN_CACHE_DIR || path.join(__dirname, '..', 'data', 'asn-cache');
// Ensure cache directory exists
try {
fs.mkdirSync(CACHE_DIR, { recursive: true });
} catch (e) {
logger.warn({ error: e.message }, 'Could not create ASN cache directory');
}
function cacheFilePath(key) {
// Sanitize key to safe filename
return path.join(CACHE_DIR, key.replace(/[^a-zA-Z0-9_:-]/g, '_') + '.json');
}
function getCached(key) {
try {
const file = cacheFilePath(key);
const raw = fs.readFileSync(file, 'utf8');
const entry = JSON.parse(raw);
if (Date.now() > entry.expiresAt) {
fs.unlinkSync(file);
return null;
}
return entry.data;
} catch {
return null; // File doesn't exist or parse failed
}
}
function setCache(key, data) {
try {
const entry = { data, expiresAt: Date.now() + CACHE_TTL_MS };
fs.writeFileSync(cacheFilePath(key), JSON.stringify(entry), 'utf8');
} catch (e) {
logger.warn({ key, error: e.message }, 'ASN cache write failed');
}
}
// ─── HTTP Helper ──────────────────────────────────────────────────────────────
function fetchJson(url) {
return new Promise((resolve, reject) => {
const req = https.get(url, {
headers: {
'User-Agent': 'uTools-Network-Suite/1.0 (https://github.com/MrUnknownDE/utools)',
'Accept': 'application/json',
},
timeout: 15000,
}, (res) => {
let raw = '';
res.on('data', (chunk) => { raw += chunk; });
res.on('end', () => {
if (res.statusCode < 200 || res.statusCode >= 300) {
return reject(new Error(`HTTP ${res.statusCode} from ${url}`));
}
try { resolve(JSON.parse(raw)); }
catch (e) { reject(new Error(`JSON parse error: ${e.message}`)); }
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error(`Timeout: ${url}`)); });
});
}
// ─── ASN Validation ───────────────────────────────────────────────────────────
function parseAsn(raw) {
if (!raw || typeof raw !== 'string') return null;
const cleaned = raw.trim().toUpperCase().replace(/^AS/, '');
const n = parseInt(cleaned, 10);
if (isNaN(n) || n < 1 || n > 4294967295 || String(n) !== cleaned) return null;
return n;
}
// ─── RIPE Stat Fetchers ───────────────────────────────────────────────────────
async function fetchOverview(asn) {
const key = `overview:${asn}`;
const cached = getCached(key);
if (cached) return cached;
const json = await fetchJson(`https://stat.ripe.net/data/as-overview/data.json?resource=AS${asn}`);
const d = json?.data;
const result = {
asn,
name: d?.holder || null,
announced: d?.announced ?? false,
type: d?.type || null,
block: d?.block || null,
};
setCache(key, result);
return result;
}
async function fetchNeighbours(asn) {
const key = `neighbours:${asn}`;
const cached = getCached(key);
if (cached) return cached;
const json = await fetchJson(`https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS${asn}`);
const neighbours = (json?.data?.neighbours || []).map(n => ({
asn: n.asn,
type: n.type, // 'left' = upstream, 'right' = downstream
power: n.power || 0,
v4_peers: n.v4_peers || 0,
v6_peers: n.v6_peers || 0,
}));
setCache(key, neighbours);
return neighbours;
}
async function fetchPrefixes(asn) {
const key = `prefixes:${asn}`;
const cached = getCached(key);
if (cached) return cached;
const json = await fetchJson(`https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS${asn}`);
const prefixes = (json?.data?.prefixes || []).map(p => p.prefix);
setCache(key, prefixes);
return prefixes;
}
async function fetchPeeringDb(asn) {
const key = `peeringdb:${asn}`;
const cached = getCached(key);
if (cached !== null) return cached;
try {
const json = await fetchJson(`https://www.peeringdb.com/api/net?asn=${asn}&depth=2`);
const net = json?.data?.[0];
if (!net) { setCache(key, null); return null; }
const result = {
peeringPolicy: net.policy_general || null,
infoType: net.info_type || null,
infoTraffic: net.info_traffic || null,
infoRatio: net.info_ratio || null,
infoScope: net.info_scope || null,
website: net.website || null,
ixps: (net.netixlan_set || []).map(ix => ({
name: ix.name,
speed: ix.speed,
ipv4: ix.ipaddr4 || null,
ipv6: ix.ipaddr6 || null,
})).slice(0, 20),
};
setCache(key, result);
return result;
} catch (e) {
logger.warn({ asn, error: e.message }, 'PeeringDB fetch failed');
return null;
}
}
// ─── Resolve names for a list of ASNs ────────────────────────────────────────
async function resolveNames(asnList) {
const results = await Promise.allSettled(asnList.map(a => fetchOverview(a)));
const map = {};
results.forEach((r, i) => {
map[asnList[i]] = r.status === 'fulfilled' ? (r.value.name || null) : null;
});
return map;
}
// ─── Route ────────────────────────────────────────────────────────────────────
router.get('/', async (req, res, next) => {
const rawAsn = req.query.asn;
const requestIp = req.ip;
const asn = parseAsn(String(rawAsn || ''));
if (!asn) {
return res.status(400).json({
success: false,
error: 'Invalid ASN. Please provide a number between 1 and 4294967295, e.g. ?asn=15169'
});
}
logger.info({ requestIp, asn }, 'ASN lookup request');
try {
// Level 1 + Level 2: fetch all base data in parallel (allSettled = one failure won't crash everything)
const [overviewResult, neighboursResult, prefixesResult, peeringdbResult] = await Promise.allSettled([
fetchOverview(asn),
fetchNeighbours(asn),
fetchPrefixes(asn),
fetchPeeringDb(asn),
]);
const overview = overviewResult.status === 'fulfilled' ? overviewResult.value : { asn, name: null, announced: false, type: null };
const neighbours = neighboursResult.status === 'fulfilled' ? neighboursResult.value : [];
const prefixes = prefixesResult.status === 'fulfilled' ? prefixesResult.value : [];
const peeringdb = peeringdbResult.status === 'fulfilled' ? peeringdbResult.value : null;
if (overviewResult.status === 'rejected') logger.warn({ asn, error: overviewResult.reason?.message }, 'Overview fetch failed, continuing with partial data');
if (neighboursResult.status === 'rejected') logger.warn({ asn, error: neighboursResult.reason?.message }, 'Neighbours fetch failed, continuing with partial data');
if (prefixesResult.status === 'rejected') logger.warn({ asn, error: prefixesResult.reason?.message }, 'Prefixes fetch failed, continuing with partial data');
// Split neighbours (keep ALL of them, sorted by power)
const allUpstreams = neighbours.filter(n => n.type === 'left').sort((a, b) => b.power - a.power);
const allDownstreams = neighbours.filter(n => n.type === 'right').sort((a, b) => b.power - a.power);
// Resolve names for only the Top 25 of each, to prevent hammering the RIPE API (rate limits)
const topLevel2Asns = [...new Set([
...allUpstreams.slice(0, 25),
...allDownstreams.slice(0, 25)
].map(n => n.asn))];
const level2Names = await resolveNames(topLevel2Asns);
// Level 3: fetch upstreams-of-upstreams for top 5 Level 2 upstreams
const level3Raw = await Promise.allSettled(
allUpstreams.slice(0, 5).map(async (upstreamNode) => {
const theirNeighbours = await fetchNeighbours(upstreamNode.asn);
const theirUpstreams = theirNeighbours
.filter(n => n.type === 'left')
.sort((a, b) => b.power - a.power)
.slice(0, 3);
return { parentAsn: upstreamNode.asn, theirUpstreams };
})
);
const level3Data = level3Raw
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
// Resolve names for Level 3 nodes
const level3Asns = [...new Set(level3Data.flatMap(d => d.theirUpstreams.map(n => n.asn)))];
const level3Names = await resolveNames(level3Asns);
// ── Build graph ───────────────────────────────────────────────────────
const graph = {
center: { asn, name: overview.name },
level2: {
upstreams: allUpstreams.map(n => ({ asn: n.asn, name: level2Names[n.asn] || null, power: n.power, v4: n.v4_peers, v6: n.v6_peers })),
downstreams: allDownstreams.map(n => ({ asn: n.asn, name: level2Names[n.asn] || null, power: n.power, v4: n.v4_peers, v6: n.v6_peers })),
},
level3: level3Data.map(d => ({
parentAsn: d.parentAsn,
parentName: level2Names[d.parentAsn] || null,
upstreams: d.theirUpstreams.map(n => ({
asn: n.asn,
name: level3Names[n.asn] || null,
power: n.power,
})),
})),
};
res.json({
success: true,
asn,
name: overview.name,
announced: overview.announced,
type: overview.type,
prefixes: prefixes, // Export all prefixes without limit
peeringdb,
graph,
});
} catch (error) {
logger.error({ asn, requestIp, error: error.message }, 'ASN lookup failed');
Sentry.captureException(error, { extra: { asn, requestIp } });
next(error);
}
});
module.exports = router;
+115
View File
@@ -0,0 +1,115 @@
// backend/routes/dnsLookup.js
const express = require('express');
const Sentry = require("@sentry/node");
const dns = require('dns').promises;
const pino = require('pino');
// Import utilities
const { isValidDomain } = require('../utils');
// Logger for this module
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router();
// Supported DNS record types
const VALID_DNS_TYPES = ['A', 'AAAA', 'MX', 'TXT', 'NS', 'CNAME', 'SOA', 'SRV', 'PTR', 'ANY'];
// Route handler for / (relative to /api/dns-lookup)
router.get('/', async (req, res, next) => {
const domainRaw = req.query.domain;
const domain = typeof domainRaw === 'string' ? domainRaw.trim() : domainRaw;
const typeRaw = req.query.type;
// Default to 'ANY' if type is missing or invalid, convert valid types to uppercase
let type = typeof typeRaw === 'string' ? typeRaw.trim().toUpperCase() : 'ANY';
if (!VALID_DNS_TYPES.includes(type)) {
logger.warn({ requestIp: req.ip, domain, requestedType: typeRaw }, 'Invalid record type requested, defaulting to ANY');
type = 'ANY'; // Default to 'ANY' for invalid types
}
const requestIp = req.ip || req.socket.remoteAddress;
logger.info({ requestIp, domain, type }, 'DNS lookup request received');
if (!isValidDomain(domain)) {
logger.warn({ requestIp, domain }, 'Invalid domain for DNS lookup');
return res.status(400).json({ success: false, error: 'Invalid domain name provided.' });
}
// Note: No isPrivateIp check here as DNS lookups for internal domains might be valid use cases.
try {
let records;
if (type === 'ANY') {
// Define types to query for 'ANY' - exclude PTR as it requires an IP
const typesToQuery = ['A', 'AAAA', 'MX', 'TXT', 'NS', 'CNAME', 'SOA', 'SRV'];
const promises = typesToQuery.map(t =>
dns.resolve(domain, t)
.then(result => ({ type: t, records: result })) // Wrap result with type
.catch(err => ({ type: t, error: err })) // Wrap error with type
);
const results = await Promise.allSettled(promises);
records = {};
results.forEach(result => {
if (result.status === 'fulfilled') {
const data = result.value;
if (data.error) {
// Log DNS resolution errors for specific types as warnings/debug
if (data.error.code !== 'ENOTFOUND' && data.error.code !== 'ENODATA') {
logger.warn({ requestIp, domain, type: data.type, error: data.error.message, code: data.error.code }, `DNS lookup failed for type ${data.type}`);
} else {
logger.debug({ requestIp, domain, type: data.type, code: data.error.code }, `No record found for type ${data.type}`);
}
// Optionally include error details in response (or just omit the type)
// records[data.type] = { error: `Lookup failed (${data.error.code || 'Unknown'})` };
} else if (data.records && data.records.length > 0) {
// Only add if records exist
records[data.type] = data.records;
}
} else {
// Handle unexpected errors from Promise.allSettled (should be rare)
logger.error({ requestIp, domain, type: 'ANY', error: result.reason?.message }, 'Unexpected error during Promise.allSettled for ANY DNS lookup');
}
});
if (Object.keys(records).length === 0) {
// If no records found for any type
logger.info({ requestIp, domain, type }, 'DNS lookup for ANY type yielded no records.');
// Send success: true, but with an empty records object or a note
// return res.json({ success: true, domain, type, records: {}, note: 'No records found for queried types.' });
}
} else {
// Handle specific type query
try {
records = await dns.resolve(domain, type);
} catch (error) {
if (error.code === 'ENOTFOUND' || error.code === 'ENODATA') {
logger.info({ requestIp, domain, type, code: error.code }, `DNS lookup failed (No record) for type ${type}`);
// Return success: true, but indicate no records found
return res.json({ success: true, domain, type, records: [], note: `No ${type} records found.` });
} else {
// Rethrow other errors to be caught by the outer catch block
throw error;
}
}
}
logger.info({ requestIp, domain, type }, 'DNS lookup successful');
// For specific type, records will be an array. For ANY, it's an object.
res.json({ success: true, domain, type, records });
} catch (error) {
// Catches errors from specific type lookups (not ENOTFOUND/ENODATA) or unexpected errors
logger.error({ requestIp, domain, type, error: error.message, code: error.code }, 'DNS lookup failed');
Sentry.captureException(error, { extra: { requestIp, domain, type } });
// Send appropriate status code based on error if possible, otherwise 500
const statusCode = error.code === 'ESERVFAIL' ? 502 : 500;
res.status(statusCode).json({ success: false, error: `DNS lookup failed: ${error.message} (Code: ${error.code || 'Unknown'})` });
// next(error); // Optional: Pass to Sentry error handler
}
});
module.exports = router;
+116
View File
@@ -0,0 +1,116 @@
// backend/routes/ipinfo.js
const express = require('express');
const Sentry = require("@sentry/node");
const dns = require('dns').promises;
const pino = require('pino'); // Assuming logger is needed, or pass it down
// Import utilities and MaxMind reader access
const { isValidIp, getCleanIp } = require('../utils');
const { getMaxMindReaders } = require('../maxmind');
// Create a logger instance for this route module
// Ideally, the main logger instance should be passed down or configured globally
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router();
// Route handler for / (relative to where this router is mounted, e.g., /api/ipinfo)
router.get('/', async (req, res, next) => {
const requestIp = req.ip || req.socket.remoteAddress;
logger.info({ ip: requestIp, method: req.method, url: req.originalUrl }, 'ipinfo request received');
const clientIp = getCleanIp(requestIp);
logger.debug({ rawIp: requestIp, cleanedIp: clientIp }, 'IP cleaning result');
if (!clientIp || !isValidIp(clientIp)) {
if (clientIp === '127.0.0.1' || clientIp === '::1') {
logger.info({ ip: clientIp }, 'Responding with localhost info');
return res.json({
ip: clientIp,
geo: { note: 'Localhost IP, no Geo data available.' },
asn: { note: 'Localhost IP, no ASN data available.' },
rdns: ['localhost'],
});
}
logger.error({ rawIp: requestIp, cleanedIp: clientIp }, 'Could not determine a valid client IP');
Sentry.captureMessage('Could not determine a valid client IP', {
level: 'error',
extra: { rawIp: requestIp, cleanedIp: clientIp }
});
// Use 400 for client error (invalid IP derived)
return res.status(400).json({ error: 'Could not determine a valid client IP address.', rawIp: requestIp, cleanedIp: clientIp });
}
try {
// Get initialized MaxMind readers
const { cityReader, asnReader } = getMaxMindReaders();
let geo = null;
try {
const geoData = cityReader.city(clientIp);
geo = {
city: geoData.city?.names?.en,
region: geoData.subdivisions?.[0]?.isoCode,
country: geoData.country?.isoCode,
countryName: geoData.country?.names?.en,
postalCode: geoData.postal?.code,
latitude: geoData.location?.latitude,
longitude: geoData.location?.longitude,
timezone: geoData.location?.timeZone,
};
// Remove null/undefined values
geo = Object.fromEntries(Object.entries(geo).filter(([_, v]) => v != null));
logger.debug({ ip: clientIp, geo }, 'GeoIP lookup successful');
} catch (e) {
// Log as warning, as this is expected for private IPs or IPs not in DB
logger.warn({ ip: clientIp, error: e.message }, `MaxMind City lookup failed`);
geo = { error: 'GeoIP lookup failed (IP not found in database or private range).' };
}
let asn = null;
try {
const asnData = asnReader.asn(clientIp);
asn = {
number: asnData.autonomousSystemNumber,
organization: asnData.autonomousSystemOrganization,
};
asn = Object.fromEntries(Object.entries(asn).filter(([_, v]) => v != null));
logger.debug({ ip: clientIp, asn }, 'ASN lookup successful');
} catch (e) {
logger.warn({ ip: clientIp, error: e.message }, `MaxMind ASN lookup failed`);
asn = { error: 'ASN lookup failed (IP not found in database or private range).' };
}
let rdns = null;
try {
const hostnames = await dns.reverse(clientIp);
rdns = hostnames;
logger.debug({ ip: clientIp, rdns }, 'rDNS lookup successful');
} catch (e) {
// Log non-existence as debug, other errors as warn
if (e.code !== 'ENOTFOUND' && e.code !== 'ENODATA') {
logger.warn({ ip: clientIp, error: e.message, code: e.code }, `rDNS lookup error`);
} else {
logger.debug({ ip: clientIp, code: e.code }, 'rDNS lookup failed (No record)');
}
// Provide a structured error in the response
rdns = { error: `rDNS lookup failed (${e.code || 'Unknown error'})` };
}
res.json({
ip: clientIp,
// Only include geo/asn if they don't contain an error and have data
geo: geo.error ? geo : (Object.keys(geo).length > 0 ? geo : null),
asn: asn.error ? asn : (Object.keys(asn).length > 0 ? asn : null),
rdns // rdns will contain either the array of hostnames or the error object
});
} catch (error) {
// Catch unexpected errors during processing (e.g., issues with getMaxMindReaders)
logger.error({ ip: clientIp, error: error.message, stack: error.stack }, 'Error processing ipinfo');
Sentry.captureException(error, { extra: { ip: clientIp } });
// Pass the error to the Sentry error handler middleware
next(error);
}
});
module.exports = router;
+98
View File
@@ -0,0 +1,98 @@
// backend/routes/lookup.js
const express = require('express');
const Sentry = require("@sentry/node");
const dns = require('dns').promises;
const pino = require('pino');
// Import utilities and MaxMind reader access
const { isValidIp, isPrivateIp } = require('../utils');
const { getMaxMindReaders } = require('../maxmind');
// Logger for this module
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router();
// Route handler for / (relative to /api/lookup)
router.get('/', async (req, res, next) => {
const targetIpRaw = req.query.targetIp;
const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw;
const requestIp = req.ip || req.socket.remoteAddress; // IP of the client making the request
logger.info({ requestIp, targetIp }, 'Lookup request received');
if (!isValidIp(targetIp)) {
logger.warn({ requestIp, targetIp }, 'Invalid target IP for lookup');
return res.status(400).json({ success: false, error: 'Invalid IP address provided for lookup.' });
}
if (isPrivateIp(targetIp)) {
logger.warn({ requestIp, targetIp }, 'Attempt to lookup private IP blocked');
return res.status(403).json({ success: false, error: 'Lookup for private or local IP addresses is not supported.' });
}
try {
// Get initialized MaxMind readers
const { cityReader, asnReader } = getMaxMindReaders();
let geoResult = null;
try {
const geoData = cityReader.city(targetIp); // Synchronous call
let geo = {
city: geoData.city?.names?.en, region: geoData.subdivisions?.[0]?.isoCode,
country: geoData.country?.isoCode, countryName: geoData.country?.names?.en,
postalCode: geoData.postal?.code, latitude: geoData.location?.latitude,
longitude: geoData.location?.longitude, timezone: geoData.location?.timeZone,
};
geo = Object.fromEntries(Object.entries(geo).filter(([_, v]) => v != null));
logger.debug({ targetIp, geo }, 'GeoIP lookup successful for lookup');
geoResult = Object.keys(geo).length > 0 ? geo : null; // Assign result or null
} catch (e) {
logger.warn({ targetIp, error: e.message }, `MaxMind City lookup failed for lookup`);
geoResult = { error: 'GeoIP lookup failed (IP not found in database or private range).' };
}
let asnResult = null;
try {
const asnData = asnReader.asn(targetIp); // Synchronous call
let asn = { number: asnData.autonomousSystemNumber, organization: asnData.autonomousSystemOrganization };
asn = Object.fromEntries(Object.entries(asn).filter(([_, v]) => v != null));
logger.debug({ targetIp, asn }, 'ASN lookup successful for lookup');
asnResult = Object.keys(asn).length > 0 ? asn : null; // Assign result or null
} catch (e) {
logger.warn({ targetIp, error: e.message }, `MaxMind ASN lookup failed for lookup`);
asnResult = { error: 'ASN lookup failed (IP not found in database or private range).' };
}
// Perform async rDNS lookup
const rdnsResult = await dns.reverse(targetIp)
.then(hostnames => {
logger.debug({ targetIp, rdns: hostnames }, 'rDNS lookup successful for lookup');
return hostnames; // Returns array of hostnames
})
.catch(e => {
if (e.code !== 'ENOTFOUND' && e.code !== 'ENODATA') {
logger.warn({ targetIp, error: e.message, code: e.code }, `rDNS lookup error for lookup`);
} else {
logger.debug({ targetIp, code: e.code }, 'rDNS lookup failed (No record) for lookup');
}
return { error: `rDNS lookup failed (${e.code || 'Unknown error'})` };
});
// Combine results and send response
res.json({
success: true, // Indicate overall success of the request processing
ip: targetIp,
geo: geoResult, // Result from the sync try...catch
asn: asnResult, // Result from the sync try...catch
rdns: rdnsResult // Result from the async operation
});
} catch (error) {
// Catch unexpected errors (e.g., issue with getMaxMindReaders or dns.reverse if not caught above)
logger.error({ targetIp, requestIp, error: error.message, stack: error.stack }, 'Error processing lookup');
Sentry.captureException(error, { extra: { targetIp, requestIp } });
next(error); // Pass to the main error handler
}
});
module.exports = router;
+39
View File
@@ -0,0 +1,39 @@
const express = require('express');
const Sentry = require("@sentry/node");
const pino = require('pino');
const { getVendor } = require('mac-oui-lookup');
const { isValidMacAddress } = require('../utils');
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router();
router.get('/', async (req, res) => {
const macRaw = req.query.mac;
const mac = typeof macRaw === 'string' ? macRaw.trim() : macRaw;
const requestIp = req.ip || req.socket.remoteAddress;
logger.info({ requestIp, mac }, 'MAC lookup request received');
if (!isValidMacAddress(mac)) {
logger.warn({ requestIp, mac }, 'Invalid MAC address for lookup');
return res.status(400).json({ success: false, error: 'Invalid MAC address format provided.' });
}
try {
const vendor = getVendor(mac);
if (vendor) {
logger.info({ requestIp, mac, vendor }, 'MAC lookup successful');
res.json({ success: true, mac, vendor });
} else {
logger.info({ requestIp, mac }, 'MAC address not found in OUI database');
res.status(404).json({ success: false, error: 'Vendor not found for this MAC address.' });
}
} catch (error) {
logger.error({ requestIp, mac, error: error.message }, 'MAC lookup failed');
Sentry.captureException(error, { extra: { requestIp, mac } });
res.status(500).json({ success: false, error: 'An unexpected error occurred during the MAC lookup.' });
}
});
module.exports = router;
+82
View File
@@ -0,0 +1,82 @@
// backend/routes/ping.js
const express = require('express');
const Sentry = require("@sentry/node");
const pino = require('pino');
// Import utilities
const { isValidIp, isPrivateIp, executeCommand, parsePingOutput } = require('../utils');
// Logger for this module
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router();
// Route handler for / (relative to /api/ping)
router.get('/', async (req, res, next) => {
const targetIpRaw = req.query.targetIp;
const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw;
const requestIp = req.ip || req.socket.remoteAddress;
logger.info({ requestIp, targetIp }, 'Ping request received');
if (!isValidIp(targetIp)) {
logger.warn({ requestIp, targetIp }, 'Invalid target IP for ping');
return res.status(400).json({ success: false, error: 'Invalid target IP address provided.' });
}
if (isPrivateIp(targetIp)) {
logger.warn({ requestIp, targetIp }, 'Attempt to ping private IP blocked');
return res.status(403).json({ success: false, error: 'Operations on private or local IP addresses are not allowed.' });
}
try {
const pingCount = process.env.PING_COUNT || '4';
let countArg = parseInt(pingCount, 10); // Use let as it might be reassigned
// Validate countArg to prevent potential issues
if (isNaN(countArg) || countArg <= 0 || countArg > 10) { // Limit count for safety
logger.warn({ requestIp, targetIp, requestedCount: pingCount }, 'Invalid or excessive ping count requested, using default.');
countArg = 4; // Default to 4 if invalid
}
const args = ['-c', `${countArg}`, targetIp];
const command = 'ping';
logger.info({ requestIp, targetIp, command: `${command} ${args.join(' ')}` }, 'Executing ping');
const output = await executeCommand(command, args);
const parsedResult = parsePingOutput(output);
if (parsedResult.error) {
logger.warn({ requestIp, targetIp, error: parsedResult.error, rawOutput: parsedResult.rawOutput }, 'Ping command executed but resulted in an error state');
// Send 200 OK but indicate failure in the response body
return res.status(200).json({
success: false,
error: parsedResult.error,
rawOutput: parsedResult.rawOutput,
stats: parsedResult.stats // Include stats even if there's an error message
});
}
logger.info({ requestIp, targetIp, stats: parsedResult.stats }, 'Ping successful');
res.json({ success: true, ...parsedResult });
} catch (error) {
// This catch block handles errors from executeCommand (e.g., command not found, non-zero exit code)
logger.error({ requestIp, targetIp, error: error.message, stderr: error.stderr }, 'Ping command failed execution');
Sentry.captureException(error, { extra: { requestIp, targetIp, stderr: error.stderr } });
// Attempt to parse the error output (might be stdout or stderr from the error object)
const errorOutput = error.stderr || error.stdout || error.message;
const parsedError = parsePingOutput(errorOutput);
// Send 500 Internal Server Error, but include parsed details if available
res.status(500).json({
success: false,
// Prioritize parsed error message, fallback to original error message
error: `Ping command failed: ${parsedError.error || error.message}`,
rawOutput: parsedError.rawOutput || errorOutput // Include raw output for debugging
});
// Optionally call next(error) if you want the main Sentry error handler to also catch this
// next(error);
}
});
module.exports = router;
+76
View File
@@ -0,0 +1,76 @@
const express = require('express');
const Sentry = require("@sentry/node");
const pino = require('pino');
const { isValidIp, isPrivateIp, checkPort } = require('../utils');
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router();
const COMMON_PORTS_TO_SCAN = [
21, 22, 25, 53, 80, 110, 143, 443, 3306, 3389, 5432, 8080, 8443
];
router.get('/', (req, res) => {
const targetIpRaw = req.query.targetIp;
const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw;
const requestIp = req.ip || req.socket.remoteAddress;
logger.info({ requestIp, targetIp }, 'Port scan stream request received');
if (!isValidIp(targetIp)) {
logger.warn({ requestIp, targetIp }, 'Invalid target IP for port scan');
return res.status(400).json({ success: false, error: 'Invalid target IP address provided.' });
}
if (isPrivateIp(targetIp)) {
logger.warn({ requestIp, targetIp }, 'Attempt to scan private IP blocked');
return res.status(403).json({ success: false, error: 'Operations on private or local IP addresses are not allowed.' });
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders();
const sendEvent = (event, data) => {
if (!res.writableEnded) {
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
}
};
let isConnectionClosed = false;
req.on('close', () => {
logger.info({ requestIp, targetIp }, 'Client disconnected from port scan stream.');
isConnectionClosed = true;
});
(async () => {
try {
sendEvent('info', { message: `Starting scan of ${COMMON_PORTS_TO_SCAN.length} common ports on ${targetIp}...` });
for (const port of COMMON_PORTS_TO_SCAN) {
if (isConnectionClosed) break;
const result = await checkPort(port, targetIp, 2000);
sendEvent('port_status', result);
}
if (!isConnectionClosed) {
logger.info({ requestIp, targetIp }, 'Port scan stream completed successfully.');
sendEvent('end', { message: 'Scan complete.' });
}
} catch (error) {
logger.error({ requestIp, targetIp, error: error.message }, 'Error during port scan stream');
// Add context directly to the captureException call
Sentry.captureException(error, { extra: { requestIp, targetIp } });
if (!isConnectionClosed) {
sendEvent('error', { error: 'An unexpected error occurred during the scan.' });
}
} finally {
if (!res.writableEnded) {
res.end();
}
}
})();
});
module.exports = router;
+147
View File
@@ -0,0 +1,147 @@
const express = require('express');
const Sentry = require("@sentry/node");
const { spawn } = require('child_process');
const pino = require('pino');
// Import utilities
const { isValidIp, isPrivateIp, parseTracerouteLine } = require('../utils');
// Logger for this module
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router();
// Helper function to safely create an error message string
function getErrorMessage(err, defaultMessage = 'An unknown error occurred') {
if (typeof err === 'string') return err;
if (err && typeof err.message === 'string' && err.message.trim() !== '') return err.message;
return defaultMessage;
}
// Route handler for / (relative to /api/traceroute)
router.get('/', (req, res) => {
const targetIpRaw = req.query.targetIp;
const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw;
const requestIp = req.ip || req.socket.remoteAddress;
logger.info({ requestIp, targetIp }, 'Traceroute stream request received');
if (!isValidIp(targetIp)) {
logger.warn({ requestIp, targetIp }, 'Invalid target IP for traceroute');
return res.status(400).json({ success: false, error: 'Invalid target IP address provided.' });
}
if (isPrivateIp(targetIp)) {
logger.warn({ requestIp, targetIp }, 'Attempt to traceroute private IP blocked');
return res.status(403).json({ success: false, error: 'Operations on private or local IP addresses are not allowed.' });
}
try {
logger.info({ requestIp, targetIp }, `Starting traceroute stream...`);
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders();
const args = ['-n', targetIp];
const command = 'traceroute';
const proc = spawn(command, args);
logger.info({ requestIp, targetIp, command: `${command} ${args.join(' ')}` }, 'Spawned traceroute process');
let buffer = '';
const sendEvent = (event, data) => {
try {
if (!res.writableEnded) {
if (event === 'error' && (!data || typeof data.error !== 'string')) {
const safeErrorMessage = getErrorMessage(data?.error, 'Traceroute encountered an unspecified error.');
logger.warn({ requestIp, targetIp, originalData: data }, `Corrected invalid error event data. Sending: ${safeErrorMessage}`);
data = { error: safeErrorMessage };
}
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
} else {
logger.warn({ requestIp, targetIp, event }, "Attempted to write to closed SSE stream.");
}
} catch (e) {
// This catch handles errors during res.write, likely client disconnect
logger.error({ requestIp, targetIp, event, error: e.message }, "Error writing to SSE stream (client likely disconnected)");
Sentry.captureException(e, { level: 'warning', extra: { requestIp, targetIp, event } });
if (proc && !proc.killed) proc.kill();
if (!res.writableEnded) res.end();
}
};
proc.stdout.on('data', (data) => {
buffer += data.toString();
let lines = buffer.split('\n');
buffer = lines.pop() || '';
lines.forEach(line => {
const parsed = parseTracerouteLine(line);
if (parsed) {
sendEvent('hop', parsed);
} else if (line.trim()) {
sendEvent('info', { message: line.trim() });
}
});
});
proc.stderr.on('data', (data) => {
const errorMsg = getErrorMessage(data.toString().trim(), 'Traceroute produced unknown stderr output.');
logger.warn({ requestIp, targetIp, stderr: errorMsg }, 'Traceroute stderr output');
Sentry.captureMessage('Traceroute stderr output', { level: 'warning', extra: { requestIp, targetIp, stderr: errorMsg } });
sendEvent('error', { error: errorMsg });
});
proc.on('error', (err) => {
const errorMsg = getErrorMessage(err, 'Failed to start traceroute command due to an unknown error.');
logger.error({ requestIp, targetIp, error: errorMsg }, `Failed to start traceroute command`);
Sentry.captureException(err, { extra: { requestIp, targetIp } }); // Capture original error
sendEvent('error', { error: `Failed to start traceroute: ${errorMsg}` });
if (!res.writableEnded) res.end();
});
proc.on('close', (code) => {
if (buffer) {
const parsed = parseTracerouteLine(buffer);
if (parsed) sendEvent('hop', parsed);
else if (buffer.trim()) sendEvent('info', { message: buffer.trim() });
}
if (code !== 0) {
const errorMsg = `Traceroute command failed with exit code ${code}`;
logger.error({ requestIp, targetIp, exitCode: code }, errorMsg);
Sentry.captureMessage('Traceroute command failed', { level: 'error', extra: { requestIp, targetIp, exitCode: code } });
sendEvent('error', { error: errorMsg });
} else {
logger.info({ requestIp, targetIp }, `Traceroute stream completed successfully.`);
}
sendEvent('end', { exitCode: code });
if (!res.writableEnded) res.end();
});
req.on('close', () => {
logger.info({ requestIp, targetIp }, 'Client disconnected from traceroute stream, killing process.');
if (proc && !proc.killed) proc.kill();
if (!res.writableEnded) res.end();
});
} catch (error) {
const errorMsg = getErrorMessage(error, 'Failed to initiate traceroute due to an internal server error.');
logger.error({ requestIp, targetIp, error: errorMsg, stack: error.stack }, 'Error setting up traceroute stream');
Sentry.captureException(error, { extra: { requestIp, targetIp } });
if (!res.headersSent) {
res.status(500).json({ success: false, error: `Failed to initiate traceroute: ${errorMsg}` });
} else {
try {
if (!res.writableEnded) {
sendEvent('error', { error: `Internal server error during setup: ${errorMsg}` });
res.end();
}
} catch (e) { logger.error({ requestIp, targetIp, error: e.message }, "Error writing final setup error to SSE stream"); }
}
}
});
module.exports = router;
+20
View File
@@ -0,0 +1,20 @@
// backend/routes/version.js
const express = require('express');
const pino = require('pino');
// Logger for this module
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router();
// Route handler for / (relative to /api/version)
router.get('/', (req, res) => {
// Read commit SHA from environment variable (set during build/deploy)
const commitSha = process.env.GIT_COMMIT_SHA || 'unknown';
const requestIp = req.ip || req.socket.remoteAddress;
logger.info({ requestIp, commitSha }, 'Version request received');
res.json({ commitSha });
});
module.exports = router;
+91
View File
@@ -0,0 +1,91 @@
// backend/routes/whoisLookup.js
const express = require('express');
const Sentry = require("@sentry/node");
const whois = require('whois-json');
const pino = require('pino');
// Import utilities
const { isValidIp, isValidDomain, isPrivateIp } = require('../utils');
// Logger for this module
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router();
// Route handler for / (relative to /api/whois-lookup)
router.get('/', async (req, res, next) => {
const queryRaw = req.query.query;
const query = typeof queryRaw === 'string' ? queryRaw.trim() : queryRaw;
const requestIp = req.ip || req.socket.remoteAddress;
logger.info({ requestIp, query }, 'WHOIS lookup request received');
// Validate if the query is either a valid IP or a valid domain
if (!isValidIp(query) && !isValidDomain(query)) {
logger.warn({ requestIp, query }, 'Invalid query for WHOIS lookup');
return res.status(400).json({ success: false, error: 'Invalid domain name or IP address provided for WHOIS lookup.' });
}
if (isValidIp(query) && isPrivateIp(query)) {
logger.warn({ requestIp, query }, 'Attempt to WHOIS lookup private IP blocked');
return res.status(403).json({ success: false, error: 'WHOIS lookup for private or local IP addresses is not supported.' });
}
// Note: No isPrivateIp check here, as WHOIS for IPs might be desired regardless of range,
// and domain lookups don't involve IP ranges.
try {
// Execute WHOIS lookup with a timeout
const result = await whois(query, {
timeout: parseInt(process.env.WHOIS_TIMEOUT || '10000', 10), // Configurable timeout (default 10s), ensure integer
// follow: 3, // Optional: limit number of redirects followed
// verbose: true // Optional: get raw text output as well
});
// Check if the result indicates an error (some servers return structured errors)
// This check might need adjustment based on the 'whois-json' library's output for errors.
if (result && (result.error || result.Error)) {
logger.warn({ requestIp, query, whoisResult: result }, 'WHOIS lookup returned an error structure');
return res.status(404).json({ success: false, error: `WHOIS lookup failed: ${result.error || result.Error}`, result });
}
// Basic check if the result is empty or just contains the query itself (might indicate no data)
if (!result || Object.keys(result).length === 0 || (Object.keys(result).length === 1 && (result.domainName === query || result.query === query))) {
logger.info({ requestIp, query }, 'WHOIS lookup returned no detailed data.');
// Consider 404 Not Found if no data is available
return res.status(404).json({ success: false, error: 'No detailed WHOIS information found for the query.', query });
}
logger.info({ requestIp, query }, 'WHOIS lookup successful');
res.json({ success: true, query, result });
} catch (error) {
logger.error({ requestIp, query, error: error.message }, 'WHOIS lookup failed');
Sentry.captureException(error, { extra: { requestIp, query } });
// Provide more user-friendly error messages based on common errors
let errorMessage = error.message;
let statusCode = 500; // Default to Internal Server Error
if (error.message.includes('ETIMEDOUT') || error.message.includes('ESOCKETTIMEDOUT')) {
errorMessage = 'WHOIS server timed out.';
statusCode = 504; // Gateway Timeout
} else if (error.message.includes('ENOTFOUND')) {
// This might indicate the domain doesn't exist or the WHOIS server for the TLD couldn't be found
errorMessage = 'Domain or IP not found, or the corresponding WHOIS server is unavailable.';
statusCode = 404; // Not Found
} else if (error.message.includes('ECONNREFUSED')) {
errorMessage = 'Connection to WHOIS server refused.';
statusCode = 503; // Service Unavailable
} else if (error.message.includes('No WHOIS server found for')) {
errorMessage = 'Could not find a WHOIS server for the requested domain/TLD.';
statusCode = 404; // Not Found (as the server for it isn't known)
}
// Add more specific error handling if needed based on observed errors
res.status(statusCode).json({ success: false, error: `WHOIS lookup failed: ${errorMessage}` });
// next(error); // Optional: Pass to Sentry error handler
}
});
module.exports = router;
+214
View File
@@ -0,0 +1,214 @@
require('dotenv').config();
// --- Sentry Initialisierung (GANZ OBEN, nach dotenv) ---
const Sentry = require("@sentry/node");
// Initialize Sentry BEFORE requiring any other modules!
Sentry.init({
// DSN should now be available from process.env if set in .env
dsn: process.env.SENTRY_DSN || "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@oooooooooooooooo.ingest.sentry.io/123456",
// Enable tracing - Adjust sample rate as needed
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
integrations: [
// send console.log, console.warn, and console.error calls as logs to Sentry
Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
],
// Enable logs to be sent to Sentry
enableLogs: true,
});
// DEBUG: Check Sentry object after init
console.log("Sentry object after init:", typeof Sentry, Sentry ? Object.keys(Sentry) : 'Sentry is undefined/null');
// --- Ende Sentry Initialisierung ---
// Require necessary core modules AFTER Sentry is initialized
const express = require('express');
const cors = require('cors');
const pino = require('pino'); // Logging library
const rateLimit = require('express-rate-limit'); // Rate limiting middleware
// Import local modules
const { initializeMaxMind } = require('./maxmind'); // MaxMind DB initialization
const ipinfoRoutes = require('./routes/ipinfo');
const pingRoutes = require('./routes/ping');
const tracerouteRoutes = require('./routes/traceroute');
const lookupRoutes = require('./routes/lookup');
const dnsLookupRoutes = require('./routes/dnsLookup');
const whoisLookupRoutes = require('./routes/whoisLookup');
const versionRoutes = require('./routes/version');
const portScanRoutes = require('./routes/portScan');
const macLookupRoutes = require('./routes/macLookup');
const asnLookupRoutes = require('./routes/asnLookup');
// --- Logger Initialisierung ---
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:standard', ignore: 'pid,hostname' } }
: undefined,
});
// Create Express app instance
const app = express();
const PORT = process.env.PORT || 3000;
// --- Sentry Middleware (Request Handler & Tracing) ---
// Must be the first middleware
if (Sentry.Handlers && Sentry.Handlers.requestHandler) {
app.use(Sentry.Handlers.requestHandler());
} else {
logger.error("Sentry.Handlers.requestHandler is not available!");
}
// Must be after requestHandler, before routes
if (Sentry.Handlers && Sentry.Handlers.tracingHandler) {
app.use(Sentry.Handlers.tracingHandler());
} else {
logger.error("Sentry.Handlers.tracingHandler is not available!");
}
// --- Ende Sentry Middleware ---
// --- Core Middleware ---
app.use(cors()); // Enable CORS
app.use(express.json()); // Parse JSON bodies
app.set('trust proxy', parseInt(process.env.TRUST_PROXY_COUNT || '2', 10)); // Adjust based on your proxy setup, ensure integer
// --- Rate Limiter ---
// Apply a general limiter to most routes
const generalLimiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || (5 * 60 * 1000).toString(), 10), // Default 5 minutes
max: parseInt(process.env.RATE_LIMIT_MAX || (process.env.NODE_ENV === 'production' ? '20' : '200'), 10), // Requests per window per IP
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
message: { success: false, error: 'Too many requests from this IP, please try again after a while' },
keyGenerator: (req, res) => req.ip, // Use client IP address from Express
handler: (req, res, next, options) => {
logger.warn({ ip: req.ip, route: req.originalUrl }, 'Rate limit exceeded');
Sentry.captureMessage('Rate limit exceeded', {
level: 'warning',
extra: { ip: req.ip, route: req.originalUrl }
});
res.status(options.statusCode).send(options.message);
}
});
// Apply the limiter to ALL API routes
app.use('/api', generalLimiter);
// --- API Routes ---
// Mount the imported route handlers
app.use('/api/ipinfo', ipinfoRoutes);
app.use('/api/ping', pingRoutes);
app.use('/api/traceroute', tracerouteRoutes);
app.use('/api/lookup', lookupRoutes);
app.use('/api/dns-lookup', dnsLookupRoutes);
app.use('/api/whois-lookup', whoisLookupRoutes);
app.use('/api/version', versionRoutes);
app.use('/api/port-scan', portScanRoutes);
app.use('/api/mac-lookup', macLookupRoutes);
app.use('/api/asn-lookup', asnLookupRoutes);
// --- Sentry Error Handler ---
// Must be AFTER all controllers and BEFORE any other error handling middleware
if (Sentry.Handlers && Sentry.Handlers.errorHandler) {
app.use(Sentry.Handlers.errorHandler({
shouldHandleError(error) {
// Capture all 500 errors
if (error.status === 500) return true;
// Capture specific client errors if needed, e.g., 403
// if (error.status === 403) return true;
// By default, capture only server errors (5xx)
return error.status >= 500;
},
}));
} else {
logger.error("Sentry.Handlers.errorHandler is not available!");
}
// --- Ende Sentry Error Handler ---
// --- Fallback Error Handler ---
// Optional: Catches errors not handled by Sentry or passed via next(err)
app.use((err, req, res, next) => {
logger.error({
error: err.message,
stack: err.stack,
url: req.originalUrl,
method: req.method,
status: err.status,
sentryId: res.sentry // Sentry ID if available
}, 'Unhandled error caught by fallback handler');
// Avoid sending stack trace in production
const errorResponse = {
error: err.message || 'Internal Server Error',
...(res.sentry && { sentryId: res.sentry }) // Include Sentry ID if available
};
res.status(err.status || 500).json(errorResponse);
});
// --- Server Start ---
let server; // Variable to hold the server instance for graceful shutdown
// Initialize external resources (like MaxMind DBs) then start the server
initializeMaxMind().then(() => {
server = app.listen(PORT, () => {
logger.info({ port: PORT, node_env: process.env.NODE_ENV || 'development' }, `Server listening`);
// Log available routes (optional)
logger.info(`API base URL: http://localhost:${PORT}/api`);
});
}).catch(error => {
logger.fatal({ error: error.message, stack: error.stack }, "Server could not start due to initialization errors.");
Sentry.captureException(error); // Capture initialization errors
process.exit(1); // Exit if initialization fails
});
// --- Graceful Shutdown ---
const signals = { 'SIGINT': 2, 'SIGTERM': 15 };
async function gracefulShutdown(signal) {
logger.info(`Received ${signal}, shutting down gracefully...`);
if (server) {
server.close(async () => {
logger.info('HTTP server closed.');
// Close Sentry to allow time for events to be sent
try {
await Sentry.close(2000); // 2 second timeout
logger.info('Sentry closed.');
} catch (e) {
logger.error({ error: e.message }, 'Error closing Sentry');
} finally {
process.exit(128 + signals[signal]); // Standard exit code for signals
}
});
} else {
// If server never started, still try to close Sentry and exit
logger.warn('Server was not running, attempting to close Sentry and exit.');
try {
await Sentry.close(2000);
logger.info('Sentry closed (server never started).');
} catch (e) {
logger.error({ error: e.message }, 'Error closing Sentry (server never started)');
} finally {
process.exit(128 + signals[signal]);
}
}
// Force exit after a timeout if graceful shutdown hangs
setTimeout(() => {
logger.warn('Graceful shutdown timed out, forcing exit.');
process.exit(1);
}, 5000); // 5 seconds
}
// Register signal handlers
Object.keys(signals).forEach((signal) => {
process.on(signal, () => gracefulShutdown(signal));
});
+353
View File
@@ -0,0 +1,353 @@
// backend/utils.js
const net = require('net'); // Node.js built-in module for IP validation
const { spawn } = require('child_process');
const pino = require('pino'); // Import pino for logging within utils if needed
const Sentry = require("@sentry/node"); // Import Sentry for error reporting
// Logger instance (assuming a logger is initialized elsewhere and passed or created here)
// For simplicity, creating a basic logger here. Ideally, pass the main logger instance.
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
/**
* Validiert eine IP-Adresse (v4 oder v6) mit Node.js' eingebautem net Modul.
* @param {string} ip - Die zu validierende IP-Adresse.
* @returns {boolean} True, wenn gültig (als v4 oder v6), sonst false.
*/
function isValidIp(ip) {
if (!ip || typeof ip !== 'string' || ip.trim() === '') {
return false;
}
const trimmedIp = ip.trim();
const ipVersion = net.isIP(trimmedIp); // Gibt 0, 4 oder 6 zurück
return ipVersion === 4 || ipVersion === 6;
}
/**
* Prüft, ob eine IP-Adresse im privaten, Loopback- oder Link-Local-Bereich liegt.
* @param {string} ip - Die zu prüfende IP-Adresse (bereits validiert).
* @returns {boolean} True, wenn die IP privat/lokal ist, sonst false.
*/
function isPrivateIp(ip) {
if (!ip) return false;
// Normalize IPv6-mapped IPv4 addresses (e.g., ::ffff:192.168.1.1 -> 192.168.1.1)
if (ip.startsWith('::ffff:')) {
ip = ip.substring(7);
}
const ipVersion = net.isIP(ip);
if (ipVersion === 4) {
const parts = ip.split('.').map(Number);
return (
parts[0] === 10 || // 10.0.0.0/8
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || // 172.16.0.0/12
(parts[0] === 192 && parts[1] === 168) || // 192.168.0.0/16
parts[0] === 127 || // 127.0.0.0/8 (Loopback)
(parts[0] === 169 && parts[1] === 254) || // 169.254.0.0/16 (Link-local)
// Block 0.0.0.0 (Commonly "Any" or "Current Network")
(parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0)
);
} else if (ipVersion === 6) {
const lowerCaseIp = ip.toLowerCase();
return (
lowerCaseIp === '::1' || // ::1/128 (Loopback)
lowerCaseIp === '::' || // ::/128 (Unspecified)
lowerCaseIp.startsWith('fc') || lowerCaseIp.startsWith('fd') || // fc00::/7 (Unique Local)
lowerCaseIp.startsWith('fe8') || lowerCaseIp.startsWith('fe9') || // fe80::/10 (Link-local)
lowerCaseIp.startsWith('fea') || lowerCaseIp.startsWith('feb')
);
}
return false;
}
/**
* Validiert einen Domainnamen (sehr einfache Prüfung).
* @param {string} domain - Der zu validierende Domainname.
* @returns {boolean} True, wenn wahrscheinlich gültig, sonst false.
*/
function isValidDomain(domain) {
if (!domain || typeof domain !== 'string' || domain.trim().length < 3) {
return false;
}
// Regex updated to be more robust and handle international characters (IDNs)
const domainRegex = /^(?:[a-z0-9\p{L}](?:[a-z0-9\p{L}-]{0,61}[a-z0-9\p{L}])?\.)+[a-z0-9\p{L}][a-z0-9\p{L}-]{0,61}[a-z0-9\p{L}]$/iu;
return domainRegex.test(domain.trim());
}
/**
* Validiert eine MAC-Adresse.
* @param {string} mac - Die zu validierende MAC-Adresse.
* @returns {boolean} True, wenn das Format gültig ist, sonst false.
*/
function isValidMacAddress(mac) {
if (!mac || typeof mac !== 'string') {
return false;
}
// This regex matches common MAC address formats (e.g., 00:1A:2B:3C:4D:5E, 00-1A-2B-3C-4D-5E, 001A2B3C4D5E)
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$|^([0-9A-Fa-f]{12})$/;
return macRegex.test(mac.trim());
}
/**
* Bereinigt eine IP-Adresse (z.B. entfernt ::ffff: Präfix von IPv4-mapped IPv6).
* @param {string} ip - Die IP-Adresse.
* @returns {string} Die bereinigte IP-Adresse.
*/
function getCleanIp(ip) {
if (!ip) return ip;
const trimmedIp = ip.trim();
if (trimmedIp.startsWith('::ffff:')) {
const potentialIp4 = trimmedIp.substring(7);
if (net.isIP(potentialIp4) === 4) {
return potentialIp4;
}
}
// Keep localhost IPs as they are
if (trimmedIp === '::1' || trimmedIp === '127.0.0.1') {
return trimmedIp;
}
// Return trimmed IP for other cases
return trimmedIp;
}
/**
* Führt einen Shell-Befehl sicher aus und gibt stdout zurück. (Nur für Ping verwendet)
* @param {string} command - Der Befehl (z.B. 'ping').
* @param {string[]} args - Die Argumente als Array.
* @returns {Promise<string>} Eine Promise, die mit stdout aufgelöst wird.
*/
function executeCommand(command, args) {
return new Promise((resolve, reject) => {
// Basic argument validation
args.forEach(arg => {
if (typeof arg === 'string' && /[;&|`$()<>]/.test(arg)) {
const error = new Error(`Invalid character detected in command argument.`);
logger.error({ command, arg }, "Potential command injection attempt detected in argument");
Sentry.captureException(error); // Send to Sentry
return reject(error);
}
});
const proc = spawn(command, args);
let stdout = '';
let stderr = '';
proc.stdout.on('data', (data) => { stdout += data.toString(); });
proc.stderr.on('data', (data) => { stderr += data.toString(); });
proc.on('error', (err) => {
const error = new Error(`Failed to start command ${command}: ${err.message}`);
logger.error({ command, args, error: err.message }, `Failed to start command`);
Sentry.captureException(error); // Send to Sentry
reject(error);
});
proc.on('close', (code) => {
if (code !== 0) {
const error = new Error(`Command ${command} failed with code ${code}: ${stderr || 'No stderr output'}`);
// Attach stdout/stderr to the error object for better context in rejection
error.stdout = stdout;
error.stderr = stderr;
logger.error({ command, args, exitCode: code, stderr: stderr.trim(), stdout: stdout.trim() }, `Command failed`);
Sentry.captureException(error, { extra: { stdout: stdout.trim(), stderr: stderr.trim() } }); // Send to Sentry
reject(error);
} else {
resolve(stdout);
}
});
});
}
/**
* Parst die Ausgabe des Linux/macOS ping Befehls.
* @param {string} pingOutput - Die rohe stdout Ausgabe von ping.
* @returns {object} Ein Objekt mit geparsten Daten oder Fehlern.
*/
function parsePingOutput(pingOutput) {
const result = {
rawOutput: pingOutput,
stats: null,
error: null,
};
try {
let packetsTransmitted = 0;
let packetsReceived = 0;
let packetLossPercent = 100;
let rtt = { min: null, avg: null, max: null, mdev: null };
const lines = pingOutput.trim().split('\n');
const statsLine = lines.find(line => line.includes('packets transmitted'));
if (statsLine) {
const transmittedMatch = statsLine.match(/(\d+)\s+packets transmitted/);
const receivedMatch = statsLine.match(/(\d+)\s+(?:received|packets received)/);
const lossMatch = statsLine.match(/([\d.]+)%\s+packet loss/);
if (transmittedMatch) packetsTransmitted = parseInt(transmittedMatch[1], 10);
if (receivedMatch) packetsReceived = parseInt(receivedMatch[1], 10);
if (lossMatch) packetLossPercent = parseFloat(lossMatch[1]);
}
// Handle both 'rtt' and 'round-trip' prefixes for broader compatibility
const rttLine = lines.find(line => line.startsWith('rtt min/avg/max/mdev') || line.startsWith('round-trip min/avg/max/stddev'));
if (rttLine) {
const rttMatch = rttLine.match(/([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/);
if (rttMatch) {
rtt = {
min: parseFloat(rttMatch[1]),
avg: parseFloat(rttMatch[2]),
max: parseFloat(rttMatch[3]),
mdev: parseFloat(rttMatch[4]), // Note: mdev/stddev might have different meanings
};
}
}
result.stats = {
packets: { transmitted: packetsTransmitted, received: packetsReceived, lossPercent: packetLossPercent },
rtt: rtt.avg !== null ? rtt : null, // Only include RTT if average is available
};
// Check for common error messages or patterns
if (packetsTransmitted > 0 && packetsReceived === 0) {
result.error = "Request timed out or host unreachable.";
} else if (pingOutput.includes('unknown host') || pingOutput.includes('Name or service not known')) {
result.error = "Unknown host.";
}
} catch (parseError) {
logger.error({ error: parseError.message, output: pingOutput }, "Failed to parse ping output");
Sentry.captureException(parseError, { extra: { pingOutput } }); // Send to Sentry
result.error = "Failed to parse ping output.";
}
return result;
}
/**
* Parst eine einzelne Zeile der Linux/macOS traceroute Ausgabe.
* @param {string} line - Eine Zeile aus stdout.
* @returns {object | null} Ein Objekt mit Hop-Daten oder null bei uninteressanten Zeilen.
*/
function parseTracerouteLine(line) {
line = line.trim();
// Ignore header lines and empty lines
if (!line || line.startsWith('traceroute to') || line.includes('hops max')) return null;
// Regex to capture hop number, hostname (optional), IP address, and RTT times
// Handles cases with or without hostname, and different spacing
const hopMatch = line.match(/^(\s*\d+)\s+(?:([a-zA-Z0-9\.\-]+)\s+\(([\d\.:a-fA-F]+)\)|([\d\.:a-fA-F]+))\s+(.*)$/);
const timeoutMatch = line.match(/^(\s*\d+)\s+(\*\s+\*\s+\*)/); // Match lines with only timeouts
if (timeoutMatch) {
// Handle timeout line
return {
hop: parseInt(timeoutMatch[1].trim(), 10),
hostname: null,
ip: null,
rtt: ['*', '*', '*'], // Represent timeouts as '*'
rawLine: line,
};
} else if (hopMatch) {
// Handle successful hop line
const hop = parseInt(hopMatch[1].trim(), 10);
const hostname = hopMatch[2]; // Hostname if present
const ipInParen = hopMatch[3]; // IP if hostname is present
const ipDirect = hopMatch[4]; // IP if hostname is not present
const restOfLine = hopMatch[5].trim();
const ip = ipInParen || ipDirect; // Determine the correct IP
// Extract RTT times, handling '*' for timeouts and removing ' ms' units
const rttParts = restOfLine.split(/\s+/);
const rtts = rttParts
.map(p => p === '*' ? '*' : p.replace(/\s*ms$/, '')) // Keep '*' or remove ' ms'
.filter(p => p === '*' || !isNaN(parseFloat(p))) // Ensure it's '*' or a number
.slice(0, 3); // Take the first 3 valid RTT values
// Pad with '*' if fewer than 3 RTTs were found (e.g., due to timeouts)
while (rtts.length < 3) rtts.push('*');
return {
hop: hop,
hostname: hostname || null, // Use null if hostname wasn't captured
ip: ip,
rtt: rtts,
rawLine: line,
};
}
// Return null if the line doesn't match expected formats
return null;
}
/**
* Checks if a specific TCP port is open on a given host.
* @param {number} port - The port to check.
* @param {string} host - The target host IP address.
* @param {number} timeout - Connection timeout in milliseconds.
* @returns {Promise<{port: number, status: 'open'|'closed'|'timeout', service: string}>} A promise that resolves with the port status.
*/
function checkPort(port, host, timeout = 2000) {
// A small map of common ports to their services
const commonPorts = {
21: 'FTP', 22: 'SSH', 23: 'Telnet', 25: 'SMTP', 53: 'DNS', 80: 'HTTP',
110: 'POP3', 143: 'IMAP', 443: 'HTTPS', 445: 'SMB', 993: 'IMAPS',
995: 'POP3S', 1433: 'MSSQL', 1521: 'Oracle', 3306: 'MySQL', 3389: 'RDP',
5432: 'PostgreSQL', 5900: 'VNC', 8080: 'HTTP-Alt', 8443: 'HTTPS-Alt'
};
const service = commonPorts[port] || 'Unknown';
return new Promise((resolve, reject) => {
// DEFENSE IN DEPTH: Prevent scanning of private IPs at the function level
if (!isValidIp(host) || isPrivateIp(host)) {
const error = new Error(`Scanning restricted: ${host} is not a valid public IP.`);
logger.warn({ host, port }, "Blocked attempt to scan restricted IP in checkPort");
return resolve({
port,
status: 'error',
service,
error: 'Restricted IP',
details: 'Scanning private or invalid IPs is not allowed.'
});
}
const socket = new net.Socket();
socket.setTimeout(timeout);
socket.on('connect', () => {
socket.destroy();
resolve({ port, status: 'open', service });
});
socket.on('timeout', () => {
socket.destroy();
resolve({ port, status: 'timeout', service });
});
socket.on('error', (err) => {
socket.destroy();
// 'ECONNREFUSED' is the key for a closed port. Other errors might be network issues.
const status = err.code === 'ECONNREFUSED' ? 'closed' : 'error';
resolve({ port, status, service, error: err.code });
});
// Explicit inline guard (defence-in-depth; also satisfies CodeQL SSRF dataflow)
if (!isValidIp(host) || isPrivateIp(host)) {
socket.destroy();
return resolve({ port, status: 'error', service, error: 'Restricted IP' });
}
socket.connect(port, host);
});
}
module.exports = {
isValidIp,
isPrivateIp,
isValidDomain,
isValidMacAddress,
getCleanIp,
executeCommand,
parsePingOutput,
parseTracerouteLine,
checkPort,
};
+13
View File
@@ -0,0 +1,13 @@
docker compose down
# Setzt die Git-Commit-Variable für den Build-Prozess
export GIT_COMMIT_SHA=$(git rev-parse --short HEAD)
export SENTRY_DSN="https://7ea70caba68f548fb96482a573006a7b@o447623.ingest.us.sentry.io/4509062020333568"
# Schritt 1: Baue die Images mit der compose.build.yml
echo "--- Building Docker images ---"
docker compose -f compose.build.yml build
# Schritt 2: Starte die Container mit der compose.yml, die die gebauten Images verwendet
echo "--- Starting containers ---"
docker compose -f compose.yml up -d
+16
View File
@@ -0,0 +1,16 @@
# compose.build.yml
services:
backend:
# Definiert, wie das Image gebaut wird
build:
context: ./backend
args:
- GIT_COMMIT_SHA=${GIT_COMMIT_SHA:-unknown}
- SENTRY_DSN=${SENTRY_DSN:-}
image: mrunknownde/utools-backend:latest
frontend:
# Definiert, wie das Image gebaut wird
build:
context: ./frontend
image: mrunknownde/utools-frontend:latest
+45
View File
@@ -0,0 +1,45 @@
services:
# Backend Service (Node.js App)
backend:
image: mrunknownde/utools-backend
container_name: utools_backend
restart: unless-stopped
environment:
NODE_ENV: production
PORT: 3000
LOG_LEVEL: info
PING_COUNT: 4
SENTRY_DSN: "https://7ea70caba68f548fb96482a573006a7b@o447623.ingest.us.sentry.io/4509062020333568"
# ASN Cache directory (filesystem persistence across restarts)
ASN_CACHE_DIR: /app/asn-cache
volumes:
# Persistent ASN lookup cache — survives container restarts
- asn_cache:/app/asn-cache
dns:
- 1.1.1.1
- 1.0.0.1
- 8.8.8.8
- 8.8.4.4
networks:
- utools_network
# Frontend Service (Nginx)
frontend:
image: mrunknownde/utools-frontend
container_name: utools_frontend
restart: unless-stopped
ports:
- "8080:80"
depends_on:
- backend
networks:
- utools_network
networks:
utools_network:
driver: bridge
# Named volume — ASN cache persists across container restarts
volumes:
asn_cache:
driver: local
+26
View File
@@ -0,0 +1,26 @@
# Stage 1: Build (falls wir später einen Build-Schritt hätten, z.B. für Tailwind Purge)
# Aktuell nicht nötig, da wir CDN/statische Dateien haben.
# Stage 2: Production Environment using Nginx
FROM nginx:1.25-alpine
# Arbeitsverzeichnis im Container (optional, aber gute Praxis)
WORKDIR /usr/share/nginx/html
# Entferne die Standard Nginx Willkommensseite
RUN rm /etc/nginx/conf.d/default.conf
# Kopiere unsere eigene Nginx Konfiguration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Kopiere die Frontend-Dateien in das Verzeichnis, das Nginx ausliefert
COPY app/ .
# Falls du später CSS-Dateien oder Bilder hast, kopiere sie auch:
# COPY styles.css .
# COPY images/ ./images
# Nginx lauscht standardmäßig auf Port 80
EXPOSE 80
# Der Basis-Image startet Nginx bereits. Kein CMD nötig, außer wir wollen Optionen ändern.
# CMD ["nginx", "-g", "daemon off;"] # Standard-CMD im Basis-Image
+471
View File
@@ -0,0 +1,471 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASN / AS Lookup - uTools</title>
<meta name="description"
content="Look up any Autonomous System Number (ASN) to see peering connections, network graph, prefixes and IXP information.">
<script src="https://cdn.tailwindcss.com"></script>
<!-- D3.js v7 for network graph -->
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<style>
.loader {
border: 4px solid rgba(168, 85, 247, 0.1);
border-left-color: #d8b4fe;
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.glass-panel {
background: rgba(17, 24, 39, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(31, 41, 55, 0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
.text-gradient {
background: linear-gradient(to right, #c084fc, #e879f9);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hidden {
display: none !important;
}
nav ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
nav a {
color: #d1d5db;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
nav a:hover {
color: #fff;
background: rgba(168, 85, 247, 0.2);
border-color: rgba(168, 85, 247, 0.4);
}
nav a.active-link {
background: rgba(168, 85, 247, 0.3);
color: #fff;
border-color: #a855f7;
}
header {
background: rgba(31, 41, 55, 0.4);
backdrop-filter: blur(10px);
padding: 1.5rem;
margin-bottom: 2rem;
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
@media (min-width: 768px) {
header {
flex-direction: row;
justify-content: space-between;
}
}
/* ── Network Graph ─────────────────────────────────────── */
#graph-container {
width: 100%;
height: 600px;
background: rgba(0, 0, 0, 0.3);
border-radius: 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.06);
overflow: hidden;
position: relative;
}
#graph-svg {
width: 100%;
height: 100%;
cursor: grab;
}
#graph-svg:active {
cursor: grabbing;
}
.node-center circle {
fill: #a855f7;
stroke: #d8b4fe;
stroke-width: 2.5;
}
.node-upstream circle {
fill: #3b82f6;
stroke: #93c5fd;
stroke-width: 1.5;
}
.node-downstream circle {
fill: #10b981;
stroke: #6ee7b7;
stroke-width: 1.5;
}
.node-tier1 circle {
fill: #6b7280;
stroke: #9ca3af;
stroke-width: 1.5;
}
.node text {
fill: #e5e7eb;
font-size: 11px;
font-family: 'Courier New', monospace;
pointer-events: none;
text-anchor: middle;
}
.node:hover circle {
filter: brightness(1.4);
cursor: pointer;
}
.link {
stroke: rgba(255, 255, 255, 0.12);
stroke-linecap: round;
}
.link-upstream {
stroke: rgba(59, 130, 246, 0.35);
}
.link-tier1 {
stroke: rgba(107, 114, 128, 0.3);
stroke-dasharray: 4 3;
}
.link-downstream {
stroke: rgba(16, 185, 129, 0.35);
}
/* Tooltip */
#graph-tooltip {
position: absolute;
pointer-events: none;
background: rgba(17, 24, 39, 0.95);
backdrop-filter: blur(8px);
border: 1px solid rgba(168, 85, 247, 0.4);
border-radius: 0.5rem;
padding: 0.6rem 0.9rem;
font-size: 12px;
color: #e5e7eb;
max-width: 220px;
z-index: 50;
opacity: 0;
transition: opacity 0.15s;
}
/* Prefix list */
.prefix-tag {
display: inline-block;
font-family: monospace;
font-size: 11px;
background: rgba(168, 85, 247, 0.15);
color: #c084fc;
border: 1px solid rgba(168, 85, 247, 0.3);
border-radius: 4px;
padding: 2px 6px;
margin: 2px;
}
/* IXP table */
.ixp-row {
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.ixp-row:last-child {
border-bottom: none;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
</style>
</head>
<body
class="bg-gray-950 text-gray-100 font-sans p-4 md:p-8 min-h-screen bg-[url('https://tailwindcss.com/_next/static/media/hero-dark.939eb757.png')] bg-cover bg-center bg-fixed selection:bg-purple-500 selection:text-white">
<header class="glass-panel">
<h1>uTools <span class="text-sm font-normal text-gray-400 opacity-75 tracking-wider uppercase ml-2">Network
Suite</span></h1>
<nav>
<ul>
<li><a href="/">IP Info &amp; Tools</a></li>
<li><a href="/subnet">Subnetz Rechner</a></li>
<li><a href="/dns">DNS Lookup</a></li>
<li><a href="/whois">WHOIS Lookup</a></li>
<li><a href="/mac">MAC Lookup</a></li>
<li><a href="/asn" class="active-link">ASN Lookup</a></li>
</ul>
</nav>
</header>
<div
class="container mx-auto max-w-6xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h1 class="text-3xl font-bold mb-2 text-center text-gradient">AS / ASN Lookup</h1>
<p class="text-center text-gray-400 text-sm mb-8">Peering graph, prefixes &amp; IXP connections for any
Autonomous System</p>
<!-- Search -->
<div class="flex flex-col sm:flex-row gap-3 mb-6 max-w-2xl mx-auto">
<input type="text" id="asn-input" placeholder="Enter ASN (e.g. 15169 or AS3320)"
class="flex-grow 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 placeholder-gray-600">
<button id="lookup-button"
class="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">
Lookup
</button>
</div>
<!-- Error -->
<div id="error-box"
class="hidden max-w-2xl mx-auto mb-6 p-4 bg-red-900/30 border border-red-500/40 text-red-300 rounded-lg text-sm">
</div>
<!-- Loading -->
<div id="loading-section" class="hidden flex flex-col items-center gap-4 py-16">
<div class="loader" style="width:40px;height:40px;border-width:5px;"></div>
<p class="text-gray-400 text-sm" id="loading-msg">Querying RIPE Stat &amp; PeeringDB…</p>
<!-- Shown after 3s for slow lookups -->
<div id="loading-hint" class="hidden mt-2 max-w-sm text-center">
<p class="text-xs text-amber-400/80 bg-amber-400/10 border border-amber-400/20 rounded-lg px-4 py-2">
⏳ Large ASes (like Cloudflare, Google, Tier-1 carriers) can take up to 15 seconds on the first
lookup — subsequent lookups are cached for 7 days.
</p>
</div>
</div>
<!-- Results -->
<div id="results-section" class="hidden fade-in">
<!-- AS Info Header -->
<div class="glass-card rounded-xl p-6 mb-6">
<div class="flex flex-col md:flex-row md:items-center gap-4">
<div class="flex-1">
<div class="flex items-center gap-3 mb-1">
<span id="res-asn" class="font-mono text-2xl font-bold text-purple-400"></span>
<span id="res-announced-badge"
class="hidden text-xs px-2 py-0.5 bg-green-500/20 border border-green-500/40 text-green-400 rounded-full">Announced</span>
<span id="res-type-badge"
class="text-xs px-2 py-0.5 bg-blue-500/20 border border-blue-500/40 text-blue-300 rounded-full"></span>
</div>
<h2 id="res-name" class="text-xl font-semibold text-white mb-1"></h2>
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-400 mb-2">
<span id="res-policy-container" class="hidden">Peering Policy: <span id="res-policy"
class="text-gray-200"></span></span>
<span id="res-website-container" class="hidden">Website: <a id="res-website" href="#"
target="_blank"
class="text-purple-400 hover:text-purple-300 transition-colors"></a></span>
</div>
<!-- Rich Info Grid -->
<div id="res-rich-info"
class="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-4 pt-4 border-t border-gray-700/50 hidden">
<div id="res-info-type-container" class="hidden">
<div class="text-xs text-gray-500 uppercase tracking-widest">Type</div>
<div id="res-info-type" class="text-sm text-gray-200 capitalize mt-0.5"></div>
</div>
<div id="res-info-scope-container" class="hidden">
<div class="text-xs text-gray-500 uppercase tracking-widest">Scope</div>
<div id="res-info-scope" class="text-sm text-gray-200 capitalize mt-0.5"></div>
</div>
<div id="res-info-traffic-container" class="hidden">
<div class="text-xs text-gray-500 uppercase tracking-widest">Traffic</div>
<div id="res-info-traffic" class="text-sm text-gray-200 capitalize mt-0.5"></div>
</div>
<div id="res-info-ratio-container" class="hidden">
<div class="text-xs text-gray-500 uppercase tracking-widest">Ratio</div>
<div id="res-info-ratio" class="text-sm text-gray-200 capitalize mt-0.5"></div>
</div>
</div>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 text-center">
<div class="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3">
<div id="res-upstream-count" class="text-xl font-bold text-blue-400"></div>
<div class="text-xs text-gray-400 mt-0.5">Upstreams</div>
</div>
<div class="bg-green-500/10 border border-green-500/20 rounded-lg p-3">
<div id="res-downstream-count" class="text-xl font-bold text-green-400"></div>
<div class="text-xs text-gray-400 mt-0.5">Downstreams</div>
</div>
<div
class="bg-purple-500/10 border border-purple-500/20 rounded-lg p-3 col-span-2 sm:col-span-1">
<div id="res-prefix-count" class="text-xl font-bold text-purple-400"></div>
<div class="text-xs text-gray-400 mt-0.5">Prefixes</div>
</div>
</div>
</div>
</div>
<!-- Network Map -->
<div class="glass-card rounded-xl p-6 mb-6">
<div class="flex items-center gap-3 mb-4">
<h3 class="text-lg font-bold text-purple-300">Network Map</h3>
<div class="flex gap-3 text-xs text-gray-400">
<span class="flex items-center gap-1"><span
class="inline-block w-3 h-3 rounded-full bg-gray-500"></span>Tier-1 / Transit</span>
<span class="flex items-center gap-1"><span
class="inline-block w-3 h-3 rounded-full bg-blue-500"></span>Upstream</span>
<span class="flex items-center gap-1"><span
class="inline-block w-3 h-3 rounded-full bg-purple-500"></span>This AS</span>
<span class="flex items-center gap-1"><span
class="inline-block w-3 h-3 rounded-full bg-green-500"></span>Downstream</span>
</div>
<span class="ml-auto text-xs text-gray-500">Scroll to zoom · Drag to pan · Click node to open</span>
</div>
<div id="graph-container">
<svg id="graph-svg"></svg>
<div id="graph-tooltip"></div>
</div>
</div>
<!-- Prefixes + IXPs side by side -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<!-- Prefixes -->
<div class="glass-card rounded-xl p-5">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest">Announced Prefixes</h3>
<button id="prefix-toggle"
class="text-xs text-purple-400 hover:text-purple-300 transition-colors">Show all</button>
</div>
<div id="prefix-list" class="max-h-48 overflow-y-auto"></div>
<p id="prefix-empty" class="hidden text-sm text-gray-500 italic">No prefix data available.</p>
</div>
<!-- IXPs -->
<div class="glass-card rounded-xl p-5">
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-3">IXP Presence <span
class="text-xs font-normal text-gray-500">(via PeeringDB)</span></h3>
<div id="ixp-list" class="space-y-1 text-sm max-h-48 overflow-y-auto">
<p id="ixp-empty" class="text-gray-500 italic text-sm">Not listed on PeeringDB.</p>
</div>
</div>
</div>
<!-- Direct Peers Table -->
<div class="glass-card rounded-xl p-5">
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-3">Direct Neighbours <span
class="text-xs font-normal text-gray-500">(Level 2 · via RIPE Stat)</span></h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Upstreams -->
<div>
<h4 class="text-xs font-semibold text-blue-400 mb-2 flex items-center gap-1">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M10 17a.75.75 0 01-.75-.75V5.56l-2.47 2.47a.75.75 0 01-1.06-1.06l3.75-3.75a.75.75 0 011.06 0l3.75 3.75a.75.75 0 11-1.06 1.06L10.75 5.56v10.69A.75.75 0 0110 17z"
clip-rule="evenodd" />
</svg>
Upstreams (Transit Providers)
</h4>
<div id="upstream-table" class="space-y-1 text-xs font-mono max-h-52 overflow-y-auto"></div>
</div>
<!-- Downstreams -->
<div>
<h4 class="text-xs font-semibold text-green-400 mb-2 flex items-center gap-1">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M10 3a.75.75 0 01.75.75v10.69l2.47-2.47a.75.75 0 111.06 1.06l-3.75 3.75a.75.75 0 01-1.06 0l-3.75-3.75a.75.75 0 111.06-1.06L9.25 14.44V3.75A.75.75 0 0110 3z"
clip-rule="evenodd" />
</svg>
Downstreams (Customers)
</h4>
<div id="downstream-table" class="space-y-1 text-xs font-mono max-h-52 overflow-y-auto"></div>
</div>
</div>
</div>
</div><!-- /results -->
<footer class="mt-12 pt-6 border-t border-gray-700/30 text-center text-xs text-gray-500">
<p>Data: <a href="https://stat.ripe.net" target="_blank"
class="text-purple-400 hover:text-purple-300 transition-colors">RIPE Stat</a> &amp; <a
href="https://www.peeringdb.com" target="_blank"
class="text-purple-400 hover:text-purple-300 transition-colors">PeeringDB</a> · Cache: 7 days</p>
<p class="mt-1">&copy; 2025 <a href="https://mrunk.de"
class="text-purple-400 hover:text-purple-300 transition-colors">MrUnknownDE</a></p>
</footer>
</div>
<script src="asn-lookup.js"></script>
</body>
</html>
+399
View File
@@ -0,0 +1,399 @@
// frontend/app/asn-lookup.js
'use strict';
const API_BASE = '/api';
// ─── State ────────────────────────────────────────────────────────────────────
let currentData = null;
let showAllPrefixes = false;
// ─── DOM Refs ─────────────────────────────────────────────────────────────────
const asnInput = document.getElementById('asn-input');
const lookupButton = document.getElementById('lookup-button');
const errorBox = document.getElementById('error-box');
const loadingSection = document.getElementById('loading-section');
const loadingMsg = document.getElementById('loading-msg');
const resultsSection = document.getElementById('results-section');
// ─── Helpers ──────────────────────────────────────────────────────────────────
function showError(msg) {
errorBox.textContent = msg;
errorBox.classList.remove('hidden');
loadingSection.classList.add('hidden');
resultsSection.classList.add('hidden');
if (window._loadingHintTimer) clearTimeout(window._loadingHintTimer);
}
function hideError() { errorBox.classList.add('hidden'); }
function setLoading(msg = 'Querying RIPE Stat & PeeringDB…') {
hideError();
loadingMsg.textContent = msg;
loadingSection.classList.remove('hidden');
resultsSection.classList.add('hidden');
// After 3s show a hint that large ASes can be slow
if (window._loadingHintTimer) clearTimeout(window._loadingHintTimer);
window._loadingHintTimer = setTimeout(() => {
const hint = document.getElementById('loading-hint');
if (hint) hint.classList.remove('hidden');
}, 3000);
}
function updateUrlParam(asn) {
const url = new URL(window.location);
url.searchParams.set('asn', asn);
window.history.pushState({}, '', url);
}
// ─── Main Lookup ──────────────────────────────────────────────────────────────
async function doLookup(rawAsn) {
const asn = String(rawAsn || '').trim().toUpperCase().replace(/^AS/, '');
if (!asn || isNaN(Number(asn))) {
showError('Please enter a valid ASN (e.g. 15169 or AS3320).');
return;
}
setLoading('Querying RIPE Stat & PeeringDB…');
updateUrlParam(asn);
try {
const res = await fetch(`${API_BASE}/asn-lookup?asn=${encodeURIComponent(asn)}`);
const data = await res.json();
if (!res.ok || !data.success) {
showError(data.error || `Request failed (HTTP ${res.status})`);
return;
}
currentData = data;
renderResults(data);
} catch (err) {
showError(`Network error: ${err.message}`);
}
}
// ─── Render ───────────────────────────────────────────────────────────────────
function renderResults(data) {
// Show results FIRST so the graph container has real dimensions (clientWidth > 0)
loadingSection.classList.add('hidden');
resultsSection.classList.remove('hidden');
// Reset loading hint for next lookup
if (window._loadingHintTimer) clearTimeout(window._loadingHintTimer);
const hint = document.getElementById('loading-hint');
if (hint) hint.classList.add('hidden');
// Header
document.getElementById('res-asn').textContent = `AS${data.asn}`;
document.getElementById('res-name').textContent = data.name || 'Unknown';
const announcedBadge = document.getElementById('res-announced-badge');
if (announcedBadge) {
if (data.announced) announcedBadge.classList.remove('hidden');
else announcedBadge.classList.add('hidden');
}
const typeBadge = document.getElementById('res-type-badge');
if (typeBadge) {
typeBadge.textContent = data.type || '';
typeBadge.classList.toggle('hidden', !data.type);
}
const peeringPolicy = data.peeringdb?.peeringPolicy;
const policyContainer = document.getElementById('res-policy-container');
const policyEl = document.getElementById('res-policy');
if (policyContainer && policyEl) {
if (peeringPolicy) {
policyEl.textContent = peeringPolicy;
policyContainer.classList.remove('hidden');
} else {
policyContainer.classList.add('hidden');
}
}
const website = data.peeringdb?.website;
const websiteContainer = document.getElementById('res-website-container');
const websiteEl = document.getElementById('res-website');
if (websiteContainer && websiteEl) {
if (website) {
websiteEl.href = website;
websiteEl.textContent = website.replace(/^https?:\/\//, '').replace(/\/$/, '');
websiteContainer.classList.remove('hidden');
} else {
websiteContainer.classList.add('hidden');
}
}
// Rich Info Grid
const richInfo = document.getElementById('res-rich-info');
let hasRichInfo = false;
const fields = [
{ id: 'type', value: data.peeringdb?.infoType },
{ id: 'scope', value: data.peeringdb?.infoScope },
{ id: 'traffic', value: data.peeringdb?.infoTraffic },
{ id: 'ratio', value: data.peeringdb?.infoRatio }
];
fields.forEach(f => {
const container = document.getElementById(`res-info-${f.id}-container`);
const el = document.getElementById(`res-info-${f.id}`);
if (container && el) {
if (f.value) {
el.textContent = f.value;
container.classList.remove('hidden');
hasRichInfo = true;
} else {
container.classList.add('hidden');
}
}
});
if (richInfo) {
if (hasRichInfo) richInfo.classList.remove('hidden');
else richInfo.classList.add('hidden');
}
document.getElementById('res-upstream-count').textContent = data.graph?.level2?.upstreams?.length ?? '?';
document.getElementById('res-downstream-count').textContent = data.graph?.level2?.downstreams?.length ?? '?';
document.getElementById('res-prefix-count').textContent = data.prefixes?.length ?? '?';
// Prefixes + IXPs (before graph — these are cheap)
renderPrefixes(data.prefixes);
renderIxps(data.peeringdb?.ixps);
renderNeighbourTable('upstream-table', data.graph?.level2?.upstreams ?? [], 'blue');
renderNeighbourTable('downstream-table', data.graph?.level2?.downstreams ?? [], 'green');
// Graph LAST — needs the container to be visible for clientWidth
if (data.graph) renderGraph(data.graph);
}
// ─── Prefix List ─────────────────────────────────────────────────────────────
function renderPrefixes(prefixes) {
const list = document.getElementById('prefix-list');
const empty = document.getElementById('prefix-empty');
const toggle = document.getElementById('prefix-toggle');
if (!prefixes || prefixes.length === 0) {
list?.classList.add('hidden');
empty?.classList.remove('hidden');
toggle?.classList.add('hidden');
return;
}
empty?.classList.add('hidden');
toggle?.classList.remove('hidden');
const toShow = showAllPrefixes ? prefixes : prefixes.slice(0, 20);
if (list) list.innerHTML = toShow.map(p => `<span class="prefix-tag">${p}</span>`).join('');
if (toggle) toggle.textContent = showAllPrefixes ? 'Show less' : `Show all (${prefixes.length})`;
}
document.getElementById('prefix-toggle').addEventListener('click', () => {
showAllPrefixes = !showAllPrefixes;
if (currentData) renderPrefixes(currentData.prefixes);
});
// ─── IXP List ─────────────────────────────────────────────────────────────────
function renderIxps(ixps) {
const list = document.getElementById('ixp-list');
const empty = document.getElementById('ixp-empty');
if (!ixps || ixps.length === 0) {
if (list) list.innerHTML = '';
empty?.classList.remove('hidden');
return;
}
empty?.classList.add('hidden');
if (list) {
list.innerHTML = ixps.map(ix => `
<div class="ixp-row py-1.5 flex items-center justify-between gap-2 text-sm">
<span class="text-gray-200 truncate">${ix.name}</span>
<span class="text-xs text-gray-500 shrink-0">${ix.speed >= 1000 ? `${ix.speed / 1000}G` : `${ix.speed}M`}</span>
</div>
`).join('');
}
}
// ─── Neighbour Table ──────────────────────────────────────────────────────────
function renderNeighbourTable(elId, nodes, colour) {
const el = document.getElementById(elId);
if (!nodes || nodes.length === 0) {
el.innerHTML = `<p class="text-gray-500 italic">None reported.</p>`;
return;
}
const colClass = colour === 'blue' ? 'text-blue-400' : 'text-green-400';
el.innerHTML = nodes.map(n => `
<div class="flex items-center gap-2 py-0.5 hover:bg-white/5 rounded px-1 cursor-pointer group"
onclick="window.location.href='/asn?asn=${n.asn}'">
<span class="${colClass} font-bold w-14 shrink-0">AS${n.asn}</span>
<span class="text-gray-300 truncate flex-1 group-hover:text-white">${n.name || '—'}</span>
<span class="text-gray-600 shrink-0">${n.power ? `pwr:${n.power}` : ''}</span>
</div>
`).join('');
}
// ─── D3 Network Graph ─────────────────────────────────────────────────────────
function renderGraph(graph) {
const container = document.getElementById('graph-container');
const svg = d3.select('#graph-svg');
svg.selectAll('*').remove();
const W = container.clientWidth;
const H = container.clientHeight;
// ── Build nodes & links ───────────────────────────────────────────────────
const nodeMap = new Map();
function addNode(asn, name, role) {
const key = String(asn);
if (!nodeMap.has(key)) nodeMap.set(key, { id: key, asn, name: name || `AS${asn}`, role });
}
addNode(graph.center.asn, graph.center.name, 'center');
// Limit graph nodes to top 15 to prevent Physics Engine crash & unreadable hairball
const vizUpstreams = graph.level2.upstreams.slice(0, 15);
const vizDownstreams = graph.level2.downstreams.slice(0, 15);
vizUpstreams.forEach(n => addNode(n.asn, n.name, 'upstream'));
vizDownstreams.forEach(n => addNode(n.asn, n.name, 'downstream'));
graph.level3.forEach(d => {
d.upstreams.forEach(n => addNode(n.asn, n.name, 'tier1'));
});
const nodes = Array.from(nodeMap.values());
const links = [];
const centerId = String(graph.center.asn);
vizUpstreams.forEach(n => {
links.push({ source: String(n.asn), target: centerId, type: 'upstream', power: n.power || 1 });
});
vizDownstreams.forEach(n => {
links.push({ source: centerId, target: String(n.asn), type: 'downstream', power: n.power || 1 });
});
graph.level3.forEach(d => {
d.upstreams.forEach(n => {
links.push({ source: String(n.asn), target: String(d.parentAsn), type: 'tier1', power: n.power || 1 });
});
});
// Remove duplicate links
const uniqueLinks = Array.from(
new Map(links.map(l => [`${l.source}-${l.target}`, l])).values()
);
// ── Layer X positions (fixed horizontal layout) ────────────────────────
const layerX = { tier1: W * 0.08, upstream: W * 0.3, center: W * 0.55, downstream: W * 0.8 };
nodes.forEach(n => {
n.fx = layerX[n.role] ?? W / 2;
});
// ── Power → stroke width ──────────────────────────────────────────────
const maxPower = Math.max(...uniqueLinks.map(l => l.power), 1);
const strokeScale = d3.scaleLinear().domain([0, maxPower]).range([0.5, 4]);
// ── Node size ─────────────────────────────────────────────────────────
const nodeRadius = { center: 20, upstream: 11, downstream: 11, tier1: 8 };
// ── Simulation ────────────────────────────────────────────────────────
const sim = d3.forceSimulation(nodes)
.force('link', d3.forceLink(uniqueLinks).id(d => d.id).distance(d => {
if (d.type === 'tier1') return 90;
if (d.type === 'upstream') return 130;
return 110;
}).strength(0.6))
.force('charge', d3.forceManyBody().strength(-220))
.force('y', d3.forceY(H / 2).strength(0.04))
.force('collide', d3.forceCollide().radius(d => nodeRadius[d.role] + 14))
.alphaDecay(0.025);
// ── Zoom/Pan ──────────────────────────────────────────────────────────
const g = svg.append('g');
svg.call(d3.zoom().scaleExtent([0.3, 3]).on('zoom', evt => g.attr('transform', evt.transform)));
// ── Draw links ────────────────────────────────────────────────────────
const link = g.append('g').selectAll('line')
.data(uniqueLinks).join('line')
.attr('class', d => `link link-${d.type}`)
.attr('stroke-width', d => strokeScale(d.power));
// ── Draw nodes ────────────────────────────────────────────────────────
const tooltip = document.getElementById('graph-tooltip');
const node = g.append('g').selectAll('g')
.data(nodes).join('g')
.attr('class', d => `node node-${d.role}`)
.style('cursor', 'pointer')
.on('click', (_, d) => {
if (d.role !== 'center') window.location.href = `/asn?asn=${d.asn}`;
})
.on('mouseenter', (evt, d) => {
tooltip.style.opacity = '1';
tooltip.innerHTML = `
<strong class="text-purple-300">AS${d.asn}</strong><br>
<span class="text-gray-300">${d.name}</span><br>
<span class="text-gray-500 text-xs capitalize">${d.role === 'tier1' ? 'Tier-1 / Transit' : d.role}</span>
`;
})
.on('mousemove', (evt) => {
const rect = container.getBoundingClientRect();
let x = evt.clientX - rect.left + 14;
let y = evt.clientY - rect.top - 10;
if (x + 230 > W) x = x - 244;
tooltip.style.left = x + 'px';
tooltip.style.top = y + 'px';
})
.on('mouseleave', () => { tooltip.style.opacity = '0'; })
.call(d3.drag()
.on('start', (evt, d) => { if (!evt.active) sim.alphaTarget(0.3).restart(); d.fy = d.y; })
.on('drag', (evt, d) => { d.fy = evt.y; })
.on('end', (evt, d) => { if (!evt.active) sim.alphaTarget(0); d.fy = null; })
);
node.append('circle').attr('r', d => nodeRadius[d.role]);
// Label: 2 lines (ASN + name truncated)
node.append('text')
.attr('dy', d => nodeRadius[d.role] + 13)
.attr('font-size', d => d.role === 'center' ? 12 : 9)
.attr('fill', '#e5e7eb')
.text(d => `AS${d.asn}`);
// Name label for ALL roles (tier1 gets shorter truncation)
node.append('text')
.attr('dy', d => nodeRadius[d.role] + 23)
.attr('font-size', d => d.role === 'tier1' ? 7 : 8)
.attr('fill', d => d.role === 'tier1' ? '#6b7280' : '#9ca3af')
.text(d => {
if (!d.name) return '';
const max = d.role === 'center' ? 22 : d.role === 'tier1' ? 12 : 16;
return d.name.length > max ? d.name.slice(0, max) + '…' : d.name;
});
// ── Tick ──────────────────────────────────────────────────────────────
sim.on('tick', () => {
// Clamp Y so nodes don't fly off
nodes.forEach(n => { n.y = Math.max(30, Math.min(H - 30, n.y)); });
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node.attr('transform', d => `translate(${d.x},${d.y})`);
});
}
// ─── Initialise ───────────────────────────────────────────────────────────────
lookupButton.addEventListener('click', () => doLookup(asnInput.value));
asnInput.addEventListener('keypress', e => { if (e.key === 'Enter') doLookup(asnInput.value); });
// Auto-lookup from URL
const urlParam = new URLSearchParams(window.location.search).get('asn');
if (urlParam) {
asnInput.value = urlParam;
doLookup(urlParam);
}
+252
View File
@@ -0,0 +1,252 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DNS Lookup - uTools</title>
<!-- Tailwind CSS Play CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Eigene Styles -->
<style>
/* Einfacher Lade-Spinner (Tailwind animiert) */
.loader {
border: 4px solid rgba(168, 85, 247, 0.1);
/* Lila sehr transparent */
border-left-color: #d8b4fe;
/* Helleres Lila */
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Glassmorphism Utilities */
.glass-panel {
background: rgba(17, 24, 39, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(31, 41, 55, 0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* Ergebnis-Pre-Formatierung */
.result-pre {
white-space: pre-wrap;
word-break: break-all;
font-family: 'Courier New', Courier, monospace;
background-color: rgba(0, 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, 0.05);
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.3);
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* Navigations-Styling */
nav ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
nav a {
color: #d1d5db;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
nav a:hover {
color: #fff;
background: rgba(168, 85, 247, 0.2);
border-color: rgba(168, 85, 247, 0.4);
box-shadow: 0 0 15px rgba(168, 85, 247, 0.1);
}
nav a.active-link {
background: rgba(168, 85, 247, 0.3);
color: #fff;
border-color: #a855f7;
}
header {
background: rgba(31, 41, 55, 0.4);
backdrop-filter: blur(10px);
padding: 1.5rem;
margin-bottom: 2rem;
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
header {
flex-direction: row;
justify-content: space-between;
}
}
header h1 {
background-clip: text;
}
.text-gradient {
background: linear-gradient(to right, #c084fc, #e879f9);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hidden {
display: none !important;
}
</style>
</head>
<body
class="bg-gray-950 text-gray-100 font-sans p-4 md:p-8 min-h-screen bg-[url('https://tailwindcss.com/_next/static/media/hero-dark.939eb757.png')] bg-cover bg-center bg-fixed selection:bg-purple-500 selection:text-white">
<header class="glass-panel">
<h1>uTools <span class="text-sm font-normal text-gray-400 opacity-75 tracking-wider uppercase ml-2">Network
Suite</span></h1>
<nav>
<ul>
<li><a href="/">IP Info &amp; Tools</a></li>
<li><a href="/subnet">Subnetz Rechner</a></li>
<li><a href="/dns" class="active-link">DNS Lookup</a></li>
<li><a href="/whois">WHOIS Lookup</a></li>
<li><a href="/mac">MAC Lookup</a></li>
<li><a href="/asn">ASN Lookup</a></li>
</ul>
</nav>
</header>
<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">
<h1 class="text-3xl font-bold mb-8 text-center text-gradient glitch-text">DNS Lookup</h1>
<!-- Bereich für DNS Lookup -->
<div class="mt-8 p-6 glass-card rounded-xl">
<div class="flex flex-col sm:flex-row gap-3 mb-6">
<input type="text" id="dns-domain-input" placeholder="Enter domain name (e.g., google.com)"
class="flex-grow 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 placeholder-gray-600">
<select id="dns-type-select"
class="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 cursor-pointer">
<option value="ANY">ANY</option>
<option value="A">A</option>
<option value="AAAA">AAAA</option>
<option value="MX">MX</option>
<option value="TXT">TXT</option>
<option value="NS">NS</option>
<option value="CNAME">CNAME</option>
<option value="SOA">SOA</option>
<option value="SRV">SRV</option>
<option value="PTR">PTR (Reverse)</option>
</select>
<button id="dns-lookup-button"
class="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">
Lookup DNS
</button>
</div>
<div id="dns-lookup-error"
class="text-red-400 mb-4 hidden p-3 bg-red-900/20 border border-red-500/30 rounded"></div>
<div id="dns-lookup-results-section" class="hidden mt-6 border-t border-gray-700/50 pt-6 fade-in">
<h3 class="text-lg font-semibold text-purple-300 mb-4 flex items-center gap-2">
<div class="w-1.5 h-6 bg-purple-500 rounded-full"></div>
DNS Results for: <span id="dns-lookup-query" class="font-mono text-purple-400 ml-1"></span>
</h3>
<div id="dns-lookup-loader" class="loader hidden mb-4"></div>
<pre id="dns-lookup-output" class="result-pre custom-scrollbar"></pre> <!-- Ergebnisbereich -->
</div>
</div>
<!-- Globaler Fehlerbereich -->
<div id="global-error"
class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg">
</div>
<!-- Footer für Version -->
<footer class="mt-12 pt-6 border-t border-gray-700/30 text-center text-xs text-gray-500">
<p>&copy; 2025 <a href="https://mrunk.de"
class="text-purple-400 hover:text-purple-300 transition-colors">MrUnknownDE</a></p>
<p class="mt-1">Version: <span id="commit-sha" class="font-mono text-gray-400">loading...</span></p>
</footer>
</div>
<!-- Eigene JS-Logik für diese Seite -->
<script src="dns-lookup.js"></script>
</body>
</html>
+188
View File
@@ -0,0 +1,188 @@
// frontend/dns-lookup.js
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Elements (DNS Lookup) ---
const dnsDomainInput = document.getElementById('dns-domain-input');
const dnsTypeSelect = document.getElementById('dns-type-select');
const dnsLookupButton = document.getElementById('dns-lookup-button');
const dnsLookupErrorEl = document.getElementById('dns-lookup-error');
const dnsLookupResultsSection = document.getElementById('dns-lookup-results-section');
const dnsLookupQueryEl = document.getElementById('dns-lookup-query');
const dnsLookupLoader = document.getElementById('dns-lookup-loader');
const dnsLookupOutputEl = document.getElementById('dns-lookup-output');
// --- DOM Elements (Common) ---
const globalErrorEl = document.getElementById('global-error');
const commitShaEl = document.getElementById('commit-sha');
// --- Configuration ---
const API_BASE_URL = '/api'; // Anpassen, falls nötig
// --- Helper Functions ---
/** Zeigt globale Fehler an */
function showGlobalError(message) {
if (!globalErrorEl) return;
globalErrorEl.textContent = `Error: ${message}`;
globalErrorEl.classList.remove('hidden');
}
/** Versteckt globale Fehler */
function hideGlobalError() {
if (!globalErrorEl) return;
globalErrorEl.classList.add('hidden');
}
/**
* Generische Funktion zum Abrufen und Anzeigen von Lookup-Ergebnissen.
* @param {string} endpoint - Der API-Endpunkt (z.B. '/dns-lookup').
* @param {object} params - Query-Parameter als Objekt (z.B. { domain: '...', type: '...' }).
* @param {HTMLElement} resultsSection - Der Container für die Ergebnisse.
* @param {HTMLElement} loaderElement - Das Loader-Element.
* @param {HTMLElement} errorElement - Das Fehleranzeige-Element für diesen Lookup.
* @param {HTMLElement} queryElement - Das Element zur Anzeige der Suchanfrage.
* @param {HTMLElement} outputElement - Das Element zur Anzeige der Ergebnisse (<pre> oder <p>).
* @param {function} displayFn - Funktion zur Formatierung und Anzeige der Daten im outputElement.
*/
async function fetchAndDisplay(endpoint, params, resultsSection, loaderElement, errorElement, queryElement, outputElement, displayFn) {
// Reset animation
resultsSection.classList.remove('fade-in');
void resultsSection.offsetWidth; // Trigger reflow
resultsSection.classList.add('fade-in');
resultsSection.classList.remove('hidden');
loaderElement.classList.remove('hidden');
errorElement.classList.add('hidden');
outputElement.textContent = ''; // Clear previous results
if (queryElement) queryElement.textContent = Object.values(params).join(', '); // Display query
hideGlobalError(); // Hide global errors before new request
const urlParams = new URLSearchParams(params);
const url = `${API_BASE_URL}${endpoint}?${urlParams.toString()}`;
try {
const response = await fetch(url);
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || `Request failed with status ${response.status}`);
}
console.log(`Received ${endpoint} data:`, data);
displayFn(data, outputElement); // Call the specific display function
} catch (error) {
console.error(`Failed to fetch ${endpoint}:`, error);
errorElement.textContent = `Error: ${error.message}`;
errorElement.classList.remove('hidden');
outputElement.textContent = ''; // Clear output on error
} finally {
loaderElement.classList.add('hidden');
}
}
/** Ruft die Versionsinformationen (Commit SHA) ab */
async function fetchVersionInfo() {
if (!commitShaEl) return; // Don't fetch if element doesn't exist
try {
const response = await fetch(`${API_BASE_URL}/version`);
if (!response.ok) throw new Error(`Network response: ${response.statusText} (${response.status})`);
const data = await response.json();
commitShaEl.textContent = data.commitSha || 'unknown';
} catch (error) {
console.error('Failed to fetch version info:', error);
commitShaEl.textContent = 'error';
// Optionally show global error
// showGlobalError(`Could not load version info: ${error.message}`);
}
}
// --- URL Parameter Functions ---
/**
* Updates the URL with DNS lookup parameters
* @param {string} domain - The domain name
* @param {string} type - The DNS record type
*/
function updateUrlParams(domain, type) {
const url = new URL(window.location);
url.searchParams.set('domain', domain);
url.searchParams.set('type', type);
window.history.pushState({}, '', url);
}
/**
* Reads URL parameters and returns domain and type if present
* @returns {object|null} Object with domain and type, or null if not present
*/
function getUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
const domain = urlParams.get('domain');
const type = urlParams.get('type');
if (domain && type) {
return { domain, type };
}
return null;
}
// --- DNS Lookup Specific Functions ---
function displayDnsResults(data, outputEl) {
if (!data.records || Object.keys(data.records).length === 0) {
outputEl.textContent = 'No records found for this domain and type.';
return;
}
// Format output as JSON string for simplicity
outputEl.textContent = JSON.stringify(data.records, null, 2);
}
function handleDnsLookupClick() {
const domain = dnsDomainInput.value.trim();
const type = dnsTypeSelect.value;
if (!domain) {
dnsLookupErrorEl.textContent = 'Please enter a domain name.';
dnsLookupErrorEl.classList.remove('hidden');
return;
}
// Update URL with parameters
updateUrlParams(domain, type);
fetchAndDisplay(
'/dns-lookup',
{ domain, type },
dnsLookupResultsSection,
dnsLookupLoader,
dnsLookupErrorEl,
dnsLookupQueryEl,
dnsLookupOutputEl,
displayDnsResults
);
}
/**
* Executes DNS lookup from URL parameters if they exist
*/
function executeLookupFromUrl() {
const params = getUrlParams();
if (params) {
// Populate the input fields
dnsDomainInput.value = params.domain;
dnsTypeSelect.value = params.type;
// Trigger the lookup
handleDnsLookupClick();
}
}
// --- Initial Load & Event Listeners ---
fetchVersionInfo(); // Lade Versionsinfo für Footer
dnsLookupButton.addEventListener('click', handleDnsLookupClick);
dnsDomainInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') handleDnsLookupClick();
});
// Execute lookup from URL parameters if present
executeLookupFromUrl();
}); // End DOMContentLoaded
+580
View File
@@ -0,0 +1,580 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IP Info & Network Tools - uTools</title> <!-- Titel angepasst -->
<!-- Tailwind CSS Play CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<!-- Eigene Styles -->
<style>
/* Container für Karten müssen eine Höhe haben */
#map {
height: 300px;
}
#lookup-map {
height: 250px;
}
/* Höhe für Lookup-Karte */
/* Einfacher Lade-Spinner (Tailwind animiert) */
.loader {
border: 4px solid rgba(168, 85, 247, 0.1);
/* Lila sehr transparent */
border-left-color: #d8b4fe;
/* Helleres Lila */
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Glassmorphism Utilities */
.glass-panel {
background: rgba(17, 24, 39, 0.7);
/* gray-900 mit Opacity */
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(31, 41, 55, 0.6);
/* gray-800 mit Opacity */
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* Basis für Glitch-Effekt (Beispiel: Text-Schatten) */
.glitch-text:hover {
text-shadow:
2px 2px 0px rgba(168, 85, 247, 0.4),
-2px -2px 0px rgba(236, 72, 153, 0.4);
}
/* Klickbarer IP-Cursor und Link-Styling */
#ip-address-link {
cursor: pointer;
text-decoration: none;
position: relative;
display: inline-block;
transition: color 0.3s ease;
}
#ip-address-link::after {
content: '';
position: absolute;
width: 100%;
transform: scaleX(0);
height: 2px;
bottom: 0;
left: 0;
background-color: #d8b4fe;
transform-origin: bottom right;
transition: transform 0.25s ease-out;
}
#ip-address-link:hover::after {
transform: scaleX(1);
transform-origin: bottom left;
}
/* Traceroute Output Formatierung */
#traceroute-output pre,
.result-pre {
white-space: pre-wrap;
word-break: break-all;
font-family: 'Courier New', Courier, monospace;
/* Mehr Terminal-Feeling */
background-color: rgba(0, 0, 0, 0.3);
/* Transparenter */
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, 0.05);
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.3);
}
#traceroute-output .hop-line {
margin-bottom: 0.25rem;
padding-left: 0.5rem;
border-left: 2px solid transparent;
transition: border-left-color 0.3s;
}
#traceroute-output .hop-line:hover {
border-left-color: #a855f7;
background: rgba(255, 255, 255, 0.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;
}
/* Helleres Lila */
#traceroute-output .hop-rtt {
color: #34d399;
margin-left: 8px;
font-size: 0.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: 0.05em;
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding-top: 10px;
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* Navigations-Styling */
nav ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
nav a {
color: #d1d5db;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
nav a:hover {
color: #fff;
background: rgba(168, 85, 247, 0.2);
/* Purple tint */
border-color: rgba(168, 85, 247, 0.4);
box-shadow: 0 0 15px rgba(168, 85, 247, 0.1);
}
nav a.active-link {
/* Optional active state */
background: rgba(168, 85, 247, 0.3);
color: #fff;
border-color: #a855f7;
}
header {
background: rgba(31, 41, 55, 0.4);
/* Sehr transparent */
backdrop-filter: blur(10px);
padding: 1.5rem;
margin-bottom: 2rem;
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
@media (min-width: 768px) {
header {
flex-direction: row;
justify-content: space-between;
}
}
/* Standard property for compatibility */
header h1 {
background-clip: text;
}
.text-gradient {
background: linear-gradient(to right, #c084fc, #e879f9);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hidden {
display: none !important;
}
</style>
</head>
<body
class="bg-gray-950 text-gray-100 font-sans p-4 md:p-8 min-h-screen bg-[url('https://tailwindcss.com/_next/static/media/hero-dark.939eb757.png')] bg-cover bg-center bg-fixed selection:bg-purple-500 selection:text-white">
<header class="glass-panel">
<h1>uTools <span class="text-sm font-normal text-gray-400 opacity-75 tracking-wider uppercase ml-2">Network
Suite</span></h1>
<nav>
<ul>
<li><a href="/" class="active-link">IP Info &amp; Tools</a></li>
<li><a href="/subnet">Subnetz Rechner</a></li>
<li><a href="/dns">DNS Lookup</a></li>
<li><a href="/whois">WHOIS Lookup</a></li>
<li><a href="/mac">MAC Lookup</a></li>
<li><a href="/asn">ASN Lookup</a></li>
</ul>
</nav>
</header>
<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">
<h1 class="text-3xl font-bold mb-8 text-center text-gradient glitch-text">Your Digital Footprint</h1>
<!-- Bereich für EIGENE IP-Infos -->
<div id="info-section" class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
<!-- Linke Spalte: Eigene IP, Geo, ASN, rDNS -->
<div class="space-y-6 fade-in" style="animation-delay: 0.1s;">
<!-- IP Card -->
<div class="glass-card rounded-lg p-5 relative overflow-hidden group">
<div class="absolute top-0 right-0 p-2 opacity-10 group-hover:opacity-20 transition-opacity">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-purple-500" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
</div>
<h2 class="text-xs font-bold text-purple-400 uppercase tracking-widest mb-2">Your Public IP</h2>
<div id="ip-info" class="min-h-[40px] flex items-center">
<div id="ip-loader" class="loader"></div>
<a id="ip-address-link" href="#"
class="text-3xl font-mono font-bold text-white tracking-tight break-all hidden hover:text-purple-300 transition-colors"
title="Click for WHOIS Lookup">
<span id="ip-address"></span>
</a>
</div>
</div>
<!-- Geo/ASN Combo Card -->
<div class="glass-card rounded-lg p-5 space-y-4">
<div>
<h2
class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2 border-b border-gray-700 pb-1">
Location Details</h2>
<div id="geo-info" class="min-h-[80px] space-y-1 text-sm text-gray-300">
<div id="geo-loader" class="loader"></div>
<div class="hidden grid grid-cols-2 gap-x-2 gap-y-1">
<p><span class="text-gray-500">Country:</span> <span id="country"
class="text-gray-200 font-medium">-</span></p>
<p><span class="text-gray-500">Region:</span> <span id="region"
class="text-gray-200 font-medium">-</span></p>
<p><span class="text-gray-500">City:</span> <span id="city"
class="text-gray-200 font-medium">-</span></p>
<p><span class="text-gray-500">Zip:</span> <span id="postal"
class="text-gray-200 font-medium">-</span></p>
<p class="col-span-2"><span class="text-gray-500">Coords:</span> <span id="coords"
class="font-mono text-xs text-purple-300">-</span></p>
<p class="col-span-2"><span class="text-gray-500">Time:</span> <span id="timezone"
class="text-gray-200 font-medium">-</span></p>
<p id="geo-error" class="text-red-400 col-span-2 text-xs"></p>
</div>
</div>
</div>
<div>
<h2
class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2 border-b border-gray-700 pb-1">
Network (ASN)</h2>
<div id="asn-info" class="min-h-[40px] space-y-1 text-sm text-gray-300">
<div id="asn-loader" class="loader"></div>
<div class="hidden">
<p><span class="text-gray-500">AS Number:</span> <span id="asn-number"
class="font-mono text-purple-300">-</span></p>
<p><span class="text-gray-500">Org:</span> <span id="asn-org"
class="font-medium text-white">-</span></p>
<p id="asn-error" class="text-red-400 text-xs"></p>
</div>
</div>
</div>
</div>
<!-- rDNS Card -->
<div class="glass-card rounded-lg p-5">
<h2
class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2 border-b border-gray-700 pb-1">
Hostname (rDNS)</h2>
<div id="rdns-info" class="min-h-[30px] text-sm text-gray-300">
<div id="rdns-loader" class="loader"></div>
<div class="hidden">
<ul id="rdns-list" class="list-none space-y-1 font-mono text-xs text-green-400">
<li>-</li>
</ul>
<p id="rdns-error" class="text-red-400 text-xs"></p>
</div>
</div>
</div>
</div>
<!-- Rechte Spalte: Eigene Karte -->
<div class="space-y-4 fade-in" style="animation-delay: 0.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">
<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
</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>
</div>
</div>
<!-- Bereich für 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">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-pink-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
IP Address / Domain Lookup
</h2>
<div class="flex flex-col sm:flex-row gap-3 mb-6">
<input type="text" id="lookup-ip-input" placeholder="Enter IP or Domain (e.g., 8.8.8.8 or google.com)"
class="flex-grow 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 placeholder-gray-600">
<button id="lookup-button"
class="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">
Lookup
</button>
</div>
<div id="lookup-error" class="text-red-400 mb-4 hidden p-3 bg-red-900/20 border border-red-500/30 rounded">
</div>
<!-- Ergebnisse des Lookups (initial versteckt) -->
<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">
<!-- Linke Spalte: IP, Geo, ASN, rDNS -->
<div class="space-y-6">
<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></h3>
<div id="lookup-result-loader" class="loader hidden"></div>
<div id="lookup-geo-info" class="space-y-1 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>
<p><span class="text-gray-500">Region:</span> <span id="lookup-region"
class="text-white">-</span></p>
<p><span class="text-gray-500">City:</span> <span id="lookup-city"
class="text-white">-</span></p>
<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>
</div>
</div>
<!-- Rechte Spalte: Karte & Aktionen -->
<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>
<!-- Action Buttons -->
<div class="flex flex-wrap gap-2 pt-2">
<button id="lookup-ping-button"
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"
disabled>Ping</button>
<button id="lookup-trace-button"
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"
disabled>Trace</button>
<button id="lookup-scan-button"
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"
disabled>Port Scan</button>
</div>
<!-- Bereich für Ping-Ergebnisse (Lookup) -->
<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>
</div>
</div>
</div>
<!-- Bereich für 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 id="traceroute-status" class="flex items-center mb-4 text-sm">
<div id="traceroute-loader" class="loader mr-3 hidden"></div>
<span id="traceroute-message" class="text-gray-300"></span>
</div>
<div id="traceroute-output" class="rounded-lg overflow-hidden custom-scrollbar">
<pre class="m-0"></pre>
</div>
</div>
<!-- Bereich für 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>
<div id="port-scan-status" class="flex items-center mb-4 text-sm">
<div id="port-scan-loader" class="loader mr-3 hidden"></div>
<span id="port-scan-message" class="text-gray-300"></span>
</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>
<!-- Globaler Fehlerbereich -->
<div id="global-error"
class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg">
</div>
<!-- Footer für Version -->
<footer class="mt-12 pt-6 border-t border-gray-700/30 text-center text-xs text-gray-500">
<p>&copy; 2025 <a href="https://mrunk.de"
class="text-purple-400 hover:text-purple-300 transition-colors">MrUnknownDE</a></p>
<p class="mt-1">Version: <span id="commit-sha" class="font-mono text-gray-400">loading...</span></p>
</footer>
</div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<!-- Eigene JS-Logik -->
<script src="script.js"></script>
</body>
</html>
+236
View File
@@ -0,0 +1,236 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MAC Vendor Lookup - uTools</title>
<!-- Tailwind CSS Play CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Eigene Styles -->
<style>
/* Einfacher Lade-Spinner (Tailwind animiert) */
.loader {
border: 4px solid rgba(168, 85, 247, 0.1);
/* Lila sehr transparent */
border-left-color: #d8b4fe;
/* Helleres Lila */
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Glassmorphism Utilities */
.glass-panel {
background: rgba(17, 24, 39, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(31, 41, 55, 0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* Ergebnis-Pre-Formatierung */
.result-pre {
white-space: pre-wrap;
word-break: break-all;
font-family: 'Courier New', Courier, monospace;
background-color: rgba(0, 0, 0, 0.3);
color: #e5e7eb;
padding: 1rem;
border-radius: 0.375rem;
max-height: 400px;
overflow-y: auto;
font-size: 1.25rem;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.3);
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* Navigations-Styling */
nav ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
nav a {
color: #d1d5db;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
nav a:hover {
color: #fff;
background: rgba(168, 85, 247, 0.2);
border-color: rgba(168, 85, 247, 0.4);
box-shadow: 0 0 15px rgba(168, 85, 247, 0.1);
}
nav a.active-link {
background: rgba(168, 85, 247, 0.3);
color: #fff;
border-color: #a855f7;
}
header {
background: rgba(31, 41, 55, 0.4);
backdrop-filter: blur(10px);
padding: 1.5rem;
margin-bottom: 2rem;
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
header {
flex-direction: row;
justify-content: space-between;
}
}
header h1 {
background-clip: text;
}
.text-gradient {
background: linear-gradient(to right, #c084fc, #e879f9);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hidden {
display: none !important;
}
</style>
</head>
<body
class="bg-gray-950 text-gray-100 font-sans p-4 md:p-8 min-h-screen bg-[url('https://tailwindcss.com/_next/static/media/hero-dark.939eb757.png')] bg-cover bg-center bg-fixed selection:bg-purple-500 selection:text-white">
<header class="glass-panel">
<h1>uTools <span class="text-sm font-normal text-gray-400 opacity-75 tracking-wider uppercase ml-2">Network
Suite</span></h1>
<nav>
<ul>
<li><a href="/">IP Info &amp; Tools</a></li>
<li><a href="/subnet">Subnetz Rechner</a></li>
<li><a href="/dns">DNS Lookup</a></li>
<li><a href="/whois">WHOIS Lookup</a></li>
<li><a href="/mac" class="active-link">MAC Lookup</a></li>
<li><a href="/asn">ASN Lookup</a></li>
</ul>
</nav>
</header>
<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">
<h1 class="text-3xl font-bold mb-8 text-center text-gradient glitch-text">MAC Address Vendor Lookup</h1>
<div class="mt-8 p-6 glass-card rounded-xl">
<div class="flex flex-col sm:flex-row gap-3 mb-6">
<input type="text" id="mac-input" placeholder="Enter MAC address (e.g., 00:1A:2B:3C:4D:5E)"
class="flex-grow 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 placeholder-gray-600">
<button id="mac-lookup-button"
class="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">
Find Vendor
</button>
</div>
<div id="mac-lookup-error"
class="text-red-400 mb-4 hidden p-3 bg-red-900/20 border border-red-500/30 rounded"></div>
<div id="mac-lookup-results-section" class="hidden mt-6 border-t border-gray-700/50 pt-6 fade-in">
<h3 class="text-lg font-semibold text-purple-300 mb-4 flex items-center justify-center gap-2">
<div class="w-1.5 h-6 bg-purple-500 rounded-full"></div>
Vendor for: <span id="mac-lookup-query" class="font-mono text-purple-400 ml-1"></span>
</h3>
<div id="mac-lookup-loader" class="loader hidden mb-4 mx-auto"></div>
<pre id="mac-lookup-output" class="result-pre custom-scrollbar"></pre>
</div>
</div>
<div id="global-error"
class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg">
</div>
<footer class="mt-12 pt-6 border-t border-gray-700/30 text-center text-xs text-gray-500">
<p>&copy; 2025 <a href="https://mrunk.de"
class="text-purple-400 hover:text-purple-300 transition-colors">MrUnknownDE</a></p>
<p class="mt-1">Version: <span id="commit-sha" class="font-mono text-gray-400">loading...</span></p>
</footer>
</div>
<script src="mac-lookup.js"></script>
</body>
</html>
+82
View File
@@ -0,0 +1,82 @@
// frontend/app/mac-lookup.js
document.addEventListener('DOMContentLoaded', () => {
const macInput = document.getElementById('mac-input');
const macLookupButton = document.getElementById('mac-lookup-button');
const macLookupErrorEl = document.getElementById('mac-lookup-error');
const macLookupResultsSection = document.getElementById('mac-lookup-results-section');
const macLookupQueryEl = document.getElementById('mac-lookup-query');
const macLookupLoader = document.getElementById('mac-lookup-loader');
const macLookupOutputEl = document.getElementById('mac-lookup-output');
const commitShaEl = document.getElementById('commit-sha');
const globalErrorEl = document.getElementById('global-error');
const API_BASE_URL = '/api';
function showGlobalError(message) {
if (!globalErrorEl) return;
globalErrorEl.textContent = `Error: ${message}`;
globalErrorEl.classList.remove('hidden');
}
async function fetchVersionInfo() {
if (!commitShaEl) return;
try {
const response = await fetch(`${API_BASE_URL}/version`);
if (!response.ok) throw new Error(`Network response: ${response.statusText}`);
const data = await response.json();
commitShaEl.textContent = data.commitSha || 'unknown';
} catch (error) {
console.error('Failed to fetch version info:', error);
commitShaEl.textContent = 'error';
}
}
function displayMacResult(data, outputEl) {
outputEl.textContent = data.vendor || 'No vendor found.';
}
async function handleMacLookup() {
const mac = macInput.value.trim();
if (!mac) {
macLookupErrorEl.textContent = 'Please enter a MAC address.';
macLookupErrorEl.classList.remove('hidden');
return;
}
// Reset animation
macLookupResultsSection.classList.remove('fade-in');
void macLookupResultsSection.offsetWidth; // Trigger reflow
macLookupResultsSection.classList.add('fade-in');
macLookupResultsSection.classList.remove('hidden');
macLookupLoader.classList.remove('hidden');
macLookupErrorEl.classList.add('hidden');
macLookupOutputEl.textContent = '';
macLookupQueryEl.textContent = mac;
try {
const response = await fetch(`${API_BASE_URL}/mac-lookup?mac=${encodeURIComponent(mac)}`);
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || `Request failed with status ${response.status}`);
}
displayMacResult(data, macLookupOutputEl);
} catch (error) {
console.error('Failed to fetch MAC vendor:', error);
macLookupErrorEl.textContent = `Error: ${error.message}`;
macLookupErrorEl.classList.remove('hidden');
macLookupOutputEl.textContent = '';
} finally {
macLookupLoader.classList.add('hidden');
}
}
fetchVersionInfo();
macLookupButton.addEventListener('click', handleMacLookup);
macInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') handleMacLookup();
});
});
+906
View File
@@ -0,0 +1,906 @@
// script.js - Hauptlogik für index.html (IP Info, IP Lookup, Traceroute)
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Elements (User IP Info) ---
const ipAddressLinkEl = document.getElementById('ip-address-link'); // Geändert von ip-address
const ipAddressSpanEl = document.getElementById('ip-address'); // Das Span *innerhalb* des Links
const countryEl = document.getElementById('country');
const regionEl = document.getElementById('region');
const cityEl = document.getElementById('city');
const postalEl = document.getElementById('postal');
const coordsEl = document.getElementById('coords');
const timezoneEl = document.getElementById('timezone');
const asnNumberEl = document.getElementById('asn-number');
const asnOrgEl = document.getElementById('asn-org');
const rdnsListEl = document.getElementById('rdns-list');
const mapContainer = document.getElementById('map-container');
const mapEl = document.getElementById('map');
const mapMessageEl = document.getElementById('map-message');
const globalErrorEl = document.getElementById('global-error');
const ipLoader = document.getElementById('ip-loader');
const geoLoader = document.getElementById('geo-loader');
const asnLoader = document.getElementById('asn-loader');
const rdnsLoader = document.getElementById('rdns-loader');
const mapLoader = document.getElementById('map-loader');
const geoErrorEl = document.getElementById('geo-error');
const asnErrorEl = document.getElementById('asn-error');
const rdnsErrorEl = document.getElementById('rdns-error');
const geoInfo = document.getElementById('geo-info');
const asnInfo = document.getElementById('asn-info');
const rdnsInfo = document.getElementById('rdns-info');
// --- DOM Elements (Lookup) ---
const lookupIpInput = document.getElementById('lookup-ip-input');
const lookupButton = document.getElementById('lookup-button');
const lookupErrorEl = document.getElementById('lookup-error');
const lookupStatusEl = document.getElementById('lookup-status'); // Optional: für Statusmeldungen wie "Resolving..."
const lookupResultsSection = document.getElementById('lookup-results-section');
const lookupIpAddressEl = document.getElementById('lookup-ip-address');
const lookupResultLoader = document.getElementById('lookup-result-loader');
const lookupCountryEl = document.getElementById('lookup-country');
const lookupRegionEl = document.getElementById('lookup-region');
const lookupCityEl = document.getElementById('lookup-city');
const lookupPostalEl = document.getElementById('lookup-postal');
const lookupCoordsEl = document.getElementById('lookup-coords');
const lookupTimezoneEl = document.getElementById('lookup-timezone');
const lookupGeoErrorEl = document.getElementById('lookup-geo-error');
const lookupAsnNumberEl = document.getElementById('lookup-asn-number');
const lookupAsnOrgEl = document.getElementById('lookup-asn-org');
const lookupAsnErrorEl = document.getElementById('lookup-asn-error');
const lookupRdnsListEl = document.getElementById('lookup-rdns-list');
const lookupRdnsErrorEl = document.getElementById('lookup-rdns-error');
const lookupMapContainer = document.getElementById('lookup-map-container');
const lookupMapEl = document.getElementById('lookup-map');
const lookupMapLoader = document.getElementById('lookup-map-loader');
const lookupMapMessageEl = document.getElementById('lookup-map-message');
const lookupPingButton = document.getElementById('lookup-ping-button');
const lookupTraceButton = document.getElementById('lookup-trace-button');
const lookupPingResultsEl = document.getElementById('lookup-ping-results');
const lookupPingLoader = document.getElementById('lookup-ping-loader');
const lookupPingOutputEl = document.getElementById('lookup-ping-output');
const lookupPingErrorEl = document.getElementById('lookup-ping-error');
// --- DOM Elements (Traceroute) ---
const tracerouteSection = document.getElementById('traceroute-section');
const tracerouteOutputEl = document.querySelector('#traceroute-output pre');
const tracerouteLoader = document.getElementById('traceroute-loader');
const tracerouteMessage = document.getElementById('traceroute-message');
// --- DOM Elements (Port Scan) ---
const portScanSection = document.getElementById('port-scan-section');
const portScanOutputEl = document.getElementById('port-scan-output');
const portScanLoader = document.getElementById('port-scan-loader');
const portScanMessage = document.getElementById('port-scan-message');
const lookupScanButton = document.getElementById('lookup-scan-button');
// --- DOM Elements (Footer) ---
const commitShaEl = document.getElementById('commit-sha');
// --- Configuration ---
const API_BASE_URL = '/api'; // Anpassen, falls nötig
// --- State ---
let map = null; // Leaflet map instance for user's IP
let lookupMap = null; // Leaflet map instance for lookup results
let currentIp = null; // Store the user's fetched IP
let currentLookupIp = null; // Store the last successfully looked-up IP
let eventSource = null; // Store the EventSource instance for traceroute
let portScanEventSource = null; // Store the EventSource for port scan
// --- Helper Functions ---
/** Zeigt globale Fehler an */
function showGlobalError(message) {
if (!globalErrorEl) return;
globalErrorEl.textContent = `Error: ${message}`;
globalErrorEl.classList.remove('hidden');
}
/** Versteckt globale Fehler */
function hideGlobalError() {
if (!globalErrorEl) return;
globalErrorEl.classList.add('hidden');
}
/**
* Prüft, ob der String eine gültige IPv4 oder IPv6 Adresse ist.
* @param {string} input - Der zu prüfende String.
* @returns {boolean} True, wenn es eine gültige IP ist, sonst false.
*/
function isValidIpAddress(input) {
if (!input || typeof input !== 'string') return false;
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// Sehr einfache IPv6 Regex (erkennt gültige Zeichen, aber nicht alle komplexen Formate perfekt)
const ipv6Regex = /^[a-fA-F0-9:]+$/;
// Komplexere IPv6 Regex (versucht mehr Fälle abzudecken, aber immer noch nicht perfekt)
const complexIpv6Regex = /^(?:(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,7}:|(?:[a-fA-F0-9]{1,4}:){1,6}:[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,5}(?::[a-fA-F0-9]{1,4}){1,2}|(?:[a-fA-F0-9]{1,4}:){1,4}(?::[a-fA-F0-9]{1,4}){1,3}|(?:[a-fA-F0-9]{1,4}:){1,3}(?::[a-fA-F0-9]{1,4}){1,4}|(?:[a-fA-F0-9]{1,4}:){1,2}(?::[a-fA-F0-9]{1,4}){1,5}|[a-fA-F0-9]{1,4}:(?:(?::[a-fA-F0-9]{1,4}){1,6})|:(?:(?::[a-fA-F0-9]{1,4}){1,7}|:)|fe80:(?::[a-fA-F0-9]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[a-fA-F0-9]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
return ipv4Regex.test(input) || complexIpv6Regex.test(input);
}
/**
* Aktualisiert ein Info-Feld und versteckt optional einen Loader.
* @param {HTMLElement} valueElement - Das Element, das den Wert anzeigt.
* @param {any} value - Der anzuzeigende Wert oder ein Fehlerobjekt {error: string}.
* @param {HTMLElement} [loaderElement] - Das zu versteckende Loader-Element.
* @param {HTMLElement} [errorElement] - Das Element zur Anzeige von Fehlern für dieses Feld.
* @param {string} [defaultValue='-'] - Standardwert bei fehlenden Daten.
*/
function updateField(valueElement, value, loaderElement = null, errorElement = null, defaultValue = '-') {
if (loaderElement) loaderElement.classList.add('hidden');
if (errorElement) errorElement.textContent = ''; // Clear previous error
// Zeige das Elternelement des valueElements, falls es vorher versteckt war (für initiale Ladeanzeige)
const dataContainer = valueElement?.closest('div:not(.loader)'); // Find closest parent div that isn't a loader
if (dataContainer?.classList.contains('hidden')) {
dataContainer.classList.remove('hidden');
}
if (value && typeof value === 'object' && value.error) {
if (valueElement) valueElement.textContent = defaultValue;
if (errorElement) errorElement.textContent = value.error;
else console.warn(`Error in field ${valueElement?.id}: ${value.error}`);
} else if (value !== null && value !== undefined && value !== '') {
if (valueElement) valueElement.textContent = value;
} else {
if (valueElement) valueElement.textContent = defaultValue;
}
}
/**
* Aktualisiert die rDNS Liste generisch.
* @param {HTMLElement} listElement - Das UL Element.
* @param {Array|object} rdnsData - Die rDNS Daten oder ein Fehlerobjekt.
* @param {HTMLElement} [loaderElement] - Das zu versteckende Loader-Element.
* @param {HTMLElement} [errorElement] - Das Element zur Anzeige von Fehlern.
*/
function updateRdns(listElement, rdnsData, loaderElement = null, errorElement = null) {
if (loaderElement) loaderElement.classList.add('hidden');
if (listElement) listElement.innerHTML = ''; // Clear previous entries
if (errorElement) errorElement.textContent = '';
// Zeige das Elternelement des listElements, falls es vorher versteckt war
const dataContainer = listElement?.closest('div:not(.loader)');
if (dataContainer?.classList.contains('hidden')) {
dataContainer.classList.remove('hidden');
}
if (rdnsData && Array.isArray(rdnsData)) {
if (rdnsData.length > 0) {
rdnsData.forEach(hostname => {
const li = document.createElement('li');
li.textContent = hostname;
if (listElement) listElement.appendChild(li);
});
} else {
if (listElement) listElement.innerHTML = '<li>No rDNS records found.</li>'; // Klarere Meldung
}
} else if (rdnsData && rdnsData.error) {
if (listElement) listElement.innerHTML = '<li>-</li>';
if (errorElement) errorElement.textContent = rdnsData.error;
} else {
if (listElement) listElement.innerHTML = '<li>-</li>';
}
}
/**
* Initialisiert oder aktualisiert eine Leaflet-Karte.
* @param {string} mapId - Die ID des Map-Containers ('map' oder 'lookup-map').
* @param {number|null} lat - Breitengrad.
* @param {number|null} lon - Längengrad.
* @param {HTMLElement} mapElement - Das Karten-Div.
* @param {HTMLElement} loaderElement - Das Loader-Element für die Karte.
* @param {HTMLElement} messageElement - Das Nachrichten-Element für die Karte.
* @returns {L.Map | null} Die Karteninstanz oder null bei Fehler.
*/
function initOrUpdateMap(mapId, lat, lon, mapElement, loaderElement, messageElement) {
if (!mapElement || !loaderElement || !messageElement) return null; // Exit if elements are missing
loaderElement.classList.add('hidden'); // Hide loader first
// Use a unique variable name for the map instance based on mapId
let mapInstance = window[mapId + '_instance'];
if (lat != null && lon != null) { // Check for non-null coordinates
mapElement.classList.remove('hidden');
messageElement.classList.add('hidden');
if (mapInstance) {
mapInstance.setView([lat, lon], 13);
mapInstance.eachLayer((layer) => {
if (layer instanceof L.Marker) mapInstance.removeLayer(layer);
});
L.marker([lat, lon]).addTo(mapInstance).bindPopup(`Approximate Location`).openPopup();
} else {
try {
mapInstance = L.map(mapId).setView([lat, lon], 13);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
subdomains: 'abcd',
maxZoom: 19
}).addTo(mapInstance);
L.marker([lat, lon]).addTo(mapInstance).bindPopup(`Approximate Location`).openPopup();
window[mapId + '_instance'] = mapInstance; // Store instance
} catch (e) {
console.error(`Leaflet map initialization failed for ${mapId}:`, e);
mapElement.classList.add('hidden');
messageElement.classList.remove('hidden');
messageElement.textContent = 'Error initializing map.';
return null;
}
}
// Invalidate size after showing/updating to prevent grey tiles
setTimeout(() => {
if (window[mapId + '_instance']) { // Check if map still exists
window[mapId + '_instance'].invalidateSize();
}
}, 100);
return mapInstance;
} else {
mapElement.classList.add('hidden');
messageElement.classList.remove('hidden');
messageElement.textContent = 'Map could not be loaded (missing or invalid coordinates).';
// If map existed, remove it to clean up resources
if (mapInstance) {
mapInstance.remove();
window[mapId + '_instance'] = null;
}
return null;
}
}
/** Ruft die IP-Informationen für die eigene IP ab */
async function fetchIpInfo() {
hideGlobalError();
[ipLoader, geoLoader, asnLoader, rdnsLoader, mapLoader].forEach(l => l?.classList.remove('hidden'));
// Hide data elements initially (containers are hidden by default in HTML)
if (ipAddressLinkEl) ipAddressLinkEl.classList.add('hidden'); // Hide link initially
if (mapEl) mapEl.classList.add('hidden');
// Ensure map message is hidden initially
if (mapMessageEl) mapMessageEl.classList.add('hidden');
try {
const response = await fetch(`${API_BASE_URL}/ipinfo`);
if (!response.ok) throw new Error(`Network response: ${response.statusText} (${response.status})`);
const data = await response.json();
console.log('Received User IP Info:', data);
currentIp = data.ip;
// Update the span inside the link
updateField(ipAddressSpanEl, data.ip, ipLoader);
if (ipAddressLinkEl) {
ipAddressLinkEl.classList.remove('hidden'); // Show link element
if (data.ip) {
// Remove old listener if it exists (safety)
ipAddressLinkEl.removeEventListener('click', handleIpClick);
// Add new listener
ipAddressLinkEl.addEventListener('click', handleIpClick);
}
}
updateField(countryEl, data.geo?.countryName ? `${data.geo.countryName} (${data.geo.country})` : null, null, geoErrorEl);
updateField(regionEl, data.geo?.region);
updateField(cityEl, data.geo?.city);
updateField(postalEl, data.geo?.postalCode);
updateField(coordsEl, data.geo?.latitude ? `${data.geo.latitude}, ${data.geo.longitude}` : null);
updateField(timezoneEl, data.geo?.timezone, geoLoader); // Hide loader on last geo field
// ASN — render as clickable link if has a number (not an error object)
const asnNum = (data.asn && !data.asn.error) ? data.asn.number : null;
if (asnNum && asnNumberEl) {
// Reveal the hidden data container manually (updateField won't run the link path via error branch)
const asnContainer = asnNumberEl.closest('div:not(.loader)');
if (asnContainer) asnContainer.classList.remove('hidden');
asnNumberEl.innerHTML =
`<a href="/asn?asn=${asnNum}" class="hover:text-purple-200 underline decoration-dotted transition-colors" title="Open ASN Lookup">AS${asnNum}</a>`;
} else {
updateField(asnNumberEl, null, null, asnErrorEl, data.asn?.error || '-');
}
updateField(asnOrgEl, data.asn?.organization, asnLoader);
updateRdns(rdnsListEl, data.rdns, rdnsLoader, rdnsErrorEl);
map = initOrUpdateMap('map', data.geo?.latitude, data.geo?.longitude, mapEl, mapLoader, mapMessageEl);
} catch (error) {
console.error('Failed to fetch user IP info:', error);
showGlobalError(`Could not load initial IP information. ${error.message}`);
[ipLoader, geoLoader, asnLoader, rdnsLoader, mapLoader].forEach(l => l?.classList.add('hidden'));
// Ensure data containers are visible to show potential errors inside them
[geoInfo, asnInfo, rdnsInfo].forEach(container => {
const dataDiv = container?.querySelector('div:not(.loader)'); // Select the data div, not the loader
if (dataDiv) dataDiv.classList.remove('hidden');
});
if (mapMessageEl) {
mapMessageEl.textContent = 'Map could not be loaded due to an error.';
mapMessageEl.classList.remove('hidden');
}
}
}
/** Ruft die Versionsinformationen (Commit SHA) ab */
async function fetchVersionInfo() {
if (!commitShaEl) return; // Don't fetch if element doesn't exist
try {
const response = await fetch(`${API_BASE_URL}/version`);
if (!response.ok) throw new Error(`Network response: ${response.statusText} (${response.status})`);
const data = await response.json();
commitShaEl.textContent = data.commitSha || 'unknown';
} catch (error) {
console.error('Failed to fetch version info:', error);
commitShaEl.textContent = 'error';
// Optionally show global error
// showGlobalError(`Could not load version info: ${error.message}`);
}
}
// --- Lookup Functions ---
// --- URL Parameter Functions ---
/**
* Updates the URL with IP lookup parameter
* @param {string} ip - The IP address or domain to lookup
*/
function updateLookupUrlParams(ip) {
const url = new URL(window.location);
url.searchParams.set('ip', ip);
window.history.pushState({}, '', url);
}
/**
* Reads URL parameters and returns IP if present
* @returns {string|null} IP or domain from URL, or null if not present
*/
function getLookupUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('ip');
}
/** Zeigt Fehler im Lookup-Bereich an */
function showLookupError(message) {
if (!lookupErrorEl) return;
lookupErrorEl.textContent = `Error: ${message}`;
lookupErrorEl.classList.remove('hidden');
// Hide status message if error occurs
if (lookupStatusEl) lookupStatusEl.classList.add('hidden');
}
/** Versteckt Fehler im Lookup-Bereich */
function hideLookupError() {
if (!lookupErrorEl) return;
lookupErrorEl.classList.add('hidden');
}
/** Zeigt eine Statusmeldung im Lookup-Bereich an */
function showLookupStatus(message) {
if (!lookupStatusEl) return;
lookupStatusEl.textContent = message;
lookupStatusEl.classList.remove('hidden');
hideLookupError(); // Hide errors when showing status
}
/** Versteckt die Statusmeldung im Lookup-Bereich */
function hideLookupStatus() {
if (!lookupStatusEl) return;
lookupStatusEl.classList.add('hidden');
}
/** Setzt den Lookup-Ergebnisbereich zurück */
function resetLookupResults() {
if (!lookupResultsSection) return;
lookupResultsSection.classList.add('hidden');
if (lookupResultLoader) lookupResultLoader.classList.add('hidden');
if (lookupMapLoader) lookupMapLoader.classList.add('hidden');
if (lookupMapEl) lookupMapEl.classList.add('hidden');
if (lookupMapMessageEl) lookupMapMessageEl.classList.add('hidden');
if (lookupPingResultsEl) lookupPingResultsEl.classList.add('hidden'); // Hide ping results too
if (lookupPingLoader) lookupPingLoader.classList.add('hidden');
if (lookupPingOutputEl) lookupPingOutputEl.textContent = '';
if (lookupPingErrorEl) lookupPingErrorEl.textContent = '';
if (portScanSection) portScanSection.classList.add('hidden'); // Hide port scan results
if (portScanOutputEl) portScanOutputEl.innerHTML = '';
hideLookupStatus(); // Hide status on reset
const fieldsToClear = [
lookupIpAddressEl, lookupCountryEl, lookupRegionEl, lookupCityEl,
lookupPostalEl, lookupCoordsEl, lookupTimezoneEl, lookupAsnNumberEl,
lookupAsnOrgEl, lookupGeoErrorEl, lookupAsnErrorEl, lookupRdnsErrorEl
];
fieldsToClear.forEach(el => { if (el) el.textContent = ''; });
if (lookupRdnsListEl) lookupRdnsListEl.innerHTML = '<li>-</li>';
if (lookupPingButton) lookupPingButton.disabled = true;
if (lookupTraceButton) lookupTraceButton.disabled = true;
if (lookupScanButton) lookupScanButton.disabled = true;
currentLookupIp = null;
// Remove lookup map instance if it exists
if (window['lookup-map_instance']) {
window['lookup-map_instance'].remove();
window['lookup-map_instance'] = null;
}
if (portScanEventSource) {
portScanEventSource.close();
portScanEventSource = null;
}
}
/** Ruft Informationen für eine spezifische IP ab */
async function fetchLookupInfo(ipToLookup) {
resetLookupResults(); // Reset before showing new results/loaders
hideLookupError();
hideGlobalError();
if (!lookupResultsSection || !lookupResultLoader || !lookupMapLoader) return; // Exit if elements missing
lookupResultsSection.classList.remove('hidden');
lookupResultLoader.classList.remove('hidden');
lookupMapLoader.classList.remove('hidden'); // Show map loader initially
hideLookupStatus(); // Hide status like "Resolving..."
try {
const response = await fetch(`${API_BASE_URL}/lookup?targetIp=${encodeURIComponent(ipToLookup)}`); // Use targetIp parameter
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || `Network response: ${response.statusText} (${response.status})`);
}
console.log('Received Lookup Info for', ipToLookup, ':', data);
currentLookupIp = data.ip; // Store the IP that was actually looked up
updateField(lookupIpAddressEl, data.ip); // Display the looked-up IP
updateField(lookupCountryEl, data.geo?.countryName ? `${data.geo.countryName} (${data.geo.country})` : null, null, lookupGeoErrorEl);
updateField(lookupRegionEl, data.geo?.region);
updateField(lookupCityEl, data.geo?.city);
updateField(lookupPostalEl, data.geo?.postalCode);
updateField(lookupCoordsEl, data.geo?.latitude ? `${data.geo.latitude}, ${data.geo.longitude}` : null);
updateField(lookupTimezoneEl, data.geo?.timezone);
// ASN — render as clickable link if available
if (data.asn?.number && lookupAsnNumberEl) {
lookupAsnNumberEl.innerHTML =
`<a href="/asn?asn=${data.asn.number}" class="text-purple-400 hover:text-purple-300 underline decoration-dotted transition-colors font-mono" title="Open ASN Lookup">AS${data.asn.number}</a>`;
} else {
updateField(lookupAsnNumberEl, data.asn?.number, null, lookupAsnErrorEl);
}
updateField(lookupAsnOrgEl, data.asn?.organization);
updateRdns(lookupRdnsListEl, data.rdns, null, lookupRdnsErrorEl);
lookupMap = initOrUpdateMap('lookup-map', data.geo?.latitude, data.geo?.longitude, lookupMapEl, lookupMapLoader, lookupMapMessageEl);
if (lookupPingButton) lookupPingButton.disabled = false;
if (lookupTraceButton) lookupTraceButton.disabled = false;
if (lookupScanButton) lookupScanButton.disabled = false;
} catch (error) {
console.error('Failed to fetch lookup info for', ipToLookup, ':', error);
showLookupError(`Lookup failed: ${error.message}`);
if (lookupMapMessageEl) {
lookupMapMessageEl.textContent = 'Map could not be loaded due to an error.';
lookupMapMessageEl.classList.remove('hidden');
}
if (lookupMapEl) lookupMapEl.classList.add('hidden');
if (lookupMapLoader) lookupMapLoader.classList.add('hidden'); // Hide loader on error
resetLookupResults(); // Hide the section again on error
} finally {
if (lookupResultLoader) lookupResultLoader.classList.add('hidden'); // Hide main loader
// Map loader is handled by initOrUpdateMap
}
}
/**
* Löst einen Domainnamen zu einer IP-Adresse auf (bevorzugt IPv4).
* @param {string} domain - Der aufzulösende Domainname.
* @returns {Promise<string|null>} Eine Promise, die mit der IP-Adresse oder null aufgelöst wird.
*/
async function resolveDomainToIp(domain) {
console.log(`Attempting to resolve domain: ${domain}`);
try {
// 1. Versuche A Record (IPv4)
let response = await fetch(`${API_BASE_URL}/dns-lookup?domain=${encodeURIComponent(domain)}&type=A`);
let data = await response.json();
if (response.ok && data.success && data.records && Array.isArray(data.records) && data.records.length > 0) {
console.log(`Resolved ${domain} to IPv4: ${data.records[0]}`);
return data.records[0]; // Nimm die erste IPv4-Adresse
}
// 2. Wenn kein A-Record, versuche AAAA Record (IPv6)
console.log(`No A record found for ${domain}, trying AAAA.`);
response = await fetch(`${API_BASE_URL}/dns-lookup?domain=${encodeURIComponent(domain)}&type=AAAA`);
data = await response.json();
if (response.ok && data.success && data.records && Array.isArray(data.records) && data.records.length > 0) {
console.log(`Resolved ${domain} to IPv6: ${data.records[0]}`);
return data.records[0]; // Nimm die erste IPv6-Adresse
}
// 3. Wenn beides fehlschlägt oder keine Records gefunden wurden
console.warn(`Could not resolve domain ${domain} to an IP address.`);
throw new Error(data.error || 'No A or AAAA records found.');
} catch (error) {
console.error('DNS resolution failed for', domain, ':', error);
throw new Error(`Could not resolve domain: ${error.message}`);
}
}
// --- Ping Function (for Lookup) ---
async function runLookupPing(ip) {
if (!ip || !lookupPingResultsEl || !lookupPingLoader || !lookupPingOutputEl || !lookupPingErrorEl) return;
lookupPingResultsEl.classList.remove('hidden');
lookupPingLoader.classList.remove('hidden');
lookupPingOutputEl.textContent = '';
lookupPingErrorEl.textContent = '';
hideLookupError(); // Hide general lookup errors
try {
const response = await fetch(`${API_BASE_URL}/ping?targetIp=${encodeURIComponent(ip)}`);
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || `Ping request failed with status ${response.status}`);
}
console.log(`Ping results for ${ip}:`, data);
// Display parsed results nicely
let outputText = `--- Ping Statistics for ${ip} ---\n`;
if (data.stats) {
outputText += `Packets: ${data.stats.packets.transmitted} transmitted, ${data.stats.packets.received} received, ${data.stats.packets.lossPercent}% loss\n`;
if (data.stats.rtt) {
outputText += `Round Trip Time (ms): min=${data.stats.rtt.min}, avg=${data.stats.rtt.avg}, max=${data.stats.rtt.max}, mdev=${data.stats.rtt.mdev}\n`;
} else if (data.stats.packets.received === 0) {
outputText += `Status: Host unreachable or request timed out.\n`;
}
} else {
outputText += `Could not parse statistics.\n`;
}
outputText += `\n--- Raw Output ---\n${data.rawOutput || 'No raw output available.'}`;
lookupPingOutputEl.textContent = outputText;
} catch (error) {
console.error(`Failed to run ping for ${ip}:`, error);
lookupPingErrorEl.textContent = `Ping Error: ${error.message}`;
} finally {
lookupPingLoader.classList.add('hidden');
}
}
// --- Traceroute Functions ---
function startTraceroute(ip) {
if (!ip) {
showGlobalError('Cannot start traceroute: IP address is missing.');
return;
}
if (!tracerouteSection || !tracerouteOutputEl || !tracerouteLoader || !tracerouteMessage) return;
if (eventSource) {
eventSource.close();
console.log('Previous EventSource closed.');
}
tracerouteSection.classList.remove('hidden');
tracerouteOutputEl.textContent = '';
tracerouteLoader.classList.remove('hidden');
tracerouteMessage.textContent = `Starting traceroute to ${ip}...`;
hideGlobalError();
hideLookupError();
const url = `${API_BASE_URL}/traceroute?targetIp=${encodeURIComponent(ip)}`;
eventSource = new EventSource(url);
eventSource.onopen = () => {
console.log('SSE connection opened for traceroute.');
tracerouteMessage.textContent = `Traceroute to ${ip} in progress...`;
};
eventSource.onerror = (event) => {
console.error('EventSource failed:', event);
let errorMsg = 'Connection error during traceroute.';
if (eventSource.readyState === EventSource.CLOSED) {
errorMsg = 'Connection closed. Server might have stopped or a network issue occurred.';
}
tracerouteMessage.textContent = errorMsg;
tracerouteLoader.classList.add('hidden');
// Don't show global error here, as it might be a normal close
eventSource.close();
};
eventSource.addEventListener('hop', (event) => {
try {
const hopData = JSON.parse(event.data);
displayTracerouteHop(hopData);
} catch (e) { displayTracerouteLine(`[Error parsing hop data: ${event.data}]`, 'error-line'); }
});
eventSource.addEventListener('info', (event) => {
try {
const infoData = JSON.parse(event.data);
displayTracerouteLine(infoData.message, 'info-line');
} catch (e) { displayTracerouteLine(`[Error parsing info data: ${event.data}]`, 'error-line'); }
});
eventSource.addEventListener('error', (event) => { // Backend error event
try {
const errorData = JSON.parse(event.data);
displayTracerouteLine(errorData.error, 'error-line');
tracerouteMessage.textContent = `Error during traceroute: ${errorData.error}`;
} catch (e) { displayTracerouteLine(`[Received unparseable error event: ${event.data}]`, 'error-line'); }
});
eventSource.addEventListener('end', (event) => {
console.log('SSE connection closed by server (end event).');
try {
const endData = JSON.parse(event.data);
const endMessage = `Traceroute finished ${endData.exitCode === 0 ? 'successfully' : `with exit code ${endData.exitCode}`}.`;
displayTracerouteLine(endMessage, 'end-line');
tracerouteMessage.textContent = endMessage;
} catch (e) { displayTracerouteLine('[Traceroute finished, error parsing end event]', 'end-line'); }
tracerouteLoader.classList.add('hidden');
eventSource.close();
});
}
function displayTracerouteLine(text, className = '') {
if (!tracerouteOutputEl) return;
const lineDiv = document.createElement('div');
if (className) lineDiv.classList.add(className);
lineDiv.classList.add('fade-in'); // Animation hinzufügen
lineDiv.textContent = text;
tracerouteOutputEl.appendChild(lineDiv);
tracerouteOutputEl.scrollTop = tracerouteOutputEl.scrollHeight;
}
function displayTracerouteHop(hopData) {
if (!tracerouteOutputEl) return;
const lineDiv = document.createElement('div');
lineDiv.classList.add('hop-line', 'fade-in'); // Animation hinzufügen
const hopNumSpan = document.createElement('span');
hopNumSpan.classList.add('hop-number');
hopNumSpan.textContent = hopData.hop || '?';
lineDiv.appendChild(hopNumSpan);
if (hopData.ip) {
const ipSpan = document.createElement('span');
ipSpan.classList.add('hop-ip');
ipSpan.textContent = hopData.ip;
lineDiv.appendChild(ipSpan);
if (hopData.hostname) {
const hostSpan = document.createElement('span');
hostSpan.classList.add('hop-hostname');
hostSpan.textContent = ` (${hopData.hostname})`;
lineDiv.appendChild(hostSpan);
}
} else if (hopData.rtt && hopData.rtt.every(r => r === '*')) {
const timeoutSpan = document.createElement('span');
timeoutSpan.classList.add('hop-timeout');
timeoutSpan.textContent = '* * *';
lineDiv.appendChild(timeoutSpan);
} else {
lineDiv.appendChild(document.createTextNode(hopData.rawLine || 'Unknown hop format'));
}
if (hopData.rtt && Array.isArray(hopData.rtt)) {
hopData.rtt.forEach(rtt => {
const rttSpan = document.createElement('span');
if (rtt === '*') {
rttSpan.classList.add('hop-timeout');
rttSpan.textContent = ' *';
} else {
rttSpan.classList.add('hop-rtt');
rttSpan.textContent = ` ${rtt} ms`;
}
lineDiv.appendChild(rttSpan);
});
}
tracerouteOutputEl.appendChild(lineDiv);
tracerouteOutputEl.scrollTop = tracerouteOutputEl.scrollHeight;
}
// --- Port Scan Functions ---
function startPortScan(ip) {
if (!ip) {
showGlobalError('Cannot start port scan: IP address is missing.');
return;
}
if (!portScanSection || !portScanOutputEl || !portScanLoader || !portScanMessage) return;
if (portScanEventSource) {
portScanEventSource.close();
}
portScanSection.classList.remove('hidden');
portScanOutputEl.innerHTML = '';
portScanLoader.classList.remove('hidden');
portScanMessage.textContent = `Starting port scan for ${ip}...`;
hideGlobalError();
hideLookupError();
const url = `${API_BASE_URL}/port-scan?targetIp=${encodeURIComponent(ip)}`;
portScanEventSource = new EventSource(url);
portScanEventSource.onopen = () => {
console.log('SSE connection opened for port scan.');
};
portScanEventSource.onerror = (event) => {
console.error('Port Scan EventSource failed:', event);
portScanMessage.textContent = 'Connection error during port scan.';
portScanLoader.classList.add('hidden');
portScanEventSource.close();
};
portScanEventSource.addEventListener('info', (event) => {
const infoData = JSON.parse(event.data);
portScanMessage.textContent = infoData.message;
});
portScanEventSource.addEventListener('port_status', (event) => {
const portData = JSON.parse(event.data);
displayPortScanResult(portData);
});
portScanEventSource.addEventListener('error', (event) => {
const errorData = JSON.parse(event.data);
displayPortScanResult({ error: errorData.error });
});
portScanEventSource.addEventListener('end', (event) => {
const endData = JSON.parse(event.data);
portScanMessage.textContent = endData.message;
portScanLoader.classList.add('hidden');
portScanEventSource.close();
});
}
function displayPortScanResult(data) {
if (!portScanOutputEl) return;
const lineDiv = document.createElement('div');
lineDiv.classList.add('mb-1', 'fade-in'); // Animation hinzufügen
let statusColor = 'text-gray-400';
let statusText = data.status.toUpperCase();
if (data.status === 'open') {
statusColor = 'text-green-400';
statusText = 'OPEN';
} else if (data.status === 'closed') {
statusColor = 'text-red-400';
statusText = 'CLOSED';
} else if (data.status === 'timeout') {
statusColor = 'text-yellow-400';
statusText = 'TIMEOUT (Filtered?)';
}
if (data.error) {
lineDiv.innerHTML = `<span class="text-red-400">Error: ${data.error}</span>`;
} else {
lineDiv.innerHTML = `Port <span class="font-bold w-12 inline-block">${data.port}</span> <span class="w-24 inline-block">(${data.service})</span>: <span class="font-bold ${statusColor}">${statusText}</span>`;
}
portScanOutputEl.appendChild(lineDiv);
portScanOutputEl.scrollTop = portScanOutputEl.scrollHeight;
}
// --- Event Handlers ---
function handleIpClick(event) {
event.preventDefault(); // Verhindert das Standardverhalten des Links (#)
if (currentIp) {
console.log(`User IP link clicked: ${currentIp}. Redirecting to WHOIS lookup...`);
// Leite zur Whois-Seite weiter und übergebe die IP als 'query'-Parameter
window.location.href = `/whois?query=${encodeURIComponent(currentIp)}`;
} else {
console.warn('Cannot redirect to WHOIS: current IP is not available.');
}
}
async function handleLookupClick() {
if (!lookupIpInput) return;
const query = lookupIpInput.value.trim();
if (!query) {
showLookupError('Please enter an IP address or domain name.');
return;
}
resetLookupResults(); // Reset results before starting
hideLookupError();
// Update URL with the query parameter
updateLookupUrlParams(query);
if (isValidIpAddress(query)) {
// Input is an IP address
console.log(`Lookup button clicked for IP: ${query}`);
fetchLookupInfo(query);
} else {
// Input is likely a domain name
console.log(`Lookup button clicked for domain: ${query}`);
showLookupStatus(`Resolving domain ${query}...`); // Show status
try {
const resolvedIp = await resolveDomainToIp(query);
if (resolvedIp) {
console.log(`Domain ${query} resolved to ${resolvedIp}. Fetching lookup info...`);
// Optional: Update input field with resolved IP? Maybe not, keep original query.
// lookupIpInput.value = resolvedIp;
fetchLookupInfo(resolvedIp); // Fetch info for the resolved IP
} else {
// Should be caught by the error in resolveDomainToIp, but as a fallback:
showLookupError(`Could not resolve domain ${query} to an IP address.`);
}
} catch (error) {
showLookupError(error.message); // Display resolution error
} finally {
hideLookupStatus(); // Hide status message regardless of outcome
}
}
}
function handleLookupPingClick() {
if (currentLookupIp) {
console.log(`Starting ping for looked-up IP: ${currentLookupIp}`);
runLookupPing(currentLookupIp); // Call the new ping function
}
}
function handleLookupTraceClick() {
if (currentLookupIp) {
console.log(`Starting traceroute for looked-up IP: ${currentLookupIp}`);
startTraceroute(currentLookupIp);
}
}
function handleLookupScanClick() {
if (currentLookupIp) {
console.log(`Starting port scan for looked-up IP: ${currentLookupIp}`);
startPortScan(currentLookupIp);
}
}
/**
* Executes IP lookup from URL parameters if they exist
*/
function executeLookupFromUrl() {
const ipParam = getLookupUrlParams();
if (ipParam && lookupIpInput) {
// Populate the input field
lookupIpInput.value = ipParam;
// Trigger the lookup
handleLookupClick();
}
}
// --- Initial Load & Event Listeners ---
fetchIpInfo(); // Lade Infos zur eigenen IP
fetchVersionInfo(); // Lade Versionsinfo für Footer
// IP Lookup Listeners (nur wenn Elemente existieren)
if (lookupButton) lookupButton.addEventListener('click', handleLookupClick);
if (lookupIpInput) lookupIpInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') handleLookupClick();
});
if (lookupPingButton) lookupPingButton.addEventListener('click', handleLookupPingClick);
if (lookupTraceButton) lookupTraceButton.addEventListener('click', handleLookupTraceClick);
if (lookupScanButton) lookupScanButton.addEventListener('click', handleLookupScanClick);
// Der Event Listener für den IP-Link wird jetzt in fetchIpInfo() hinzugefügt,
// nachdem die IP erfolgreich abgerufen wurde.
// Execute lookup from URL parameters if present
executeLookupFromUrl();
}); // End DOMContentLoaded
+352
View File
@@ -0,0 +1,352 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IP Subnetz Rechner - uTools</title> <!-- Titel angepasst -->
<!-- Tailwind CSS Play CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Eigene Styles (für Navigation etc., wie in index.html) -->
<style>
/* Einfacher Lade-Spinner (Tailwind animiert) */
.loader {
border: 4px solid rgba(168, 85, 247, 0.1);
/* Lila sehr transparent */
border-left-color: #d8b4fe;
/* Helleres Lila */
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Glassmorphism Utilities */
.glass-panel {
background: rgba(17, 24, 39, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(31, 41, 55, 0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* Navigations-Styling */
nav ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
nav a {
color: #d1d5db;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
nav a:hover {
color: #fff;
background: rgba(168, 85, 247, 0.2);
border-color: rgba(168, 85, 247, 0.4);
box-shadow: 0 0 15px rgba(168, 85, 247, 0.1);
}
nav a.active-link {
background: rgba(168, 85, 247, 0.3);
color: #fff;
border-color: #a855f7;
}
header {
background: rgba(31, 41, 55, 0.4);
backdrop-filter: blur(10px);
padding: 1.5rem;
margin-bottom: 2rem;
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
header {
flex-direction: row;
justify-content: space-between;
}
}
header h1 {
background-clip: text;
}
.text-gradient {
background: linear-gradient(to right, #c084fc, #e879f9);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hidden {
display: none !important;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
</style>
</head>
<body
class="bg-gray-950 text-gray-100 font-sans p-4 md:p-8 min-h-screen bg-[url('https://tailwindcss.com/_next/static/media/hero-dark.939eb757.png')] bg-cover bg-center bg-fixed selection:bg-purple-500 selection:text-white">
<header class="glass-panel">
<h1>uTools <span class="text-sm font-normal text-gray-400 opacity-75 tracking-wider uppercase ml-2">Network
Suite</span></h1>
<nav>
<ul>
<li><a href="/">IP Info &amp; Tools</a></li>
<li><a href="/subnet" class="active-link">Subnetz Rechner</a></li>
<li><a href="/dns">DNS Lookup</a></li>
<li><a href="/whois">WHOIS Lookup</a></li>
<li><a href="/mac">MAC Lookup</a></li>
<li><a href="/asn">ASN Lookup</a></li>
</ul>
</nav>
</header>
<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 glitch-text">IP Subnetz Rechner</h2>
<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-6">
<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
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">
</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
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">
</div>
</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
</button>
</form>
<div id="results" class="glass-card rounded-xl p-6 hidden fade-in"> <!-- Ergebnisse initial verstecken -->
<h3
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-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:
</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>
<span id="network-address" class="font-mono text-white font-semibold">-</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>
<span id="broadcast-address" class="font-mono text-purple-400 font-semibold">-</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>
<span id="subnet-mask" class="font-mono text-gray-300">-</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>
<span id="host-count" class="font-mono text-green-400 font-bold">-</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>
<span id="first-host" class="font-mono text-blue-300">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<span class="text-gray-400">Letzte Host-Adresse:</span>
<span id="last-host" class="font-mono text-blue-300">-</span>
</div>
</div>
</div>
<!-- Beispiel-Subnetze -->
<div id="examples" 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)</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 scope="col" class="px-6 py-3">Bereich</th>
<th scope="col" class="px-6 py-3">CIDR</th>
<th scope="col" class="px-6 py-3">Subnetzmaske</th>
<th scope="col" class="px-6 py-3">Beschreibung</th>
<th scope="col" class="px-6 py-3">Aktion</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">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>
</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">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>
</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">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>
</tr>
</tbody>
</table>
</div>
<p class="mt-4 text-xs text-gray-500 italic">Klicken Sie auf "Beispiel", um die Felder oben auszufüllen und
die Berechnung zu starten.</p>
</div>
<!-- Globaler Fehlerbereich -->
<div id="global-error"
class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg">
</div>
</div>
<footer class="mt-12 pt-6 border-t border-gray-700/30 text-center text-xs text-gray-500">
<p>&copy; 2025 <a href="https://mrunk.de"
class="text-purple-400 hover:text-purple-300 transition-colors">MrUnknownDE</a></p>
<p class="mt-1">Version: <span id="commit-sha" class="font-mono text-gray-400">loading...</span></p>
<!-- Footer mit Version hinzugefügt -->
</footer>
<!-- Nur das Skript für den Rechner laden -->
<script src="subnet-calculator.js"></script>
<script>
// Kleine Ergänzung, um die Beispiel-Links klickbar zu machen und Version zu laden
document.addEventListener('DOMContentLoaded', () => {
// Beispiel-Links
document.querySelectorAll('.example-link').forEach(link => {
link.addEventListener('click', (event) => {
const ip = event.target.getAttribute('data-ip');
const cidr = event.target.getAttribute('data-cidr');
document.getElementById('ip-address').value = ip;
document.getElementById('cidr').value = cidr;
// Berechnung direkt auslösen
document.getElementById('subnet-form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
window.scrollTo({ top: 0, behavior: 'smooth' }); // Nach oben scrollen
});
});
// Version laden (gemeinsame Funktion)
const commitShaEl = document.getElementById('commit-sha');
const globalErrorEl = document.getElementById('global-error');
const API_BASE_URL = '/api'; // Muss hier definiert sein, wenn nicht global
async function fetchVersionInfo() {
try {
const response = await fetch(`${API_BASE_URL}/version`);
if (!response.ok) throw new Error(`Network response: ${response.statusText} (${response.status})`);
const data = await response.json();
if (commitShaEl) {
commitShaEl.textContent = data.commitSha || 'unknown';
}
} catch (error) {
console.error('Failed to fetch version info:', error);
if (commitShaEl) commitShaEl.textContent = 'error';
if (globalErrorEl) { // Zeige Fehler global an, wenn Element existiert
globalErrorEl.textContent = `Error loading version: ${error.message}`;
globalErrorEl.classList.remove('hidden');
}
}
}
fetchVersionInfo();
});
</script>
</body>
</html>
+211
View File
@@ -0,0 +1,211 @@
// Event Listener hinzufügen, sobald das DOM geladen ist
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('subnet-form');
if (form) {
form.addEventListener('submit', handleSubnetCalculation);
} else {
console.error("Subnetz-Formular (ID: subnet-form) nicht gefunden!");
}
});
// Funktion zur Behandlung der Subnetzberechnung bei Formularübermittlung
function handleSubnetCalculation(event) {
event.preventDefault(); // Verhindert das Neuladen der Seite
clearResults(); // Ergebnisse zuerst löschen/verstecken
const ipAddressInput = document.getElementById('ip-address').value.trim();
const cidrInput = document.getElementById('cidr').value.trim();
const resultsDiv = document.getElementById('results'); // Ergebnis-Div holen
// Einfache Validierung
if (!isValidIP(ipAddressInput)) {
alert("Bitte geben Sie eine gültige IPv4-Adresse ein.");
return;
}
let cidr;
let subnetMask;
// Prüfen, ob CIDR oder Subnetzmaske eingegeben wurde
if (cidrInput.includes('.')) { // Annahme: Subnetzmaske im Format xxx.xxx.xxx.xxx
if (!isValidIP(cidrInput)) {
alert("Bitte geben Sie eine gültige Subnetzmaske ein.");
return;
}
subnetMask = cidrInput;
cidr = maskToCidr(subnetMask);
if (cidr === null) {
alert("Ungültige Subnetzmaske. Sie muss aus einer kontinuierlichen Folge von Einsen gefolgt von Nullen bestehen (z.B. 255.255.255.0, nicht 255.255.0.255).");
return;
}
} else { // Annahme: CIDR-Notation
cidr = parseInt(cidrInput, 10);
if (isNaN(cidr) || cidr < 0 || cidr > 32) {
alert("Bitte geben Sie einen gültigen CIDR-Wert (0-32) ein.");
return;
}
subnetMask = cidrToMask(cidr);
if (subnetMask === null) {
alert("Interner Fehler bei der Umwandlung von CIDR zu Maske.");
return;
}
}
// Berechnung durchführen und Ergebnisse anzeigen
try {
const results = calculateSubnet(ipAddressInput, cidr);
displayResults(results, subnetMask);
if (resultsDiv) {
resultsDiv.classList.remove('hidden'); // Ergebnisbereich sichtbar machen
} else {
console.error("Ergebnis-Div (ID: results) nicht gefunden!");
}
} catch (error) {
console.error("Fehler bei der Subnetzberechnung:", error);
alert("Fehler bei der Berechnung: " + error.message);
clearResults();
}
}
// --- Validierungs- und Hilfsfunktionen ---
function isValidIP(ip) {
const ipPattern = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
return ipPattern.test(ip);
}
function ipToBinary(ip) {
return ip.split('.').map(octet => parseInt(octet, 10).toString(2).padStart(8, '0')).join('');
}
function binaryToIp(binary) {
if (binary.length !== 32) return null;
const octets = [];
for (let i = 0; i < 32; i += 8) {
octets.push(parseInt(binary.substring(i, i + 8), 2));
}
return octets.join('.');
}
function cidrToMask(cidr) {
if (cidr < 0 || cidr > 32) return null;
const maskBinary = '1'.repeat(cidr) + '0'.repeat(32 - cidr);
return binaryToIp(maskBinary);
}
function maskToCidr(mask) {
if (!isValidIP(mask)) return null;
const binaryMask = ipToBinary(mask);
let encounteredZero = false;
for (let i = 0; i < 32; i++) {
if (binaryMask[i] === '1') {
if (encounteredZero) return null;
} else {
encounteredZero = true;
}
}
let cidr = 0;
for(let i = 0; i < 32; i++) {
if (binaryMask[i] === '1') {
cidr++;
} else {
break;
}
}
return cidr;
}
// --- Berechnungsfunktion ---
function calculateSubnet(ip, cidr) {
const ipBinary = ipToBinary(ip);
const maskBinary = '1'.repeat(cidr) + '0'.repeat(32 - cidr);
// Netzwerkadresse berechnen (Bitweises UND von IP und Maske)
let networkBinary = '';
for (let i = 0; i < 32; i++) {
networkBinary += (parseInt(ipBinary[i], 10) & parseInt(maskBinary[i], 10)).toString();
}
const networkAddress = binaryToIp(networkBinary);
const networkNum = parseInt(networkBinary, 2); // Netzwerkadresse als Zahl
// Broadcast-Adresse berechnen (Netzwerk-Teil + Host-Teil mit Einsen) - Korrigierte Methode
const hostBitsCount = 32 - cidr;
let broadcastBinary = networkBinary.substring(0, cidr) + '1'.repeat(hostBitsCount);
// Sicherstellen, dass die Länge 32 Bit beträgt (sollte sie aber ohnehin)
broadcastBinary = broadcastBinary.padEnd(32, '1'); // Auffüllen mit 1, falls Länge < 32 (unwahrscheinlich)
const broadcastAddress = binaryToIp(broadcastBinary);
// broadcastNum wird für die letzte Host-Adresse benötigt
const broadcastNum = parseInt(broadcastBinary, 2);
// Anzahl der Hosts
const hostBits = 32 - cidr; // hostBitsCount umbenannt für Konsistenz
let hostCount = 0;
if (hostBits >= 2) { // Mindestens /30 für 2 Hosts (-2)
hostCount = Math.pow(2, hostBits) - 2;
} else if (hostBits === 1) { // /31 hat 2 Adressen, beide nutzbar (RFC 3021)
hostCount = 2;
} else { // /32 hat nur 1 Adresse
hostCount = 1;
}
// Erste Host-Adresse
let firstHost = '-';
if (hostBits >= 2) { // /30 oder größer: Netzwerkadresse + 1
// Sicherstellen, dass die Addition korrekt behandelt wird (als Zahl)
const firstHostNum = networkNum + 1;
const firstHostBinary = firstHostNum.toString(2).padStart(32, '0');
firstHost = binaryToIp(firstHostBinary);
} else if (cidr === 31) { // /31: Die erste Adresse des /31
firstHost = networkAddress;
} else { // /32: Nur die eine Adresse
firstHost = networkAddress;
}
// Letzte Host-Adresse
let lastHost = '-';
if (hostBits >= 2) { // /30 oder größer: Broadcast-Adresse - 1
// Sicherstellen, dass die Subtraktion korrekt behandelt wird (als Zahl)
const lastHostNum = broadcastNum - 1;
const lastHostBinary = lastHostNum.toString(2).padStart(32, '0');
lastHost = binaryToIp(lastHostBinary);
} else if (cidr === 31) { // /31: Die zweite Adresse des /31
lastHost = broadcastAddress;
} else { // /32: Nur die eine Adresse
lastHost = networkAddress;
}
return {
networkAddress,
broadcastAddress,
hostCount,
firstHost,
lastHost
};
}
// --- Anzeige-Funktionen ---
function displayResults(results, subnetMask) {
document.getElementById('network-address').textContent = results.networkAddress;
document.getElementById('broadcast-address').textContent = results.broadcastAddress;
document.getElementById('host-count').textContent = results.hostCount >= 0 ? results.hostCount.toLocaleString() : '-';
document.getElementById('first-host').textContent = results.firstHost;
document.getElementById('last-host').textContent = results.lastHost;
document.getElementById('subnet-mask').textContent = subnetMask;
}
function clearResults() {
document.getElementById('network-address').textContent = '-';
document.getElementById('broadcast-address').textContent = '-';
document.getElementById('host-count').textContent = '-';
document.getElementById('first-host').textContent = '-';
document.getElementById('last-host').textContent = '-';
document.getElementById('subnet-mask').textContent = '-';
const resultsDiv = document.getElementById('results');
if (resultsDiv && !resultsDiv.classList.contains('hidden')) {
resultsDiv.classList.add('hidden');
}
}
+239
View File
@@ -0,0 +1,239 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WHOIS Lookup - uTools</title>
<!-- Tailwind CSS Play CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Eigene Styles -->
<style>
/* Einfacher Lade-Spinner (Tailwind animiert) */
.loader {
border: 4px solid rgba(168, 85, 247, 0.1);
/* Lila sehr transparent */
border-left-color: #d8b4fe;
/* Helleres Lila */
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Glassmorphism Utilities */
.glass-panel {
background: rgba(17, 24, 39, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(31, 41, 55, 0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* Ergebnis-Pre-Formatierung */
.result-pre {
white-space: pre-wrap;
word-break: break-all;
font-family: 'Courier New', Courier, monospace;
background-color: rgba(0, 0, 0, 0.3);
color: #e5e7eb;
padding: 1rem;
border-radius: 0.375rem;
max-height: 600px;
overflow-y: auto;
font-size: 0.875rem;
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.3);
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* Navigations-Styling */
nav ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
nav a {
color: #d1d5db;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
nav a:hover {
color: #fff;
background: rgba(168, 85, 247, 0.2);
border-color: rgba(168, 85, 247, 0.4);
box-shadow: 0 0 15px rgba(168, 85, 247, 0.1);
}
nav a.active-link {
background: rgba(168, 85, 247, 0.3);
color: #fff;
border-color: #a855f7;
}
header {
background: rgba(31, 41, 55, 0.4);
backdrop-filter: blur(10px);
padding: 1.5rem;
margin-bottom: 2rem;
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
header {
flex-direction: row;
justify-content: space-between;
}
}
header h1 {
background-clip: text;
}
.text-gradient {
background: linear-gradient(to right, #c084fc, #e879f9);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hidden {
display: none !important;
}
</style>
</head>
<body
class="bg-gray-950 text-gray-100 font-sans p-4 md:p-8 min-h-screen bg-[url('https://tailwindcss.com/_next/static/media/hero-dark.939eb757.png')] bg-cover bg-center bg-fixed selection:bg-purple-500 selection:text-white">
<header class="glass-panel">
<h1>uTools <span class="text-sm font-normal text-gray-400 opacity-75 tracking-wider uppercase ml-2">Network
Suite</span></h1>
<nav>
<ul>
<li><a href="/">IP Info &amp; Tools</a></li>
<li><a href="/subnet">Subnetz Rechner</a></li>
<li><a href="/dns">DNS Lookup</a></li>
<li><a href="/whois" class="active-link">WHOIS Lookup</a></li>
<li><a href="/mac">MAC Lookup</a></li>
<li><a href="/asn">ASN Lookup</a></li>
</ul>
</nav>
</header>
<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">
<h1 class="text-3xl font-bold mb-8 text-center text-gradient glitch-text">WHOIS Lookup</h1>
<!-- Bereich für WHOIS Lookup -->
<div class="mt-8 p-6 glass-card rounded-xl">
<div class="flex flex-col sm:flex-row gap-3 mb-6">
<input type="text" id="whois-query-input" placeholder="Enter domain or IP (e.g., google.com or 8.8.8.8)"
class="flex-grow 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 placeholder-gray-600">
<button id="whois-lookup-button"
class="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">
Lookup WHOIS
</button>
</div>
<div id="whois-lookup-error"
class="text-red-400 mb-4 hidden p-3 bg-red-900/20 border border-red-500/30 rounded"></div>
<div id="whois-lookup-results-section" class="hidden mt-6 border-t border-gray-700/50 pt-6 fade-in">
<h3 class="text-lg font-semibold text-purple-300 mb-4 flex items-center gap-2">
<div class="w-1.5 h-6 bg-purple-500 rounded-full"></div>
WHOIS Results for: <span id="whois-lookup-query" class="font-mono text-purple-400 ml-1"></span>
</h3>
<div id="whois-lookup-loader" class="loader hidden mb-4"></div>
<pre id="whois-lookup-output" class="result-pre custom-scrollbar"></pre> <!-- Ergebnisbereich -->
</div>
</div>
<!-- Globaler Fehlerbereich -->
<div id="global-error"
class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg">
</div>
<!-- Footer für Version -->
<footer class="mt-12 pt-6 border-t border-gray-700/30 text-center text-xs text-gray-500">
<p>&copy; 2025 <a href="https://mrunk.de"
class="text-purple-400 hover:text-purple-300 transition-colors">MrUnknownDE</a></p>
<p class="mt-1">Version: <span id="commit-sha" class="font-mono text-gray-400">loading...</span></p>
</footer>
</div>
<!-- Eigene JS-Logik für diese Seite -->
<script src="whois-lookup.js"></script>
</body>
</html>
+151
View File
@@ -0,0 +1,151 @@
// frontend/whois-lookup.js
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Elements (WHOIS Lookup) ---
const whoisQueryInput = document.getElementById('whois-query-input');
const whoisLookupButton = document.getElementById('whois-lookup-button');
const whoisLookupErrorEl = document.getElementById('whois-lookup-error');
const whoisLookupResultsSection = document.getElementById('whois-lookup-results-section');
const whoisLookupQueryEl = document.getElementById('whois-lookup-query');
const whoisLookupLoader = document.getElementById('whois-lookup-loader');
const whoisLookupOutputEl = document.getElementById('whois-lookup-output');
// --- DOM Elements (Common) ---
const globalErrorEl = document.getElementById('global-error');
const commitShaEl = document.getElementById('commit-sha');
// --- Configuration ---
const API_BASE_URL = '/api'; // Anpassen, falls nötig
// --- Helper Functions ---
/** Zeigt globale Fehler an */
function showGlobalError(message) {
if (!globalErrorEl) return;
globalErrorEl.textContent = `Error: ${message}`;
globalErrorEl.classList.remove('hidden');
}
/** Versteckt globale Fehler */
function hideGlobalError() {
if (!globalErrorEl) return;
globalErrorEl.classList.add('hidden');
}
/**
* Generische Funktion zum Abrufen und Anzeigen von Lookup-Ergebnissen.
* @param {string} endpoint - Der API-Endpunkt (z.B. '/whois-lookup').
* @param {object} params - Query-Parameter als Objekt (z.B. { query: '...' }).
* @param {HTMLElement} resultsSection - Der Container für die Ergebnisse.
* @param {HTMLElement} loaderElement - Das Loader-Element.
* @param {HTMLElement} errorElement - Das Fehleranzeige-Element für diesen Lookup.
* @param {HTMLElement} queryElement - Das Element zur Anzeige der Suchanfrage.
* @param {HTMLElement} outputElement - Das Element zur Anzeige der Ergebnisse (<pre> oder <p>).
* @param {function} displayFn - Funktion zur Formatierung und Anzeige der Daten im outputElement.
*/
async function fetchAndDisplay(endpoint, params, resultsSection, loaderElement, errorElement, queryElement, outputElement, displayFn) {
// Reset animation
resultsSection.classList.remove('fade-in');
void resultsSection.offsetWidth; // Trigger reflow
resultsSection.classList.add('fade-in');
resultsSection.classList.remove('hidden');
loaderElement.classList.remove('hidden');
errorElement.classList.add('hidden');
outputElement.textContent = ''; // Clear previous results
if (queryElement) queryElement.textContent = Object.values(params).join(', '); // Display query
hideGlobalError(); // Hide global errors before new request
const urlParams = new URLSearchParams(params);
const url = `${API_BASE_URL}${endpoint}?${urlParams.toString()}`;
try {
const response = await fetch(url);
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || `Request failed with status ${response.status}`);
}
console.log(`Received ${endpoint} data:`, data);
displayFn(data, outputElement); // Call the specific display function
} catch (error) {
console.error(`Failed to fetch ${endpoint}:`, error);
errorElement.textContent = `Error: ${error.message}`;
errorElement.classList.remove('hidden');
outputElement.textContent = ''; // Clear output on error
} finally {
loaderElement.classList.add('hidden');
}
}
/** Ruft die Versionsinformationen (Commit SHA) ab */
async function fetchVersionInfo() {
if (!commitShaEl) return; // Don't fetch if element doesn't exist
try {
const response = await fetch(`${API_BASE_URL}/version`);
if (!response.ok) throw new Error(`Network response: ${response.statusText} (${response.status})`);
const data = await response.json();
commitShaEl.textContent = data.commitSha || 'unknown';
} catch (error) {
console.error('Failed to fetch version info:', error);
commitShaEl.textContent = 'error';
// Optionally show global error
// showGlobalError(`Could not load version info: ${error.message}`);
}
}
// --- WHOIS Lookup Specific Functions ---
function displayWhoisResults(data, outputEl) {
// WHOIS data can be large and unstructured, display as raw text
if (typeof data.result === 'string') {
outputEl.textContent = data.result; // Display raw text
} else {
// Fallback if the result is not a string (shouldn't happen with current backend)
outputEl.textContent = JSON.stringify(data.result, null, 2);
}
}
function handleWhoisLookupClick() {
const query = whoisQueryInput.value.trim();
if (!query) {
whoisLookupErrorEl.textContent = 'Please enter a domain or IP address.';
whoisLookupErrorEl.classList.remove('hidden');
return;
}
fetchAndDisplay(
'/whois-lookup',
{ query },
whoisLookupResultsSection,
whoisLookupLoader,
whoisLookupErrorEl,
whoisLookupQueryEl,
whoisLookupOutputEl,
displayWhoisResults
);
}
/** Prüft URL-Parameter und startet ggf. den Lookup */
function checkUrlParamsAndLookup() {
const urlParams = new URLSearchParams(window.location.search);
const queryFromUrl = urlParams.get('query');
if (queryFromUrl && whoisQueryInput) {
console.log(`Found query parameter in URL: ${queryFromUrl}`);
whoisQueryInput.value = queryFromUrl; // Set input field value
handleWhoisLookupClick(); // Trigger the lookup
}
}
// --- Initial Load & Event Listeners ---
fetchVersionInfo(); // Lade Versionsinfo für Footer
whoisLookupButton.addEventListener('click', handleWhoisLookupClick);
whoisQueryInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') handleWhoisLookupClick();
});
// Prüfe URL-Parameter nach dem Setup der Listener
checkUrlParamsAndLookup();
}); // End DOMContentLoaded
+55
View File
@@ -0,0 +1,55 @@
server {
listen 80;
server_name localhost; # Oder deine Domain
# Root-Verzeichnis für statische Dateien
root /usr/share/nginx/html;
index index.html;
# Logging (optional, aber nützlich)
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Clean URL rewrites - Map short URLs to actual HTML files
rewrite ^/dns$ /dns-lookup.html last;
rewrite ^/dns-lookup$ /dns-lookup.html last;
rewrite ^/whois$ /whois-lookup.html last;
rewrite ^/whois-lookup$ /whois-lookup.html last;
rewrite ^/mac$ /mac-lookup.html last;
rewrite ^/mac-lookup$ /mac-lookup.html last;
rewrite ^/subnet$ /subnet-calculator.html last;
rewrite ^/subnet-calculator$ /subnet-calculator.html last;
rewrite ^/asn$ /asn-lookup.html last;
rewrite ^/asn-lookup$ /asn-lookup.html last;
# Statische Dateien direkt ausliefern
location / {
# First try the exact URI, then with .html, then fall back to index.html
try_files $uri $uri.html $uri/ /index.html;
}
# API-Anfragen an den Backend-Service weiterleiten
location /api/ {
# Der Name 'backend' muss dem Service-Namen in docker-compose.yml entsprechen
proxy_pass http://backend:3000; # Leitet an den Backend-Container auf Port 3000 weiter
# Wichtige Proxy-Header setzen
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Header für Server-Sent Events (Traceroute)
proxy_set_header Connection '';
proxy_http_version 1.1;
proxy_buffering off; # Wichtig für Streaming
proxy_cache off; # Wichtig für Streaming
proxy_read_timeout 300s; # Längerer Timeout für potenziell lange Traceroutes
}
}
# Upstream-Definition (optional, aber sauberer für proxy_pass)
# upstream backend_server {
# server backend:3000;
# }
# Dann in location /api/: proxy_pass http://backend_server;
-2
View File
@@ -1,2 +0,0 @@
<h1>This is a Test</h1>
<p>runing on Ubuntu 20.04</p>
-39
View File
@@ -1,39 +0,0 @@
<script async src='https://maps.googleapis.com/maps/api/js?key=AIzaSyCMPtVMDhHelORhyk2AAc9FtjgnjybvdMU&callback=initMap&v=weekly'></script>
<script type="module" src="./maps.js"></script>
<?php
$IP = $_SERVER['REMOTE_ADDR'];
$ip = htmlentities($_GET["ip"]);
$host = gethostbyaddr($ip);
$latitude = htmlentities($_POST['latitude'], ENT_QUOTES, 'UTF-8');
$longitude = htmlentities($_POST['longitude'], ENT_QUOTES, 'UTF-8');
$city = htmlentities($_POST['city'], ENT_QUOTES, 'UTF-8');
$details = json_decode(file_get_contents("http://ipinfo.io/{$IP}/json?token=391da55dff40d9"));
$location = json_decode(file_get_contents("http://ipinfo.io/{$IP}/json?token=391da55dff40d9"));
echo "<b>IP: </b>" .$details->ip;
?>
<?php
$IP = $_SERVER['REMOTE_ADDR'];
$ip = htmlentities($_GET["ip"]);
$latitude = htmlentities($_POST['latitude'], ENT_QUOTES, 'UTF-8');
$longitude = htmlentities($_POST['longitude'], ENT_QUOTES, 'UTF-8');
$city = htmlentities($_POST['city'], ENT_QUOTES, 'UTF-8');
$details = json_decode(file_get_contents("http://ipinfo.io/{$ip}/json?token=391da55dff40d9"));
$location = json_decode(file_get_contents("http://ipinfo.io/{$ip}/json?token=391da55dff40d9"));
if(isset($_GET['ip']))
{
print ('<form method="get" action="">
<h1 class="h3 mb-3 fw-normal">IP Lookup</h1>
<div class="form-floating">
<form method="get" action="">
<input type="text" class="form-control" id="floatingInput" maxlength="32" placeholder="IP" title="Enter IP Address here" />
<br/>
<button class="w-100 btn btn-lg btn-primary button" type="submit">Let`s Go</button>
</div>
<p class="mt-5 mb-3 text-muted">sysLUL.de &copy; 2022</p>
</form>');
-7
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-40
View File
@@ -1,40 +0,0 @@
html,
body {
height: 100%;
background-color: black;
}
body {
display: flex;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: auto;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
-81
View File
@@ -1,81 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Hugo 0.84.0">
<title>Jumbotron example · Bootstrap v5.0</title>
<link rel="canonical" href="https://getbootstrap.com/docs/5.0/examples/jumbotron/">
<!-- Bootstrap core CSS -->
<link href="../assets/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
</head>
<body>
<main>
<div class="container py-4">
<header class="pb-3 mb-4 border-bottom">
<a href="/" class="d-flex align-items-center text-dark text-decoration-none">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="32" class="me-2" viewBox="0 0 118 94" role="img"><title>Bootstrap</title><path fill-rule="evenodd" clip-rule="evenodd" d="M24.509 0c-6.733 0-11.715 5.893-11.492 12.284.214 6.14-.064 14.092-2.066 20.577C8.943 39.365 5.547 43.485 0 44.014v5.972c5.547.529 8.943 4.649 10.951 11.153 2.002 6.485 2.28 14.437 2.066 20.577C12.794 88.106 17.776 94 24.51 94H93.5c6.733 0 11.714-5.893 11.491-12.284-.214-6.14.064-14.092 2.066-20.577 2.009-6.504 5.396-10.624 10.943-11.153v-5.972c-5.547-.529-8.934-4.649-10.943-11.153-2.002-6.484-2.28-14.437-2.066-20.577C105.214 5.894 100.233 0 93.5 0H24.508zM80 57.863C80 66.663 73.436 72 62.543 72H44a2 2 0 01-2-2V24a2 2 0 012-2h18.437c9.083 0 15.044 4.92 15.044 12.474 0 5.302-4.01 10.049-9.119 10.88v.277C75.317 46.394 80 51.21 80 57.863zM60.521 28.34H49.948v14.934h8.905c6.884 0 10.68-2.772 10.68-7.727 0-4.643-3.264-7.207-9.012-7.207zM49.948 49.2v16.458H60.91c7.167 0 10.964-2.876 10.964-8.281 0-5.406-3.903-8.178-11.425-8.178H49.948z" fill="currentColor"></path></svg>
<span class="fs-4">Jumbotron example</span>
</a>
</header>
<div class="p-5 mb-4 bg-light rounded-3">
<div class="container-fluid py-5">
<h1 class="display-5 fw-bold">Custom jumbotron</h1>
<p class="col-md-8 fs-4">Using a series of utilities, you can create this jumbotron, just like the one in previous versions of Bootstrap. Check out the examples below for how you can remix and restyle it to your liking.</p>
<button class="btn btn-primary btn-lg" type="button">Example button</button>
</div>
</div>
<div class="row align-items-md-stretch">
<div class="col-md-6">
<div class="h-100 p-5 text-white bg-dark rounded-3">
<h2>Change the background</h2>
<p>Swap the background-color utility and add a `.text-*` color utility to mix up the jumbotron look. Then, mix and match with additional component themes and more.</p>
<button class="btn btn-outline-light" type="button">Example button</button>
</div>
</div>
<div class="col-md-6">
<div class="h-100 p-5 bg-light border rounded-3">
<h2>Add borders</h2>
<p>Or, keep it light and add a border for some added definition to the boundaries of your content. Be sure to look under the hood at the source HTML here as we've adjusted the alignment and sizing of both column's content for equal-height.</p>
<button class="btn btn-outline-secondary" type="button">Example button</button>
</div>
</div>
</div>
<footer class="pt-3 mt-4 text-muted border-top">
&copy; 2021
</footer>
</div>
</main>
</body>
</html>
-82
View File
@@ -1,82 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<title>uTraceMe</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="IP Lookup">
<meta name="keywords" content="ip lookup, what is my ip, my ip address, my ip, ip address lookup, ip geolocation, latitude longitude finder, ip lookup php script, ip2location, geolocation, ip-location, my ip lookup, ip-lookup, geoip, geo ip, ip finder, ip tools, ip tools, ip location finder, location finder, what is my ip location, ip address geolocation, ????? ????, ???? ???? ?????, ????? ip, ?????? ????">
<meta name="author" content="Johannes Krüger">
<link href="css/bootstrap.min.css" rel="stylesheet">
<link href="css/main.css" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
</head>
<body>
<main>
<div class="container py-4">
<header class="pb-3 mb-4 border-bottom">
<a href="/" class="d-flex align-items-center text-dark text-decoration-none">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="32" class="me-2" viewBox="0 0 118 94" role="img"><title>Bootstrap</title><path fill-rule="evenodd" clip-rule="evenodd" d="M24.509 0c-6.733 0-11.715 5.893-11.492 12.284.214 6.14-.064 14.092-2.066 20.577C8.943 39.365 5.547 43.485 0 44.014v5.972c5.547.529 8.943 4.649 10.951 11.153 2.002 6.485 2.28 14.437 2.066 20.577C12.794 88.106 17.776 94 24.51 94H93.5c6.733 0 11.714-5.893 11.491-12.284-.214-6.14.064-14.092 2.066-20.577 2.009-6.504 5.396-10.624 10.943-11.153v-5.972c-5.547-.529-8.934-4.649-10.943-11.153-2.002-6.484-2.28-14.437-2.066-20.577C105.214 5.894 100.233 0 93.5 0H24.508zM80 57.863C80 66.663 73.436 72 62.543 72H44a2 2 0 01-2-2V24a2 2 0 012-2h18.437c9.083 0 15.044 4.92 15.044 12.474 0 5.302-4.01 10.049-9.119 10.88v.277C75.317 46.394 80 51.21 80 57.863zM60.521 28.34H49.948v14.934h8.905c6.884 0 10.68-2.772 10.68-7.727 0-4.643-3.264-7.207-9.012-7.207zM49.948 49.2v16.458H60.91c7.167 0 10.964-2.876 10.964-8.281 0-5.406-3.903-8.178-11.425-8.178H49.948z" fill="currentColor"></path></svg>
<span class="fs-4">Jumbotron example</span>
</a>
</header>
<div class="p-5 mb-4 bg-light rounded-3">
<div class="container-fluid py-5">
<h1 class="display-5 fw-bold">Custom jumbotron</h1>
<p class="col-md-8 fs-4">Using a series of utilities, you can create this jumbotron, just like the one in previous versions of Bootstrap. Check out the examples below for how you can remix and restyle it to your liking.</p>
<button class="btn btn-primary btn-lg" type="button">Example button</button>
</div>
</div>
<div class="row align-items-md-stretch">
<div class="col-md-6">
<div class="h-100 p-5 text-white bg-dark rounded-3">
<h2>Change the background</h2>
<p>Swap the background-color utility and add a `.text-*` color utility to mix up the jumbotron look. Then, mix and match with additional component themes and more.</p>
<button class="btn btn-outline-light" type="button">Example button</button>
</div>
</div>
<div class="col-md-6">
<div class="h-100 p-5 bg-light border rounded-3">
<h2>Add borders</h2>
<p>Or, keep it light and add a border for some added definition to the boundaries of your content. Be sure to look under the hood at the source HTML here as we've adjusted the alignment and sizing of both column's content for equal-height.</p>
<button class="btn btn-outline-secondary" type="button">Example button</button>
</div>
</div>
</div>
<footer class="pt-3 mt-4 text-muted border-top">
&copy; 2021
</footer>
</div>
</main>
</body>
</html>
</body>
-17
View File
@@ -1,17 +0,0 @@
// Google Maps
// Initialize and add the map
function initMap() {
// The map, centered at Uluru
const map = new google.maps.Map(document.getElementById("map"), {
zoom: 4,
center: '{$details->loc}',
});
// The marker, positioned at Uluru
const marker = new google.maps.Marker({
position: {'{$details->loc}',
map: map,
});
}
window.initMap = initMap;
-88
View File
@@ -1,88 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<title>uTraceMe - IP Lookup</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="IP Lookup">
<meta name="keywords" content="ip lookup, what is my ip, my ip address, my ip, ip address lookup, ip geolocation, latitude longitude finder, ip lookup php script, ip2location, geolocation, ip-location, my ip lookup, ip-lookup, geoip, geo ip, ip finder, ip tools, ip tools, ip location finder, location finder, what is my ip location, ip address geolocation, ????? ????, ???? ???? ?????, ????? ip, ?????? ????">
<meta name="author" content="Johannes Krüger">
<script async src='https://maps.googleapis.com/maps/api/js?key=AIzaSyCMPtVMDhHelORhyk2AAc9FtjgnjybvdMU&callback=initMap&v=weekly'></script>
<script type="module" src="./maps.js"></script>
</head>
<body>
<h2>Lookup IP Address Location</h2>
<br>
<?php
// Variable
$IP = $_SERVER['REMOTE_ADDR'];
$ip = htmlentities($_GET["ip"]);
$latitude = htmlentities($_POST['latitude'], ENT_QUOTES, 'UTF-8');
$longitude = htmlentities($_POST['longitude'], ENT_QUOTES, 'UTF-8');
$city = htmlentities($_POST['city'], ENT_QUOTES, 'UTF-8');
$details = json_decode(file_get_contents("http://ipinfo.io/{$ip}/json?token=391da55dff40d9"));
$location = json_decode(file_get_contents("http://ipinfo.io/{$ip}/json?token=391da55dff40d9"));
// start public Code
if(isset($_GET['ip']))
{
echo '<form method="get" action="">
<input type="text" name="ip" id="ip" maxlength="32" placeholder="IP" title="Enter IP Address here" />
<input type="submit" class="button" value="Lookup IP Address" />
</form>';
echo "<br><br><b>Short View</b><br>";
echo "<b>IP: </b>" .$details->ip;
echo "<br><b>Organisation: </b>" .$details->org;
echo "<br><b>Stadt: </b>" .$details->city;
echo "<br><b>Postleitzahl: </b>" .$details->postal;
echo "<br><b>Bundesland: </b>" .$details->region;
echo "<br><b>Land: </b>" .$details->country;
echo "<br><b>Lage: </b>" .$details->loc;
echo "<br><b>Hostname (rDNS): </b>" .$details->hostname;
echo "<br>";
//echo "<div style="border-radius: 10px;width: 480px;height: 240px;"><iframe src='https://maps.googleapis.com/maps/api/staticmap?center={$details->loc}&markers=color:red%7Clabel:S{$details->loc}&zoom=10&size=480x240&key=AIzaSyCMPtVMDhHelORhyk2AAc9FtjgnjybvdMU' FRAMEBORDER=NO FRAMESPACING=0 BORDER=0 ></iframe></div><br>";
}
else {
print ('<form method="get" action="">
<input type="text" name="ip" id="ip" maxlength="15" placeholder="IP" title="Enter IP Address here" value="'.$IP.'" />
<input type="submit" class="button" value="Lookup IP Address" />
</form>');
echo "<br>Here's what you will find out:<br><br>
<li>Your IP (but you can check other IP)</li>
<li>IP type</li>
<li>Continent code</li>
<li>Continent name</li>
<li>Country code</li>
<li>Country name</li>
<li>City</li>
<li>State/Region</li>
<li>Region code</li>
<li>Zip code</li>
<li>Calling code</li>
<li>Latitude</li>
<li>Longitude</li>
<li>Timezone</li>
<li>Currency</li>
<li>Mobile</li>
<li>Proxy</li>
<li>Organization</li>
<li>Hostname</li>
<li>Your Browser User-Agent</li>
<li>Geolocation Map</li>
<li>Map Latitude Longitude finder</li>
";
}
?>