mirror of
https://github.com/MrUnknownDE/utools.git
synced 2026-05-30 16:10:06 +02:00
Compare commits
72 Commits
old-stuff-KEKW
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| c698aa9692 | |||
| ea15c7e5b6 | |||
| eaf1c639d6 | |||
| fe70962fe8 | |||
| e71fd45b66 | |||
| 538ec0ca86 | |||
| a985f591c4 | |||
| 6dd2324624 | |||
| e95770bdce | |||
| bab3b59750 | |||
| 1c6802995f | |||
| 7f11612aa6 | |||
| aa38eddc07 | |||
| 2498d10f6d | |||
| b9cfe43986 | |||
| 0da70547aa | |||
| 6dfc86ce48 | |||
| be3aba6d86 | |||
| 3c1b5156a9 | |||
| 12ad5a86be | |||
| 8e2caf1e73 | |||
| 85c03bc483 | |||
| 3cd55bb7a9 | |||
| 75614e7cd2 | |||
| c0ddd1cf7d | |||
| 93a4574553 | |||
| 0fa3e6ac7c | |||
| 5fd7d49602 | |||
| cb4adabbc2 | |||
| 47efadd6d6 | |||
| 0e153db2c4 | |||
| 95b83f060b | |||
| 33e8400d51 | |||
| 98796edcbb | |||
| 320606ca3f | |||
| 4d00fd02cf | |||
| 168618c4fb | |||
| e6ee946cc3 | |||
| 2207a0d325 | |||
| 12370ea173 | |||
| 06e4971f1f | |||
| 84611f36f5 | |||
| f36e52a2fe | |||
| 12bb9a157e | |||
| d5b18295c0 | |||
| 039fda5797 | |||
| 4f8718c18a | |||
| 5d415a597b | |||
| b98f9047ef | |||
| 57c671e17b | |||
| 815cc48e9b | |||
| 2f070acaf4 | |||
| c59341552f | |||
| 7c38595984 | |||
| e560c8358c | |||
| 6dae7c356f | |||
| bee37d3eda | |||
| 02079ff639 | |||
| c399ea9d26 | |||
| 45e012c817 | |||
| 99eb24f665 | |||
| c1d9e41d43 | |||
| 4535631e9f | |||
| 654df54fa7 | |||
| 06862e2023 | |||
| f9daddc122 | |||
| 0a2163ff38 | |||
| b5c1908b04 | |||
| 0e52a0cd06 | |||
| 5266b05ce3 | |||
| b001ca6d29 | |||
| 7eac8b00ae |
@@ -0,0 +1,97 @@
|
||||
name: Build and Push Docker Images
|
||||
|
||||
# Trigger: Wann soll der Workflow laufen?
|
||||
on:
|
||||
workflow_dispatch: # Ermöglicht manuelles Starten über die GitHub UI
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest # Verwendet den neuesten Ubuntu-Runner von GitHub
|
||||
|
||||
# Berechtigungen für den GITHUB_TOKEN, um nach GHCR pushen zu können
|
||||
permissions:
|
||||
contents: read # Zum Auschecken des Codes
|
||||
packages: write # Zum Pushen nach GitHub Packages (GHCR)
|
||||
|
||||
steps:
|
||||
# 1. Code auschecken
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# 2. Docker Metadaten extrahieren (Tags, Labels)
|
||||
# Wir definieren hier die Namen für beide Images
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository_owner }}/utools-backend
|
||||
ghcr.io/${{ github.repository_owner }}/utools-frontend
|
||||
# Tags generieren:
|
||||
# - Typ 'sha' -> Kurzer Commit-Hash (z.B. sha-a1b2c3d)
|
||||
# - Typ 'ref' für Events 'branch' -> Branch-Name (z.B. 'main' wird zu 'latest')
|
||||
# - Typ 'ref' für Events 'tag' -> Git-Tag-Name (z.B. v1.0.0)
|
||||
tags: |
|
||||
type=sha,prefix=sha-
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=ref,event=pr # Nur für PR-Events
|
||||
|
||||
# 3. QEMU für Multi-Plattform Builds (optional, aber gute Praxis)
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# 4. Docker Buildx einrichten (verbesserter Builder)
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# 5. Login bei GitHub Container Registry (GHCR)
|
||||
# Verwendet den automatisch generierten GITHUB_TOKEN
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }} # Benutzer oder Organisation, dem das Repo gehört
|
||||
password: ${{ secrets.GHCR_PUSH_TOKEN }}
|
||||
|
||||
# 6. Backend Image bauen und pushen
|
||||
- name: Build and push Backend image
|
||||
id: build-backend
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./backend # Pfad zum Backend-Dockerfile
|
||||
# Nur pushen, wenn es ein Push zum main-Branch oder ein Git-Tag ist
|
||||
push: ${{ github.event_name == 'push' || github.event_name == 'create' && startsWith(github.ref, 'refs/tags/') }}
|
||||
# Tags und Labels aus dem Metadaten-Schritt verwenden (gefiltert für Backend)
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# Filter für das spezifische Backend-Image (Index 0 in der 'images'-Liste oben)
|
||||
# Wichtig: Passe den Index an, falls du die Reihenfolge änderst!
|
||||
image-name-index: 0 # Index des Backend-Images in der 'images'-Liste
|
||||
cache-from: type=gha # GitHub Actions Cache verwenden (Lesen)
|
||||
cache-to: type=gha,mode=max # GitHub Actions Cache verwenden (Schreiben)
|
||||
|
||||
# 7. Frontend Image bauen und pushen
|
||||
- name: Build and push Frontend image
|
||||
id: build-frontend
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./frontend # Pfad zum Frontend-Dockerfile
|
||||
# Nur pushen, wenn es ein Push zum main-Branch oder ein Git-Tag ist
|
||||
push: ${{ github.event_name == 'push' || github.event_name == 'create' && startsWith(github.ref, 'refs/tags/') }}
|
||||
# Tags und Labels aus dem Metadaten-Schritt verwenden (gefiltert für Frontend)
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# Filter für das spezifische Frontend-Image (Index 1 in der 'images'-Liste oben)
|
||||
image-name-index: 1 # Index des Frontend-Images in der 'images'-Liste
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# 8. (Optional) Output der Image-Namen und Tags
|
||||
- name: Print image names and tags
|
||||
if: always() # Auch ausführen, wenn vorherige Schritte fehlschlagen (zum Debuggen)
|
||||
run: |
|
||||
echo "Backend Image Tags: ${{ steps.meta.outputs.tags }}"
|
||||
echo "Frontend Image Tags: ${{ steps.meta.outputs.tags }}"
|
||||
echo "Backend Image Digest: ${{ steps.build-backend.outputs.digest }}"
|
||||
echo "Frontend Image Digest: ${{ steps.build-frontend.outputs.digest }}"
|
||||
@@ -0,0 +1,93 @@
|
||||
name: Update MaxMind GeoLite2 DBs
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Ermöglicht manuelles Starten
|
||||
schedule:
|
||||
# Läuft jeden Dienstag um 05:00 UTC (anpassbar)
|
||||
- cron: '0 5 * * 2'
|
||||
|
||||
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
|
||||
|
||||
- name: Download geoipupdate tool
|
||||
run: |
|
||||
# Lade eine spezifische Version oder die neueste herunter
|
||||
GEOIPUPDATE_VERSION="4.11.1" # Beispielversion, prüfe auf neuere Releases
|
||||
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
|
||||
# Wichtig: Secrets nicht direkt im Log ausgeben!
|
||||
run: |
|
||||
echo "Creating GeoIP.conf..."
|
||||
cat << EOF > GeoIP.conf
|
||||
# GeoIP.conf file for geoipupdate
|
||||
# Replace with your actual AccountID and LicenseKey from GitHub Secrets
|
||||
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."
|
||||
# Umgebungsvariablen für Sicherheit (werden nicht geloggt)
|
||||
env:
|
||||
MAXMIND_ACCOUNT_ID: ${{ secrets.MAXMIND_ACCOUNT_ID }}
|
||||
MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }}
|
||||
|
||||
- name: Run geoipupdate
|
||||
run: |
|
||||
echo "Running geoipupdate..."
|
||||
# -f gibt die Konfigurationsdatei an
|
||||
# -d gibt das Zielverzeichnis an (relativ zum Repo-Root)
|
||||
# -v für ausführliche Ausgabe (hilft beim Debuggen)
|
||||
geoipupdate -f GeoIP.conf -d ./backend/data -v
|
||||
echo "geoipupdate finished."
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
run: |
|
||||
# Prüfe, ob sich die .mmdb Dateien geändert haben
|
||||
if git status --porcelain | grep -q 'backend/data/.*\.mmdb'; then
|
||||
echo "Changes detected in MaxMind databases."
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No changes detected in MaxMind databases."
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Commit and push changes
|
||||
# Nur ausführen, wenn Schritt 'check_changes' Änderungen gemeldet hat
|
||||
if: steps.check_changes.outputs.changed == 'true'
|
||||
run: |
|
||||
echo "Committing and pushing changes..."
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
git add ./backend/data/*.mmdb
|
||||
# Erstelle Commit-Nachricht mit Datum
|
||||
COMMIT_DATE=$(date -u +"%Y-%m-%d")
|
||||
git commit -m "Update MaxMind GeoLite2 databases (${COMMIT_DATE})"
|
||||
# Pushe zum aktuellen Branch (z.B. main)
|
||||
git push
|
||||
echo "Changes pushed."
|
||||
# Umgebungsvariable für den Token (wird automatisch von GitHub bereitgestellt)
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: No changes to commit
|
||||
if: steps.check_changes.outputs.changed == 'false'
|
||||
run: echo "Skipping commit as no database files were updated."
|
||||
+144
@@ -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
|
||||
@@ -1,3 +1,165 @@
|
||||
# utools
|
||||
# ✨ utools - IP Information & Diagnostics Webapp ✨
|
||||
|
||||
Ich versuche mal was :)
|
||||
[](https://github.com/mrunknownde/utools/actions/workflows/docker-build-push.yml)
|
||||
[](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.
|
||||
|
||||
<!-- Optional: Füge hier einen Screenshot hinzu -->
|
||||
<!--  -->
|
||||
|
||||
### Livedemo: https://utools.johanneskr.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 Map:** Visualizes the geolocation on an OpenStreetMap.
|
||||
* **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 (on click) 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 (Network Address, Broadcast Address, Usable Hosts, etc.) for a given IP and CIDR or Subnet Mask.
|
||||
* **WHOIS Lookup:** Retrieves WHOIS information for a given domain or IP address.
|
||||
* **Dockerized:** Both frontend and backend are containerized for easy deployment.
|
||||
* **Modern UI:** Built with Tailwind CSS for a clean and responsive interface.
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
* **Backend:**
|
||||
* Node.js
|
||||
* Express.js
|
||||
* MaxMind GeoLite2 Databases (for GeoIP and ASN)
|
||||
* Pino (for logging)
|
||||
* `whois-json` (for WHOIS lookups)
|
||||
* `net`, `dns`, `child_process` (Node.js built-ins for Ping, Traceroute, rDNS, DNS Lookup)
|
||||
* `@sentry/node` (optional error tracking)
|
||||
* **Frontend:**
|
||||
* Vanilla JavaScript (ES6+)
|
||||
* Tailwind CSS (via Play CDN for simplicity, can be built)
|
||||
* Leaflet.js (for OpenStreetMap)
|
||||
* Nginx (for serving static files and as a reverse proxy)
|
||||
* **Deployment:**
|
||||
* Docker & Docker Compose
|
||||
* GitHub Actions (for CI/CD - building and pushing images to GHCR)
|
||||
|
||||
## 🏁 Getting Started
|
||||
|
||||
You can run this application easily using Docker and Docker Compose.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
* [Docker](https://docs.docker.com/get-docker/) installed
|
||||
* [Docker Compose](https://docs.docker.com/compose/install/) installed (usually included with Docker Desktop)
|
||||
|
||||
### Option 1: Using Pre-built Images (Recommended)
|
||||
|
||||
This method uses the Docker images automatically built and pushed to GitHub Container Registry (GHCR) by the GitHub Actions workflow.
|
||||
|
||||
1. **Create `docker-compose.yml`:**
|
||||
Save the following content as `docker-compose.yml` in a new directory on your machine:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
# Use the pre-built image from GHCR
|
||||
image: ghcr.io/mrunknownde/utools-backend:latest # Or specify a specific tag/sha
|
||||
container_name: utools_backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Production environment settings
|
||||
NODE_ENV: production
|
||||
PORT: 3000
|
||||
LOG_LEVEL: info # Adjust log level if needed (e.g., 'debug', 'warn')
|
||||
PING_COUNT: 4
|
||||
# Optional: Set Sentry DSN for error tracking if you use Sentry
|
||||
# SENTRY_DSN: "YOUR_SENTRY_DSN"
|
||||
dns:
|
||||
# Explicitly set reliable public DNS servers for rDNS lookups inside the container
|
||||
- 1.1.1.1 # Cloudflare DNS
|
||||
- 1.0.0.1 # Cloudflare DNS
|
||||
- 8.8.8.8 # Google DNS
|
||||
- 8.8.4.4 # Google DNS
|
||||
networks:
|
||||
- utools_network
|
||||
# Note: No ports exposed directly, access is via frontend proxy
|
||||
|
||||
frontend:
|
||||
# Use the pre-built image from GHCR
|
||||
image: ghcr.io/mrunknownde/utools-frontend:latest # Or specify a specific tag/sha
|
||||
container_name: utools_frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
# Expose port 8080 on the host, mapping to port 80 in the container (Nginx)
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
- backend # Ensures backend service is started first
|
||||
networks:
|
||||
- utools_network
|
||||
|
||||
networks:
|
||||
utools_network:
|
||||
driver: bridge
|
||||
name: utools_network # Give the network a specific name
|
||||
```
|
||||
|
||||
2. **Start the Application:**
|
||||
Open a terminal in the directory where you saved the `docker-compose.yml` file and run:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
*(Note: Use `docker-compose` (with hyphen) if you have an older version)*
|
||||
This will download the images (if not already present) and start the containers in the background.
|
||||
|
||||
3. **Access the Webapp:**
|
||||
Open your web browser and navigate to `http://localhost:8080`.
|
||||
|
||||
### Option 2: Building Images from Source
|
||||
|
||||
If you want to modify the code or build the images yourself:
|
||||
|
||||
1. **Clone the Repository:**
|
||||
```bash
|
||||
git clone https://github.com/mrunknownde/utools.git
|
||||
cd utools
|
||||
```
|
||||
2. **Build and Start:**
|
||||
Use Docker Compose to build the images based on the `Dockerfile`s in the `backend` and `frontend` directories and then start the containers:
|
||||
```bash
|
||||
# Optional: Set GIT_COMMIT_SHA for build args if needed
|
||||
# export GIT_COMMIT_SHA=$(git rev-parse --short HEAD)
|
||||
docker compose -f compose.yml up -d --build
|
||||
```
|
||||
*(Note: Use `docker-compose` (with hyphen) if you have an older version. The `compose.yml` in the repository correctly uses `build:` directives)*
|
||||
|
||||
3. **Access the Webapp:**
|
||||
Open your web browser and navigate to `http://localhost:8080`.
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
The application is configured mainly through environment variables set in the `docker-compose.yml` file for the `backend` service:
|
||||
|
||||
* `NODE_ENV`: Set to `production` for optimal performance and JSON logging.
|
||||
* `PORT`: The internal port the Node.js application listens on (default: `3000`).
|
||||
* `LOG_LEVEL`: Controls the logging verbosity (e.g., `debug`, `info`, `warn`, `error`).
|
||||
* `PING_COUNT`: Number of ping packets to send (default: `4`).
|
||||
* `SENTRY_DSN` (Optional): Your Sentry Data Source Name for error tracking. Can be set during build via args or at runtime via environment variable.
|
||||
* `dns` (in compose): Specifies DNS servers for the backend container, crucial for reliable rDNS lookups.
|
||||
|
||||
The MaxMind database paths (`GEOIP_CITY_DB`, `GEOIP_ASN_DB`) are set within the backend's Dockerfile but could potentially be overridden if needed (e.g., using volumes).
|
||||
|
||||
## 🌐 Data Sources
|
||||
|
||||
* **Geolocation & ASN:** This tool uses GeoLite2 data created by MaxMind, available from [https://www.maxmind.com](https://www.maxmind.com).
|
||||
* **Important:** The GeoLite2 databases require periodic updates. You need a MaxMind account (free) to download them. The Docker images contain the databases at build time. For long-term use, you should implement a process to update the `.mmdb` files within the `backend/data` directory (if using volumes) or rebuild the backend image regularly using the provided GitHub Action workflow (`maxmind-update.yml`).
|
||||
* **Map Tiles:** Provided by OpenStreetMap contributors.
|
||||
* **WHOIS Data:** Retrieved in real-time using the `whois-json` library, which queries standard WHOIS servers.
|
||||
* **DNS Data:** Retrieved in real-time using Node.js' built-in `dns` module.
|
||||
|
||||
## 📜 License
|
||||
|
||||
This project is licensed under the MIT License. See the `LICENSE` file for details.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.git
|
||||
.env
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
*.md
|
||||
@@ -0,0 +1,72 @@
|
||||
# Stage 1: Build Dependencies
|
||||
# Use an official Node.js runtime as a parent image
|
||||
FROM node:18-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:18-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
|
||||
# Optional: Change ownership of 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"]
|
||||
@@ -0,0 +1,72 @@
|
||||
# Stage 1: Build Dependencies
|
||||
# Use an official Node.js runtime as a parent image
|
||||
FROM node:18-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:18-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
|
||||
# Optional: Change ownership of 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"]
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 58 MiB |
@@ -0,0 +1,7 @@
|
||||
# .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
|
||||
@@ -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,
|
||||
};
|
||||
Generated
+2558
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"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": "^8.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"macaddress": "^0.5.3",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"whois-json": "^2.0.4"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,219 @@
|
||||
const express = require('express');
|
||||
const { exec } = require('child_process');
|
||||
const router = express.Router();
|
||||
const os = require('os'); // Für Timeout-Signal
|
||||
|
||||
// Funktion zum Parsen der openssl x509 -text Ausgabe
|
||||
function parseSslOutput(output) {
|
||||
const result = {
|
||||
issuer: null,
|
||||
subject: null,
|
||||
validFrom: null,
|
||||
validTo: null,
|
||||
validity: "Could not determine validity", // Standardwert
|
||||
error: null,
|
||||
details: output // Rohausgabe für Debugging/Anzeige
|
||||
};
|
||||
|
||||
try {
|
||||
// Extrahiere Issuer und Subject (robusterer Regex, der Zeilenumbrüche berücksichtigt)
|
||||
const issuerMatch = output.match(/Issuer:([^\n]+(?:\n\s+[^\n]+)*)/);
|
||||
if (issuerMatch) result.issuer = issuerMatch[1].replace(/\n\s+/g, ' ').trim();
|
||||
|
||||
const subjectMatch = output.match(/Subject:([^\n]+(?:\n\s+[^\n]+)*)/);
|
||||
if (subjectMatch) result.subject = subjectMatch[1].replace(/\n\s+/g, ' ').trim();
|
||||
|
||||
// Extrahiere Gültigkeitsdaten (verschiedene Datumsformate berücksichtigen)
|
||||
const validFromMatch = output.match(/Not Before\s*:\s*(.+)/);
|
||||
if (validFromMatch) {
|
||||
try {
|
||||
result.validFrom = new Date(validFromMatch[1].trim()).toISOString();
|
||||
} catch (dateError) {
|
||||
console.warn("Could not parse 'Not Before' date:", validFromMatch[1].trim());
|
||||
}
|
||||
}
|
||||
|
||||
const validToMatch = output.match(/Not After\s*:\s*(.+)/);
|
||||
if (validToMatch) {
|
||||
try {
|
||||
result.validTo = new Date(validToMatch[1].trim()).toISOString();
|
||||
} catch (dateError) {
|
||||
console.warn("Could not parse 'Not After' date:", validToMatch[1].trim());
|
||||
}
|
||||
}
|
||||
|
||||
// Bewerte Gültigkeit basierend auf geparsten Daten
|
||||
if (result.validFrom && result.validTo) {
|
||||
const now = new Date();
|
||||
const validFromDate = new Date(result.validFrom);
|
||||
const validToDate = new Date(result.validTo);
|
||||
if (!isNaN(validFromDate) && !isNaN(validToDate)) { // Prüfen ob Daten gültig sind
|
||||
if (now < validFromDate) {
|
||||
result.validity = "Invalid (Not Yet Valid)";
|
||||
} else if (now > validToDate) {
|
||||
result.validity = "Invalid (Expired)";
|
||||
} else {
|
||||
result.validity = "Valid";
|
||||
}
|
||||
} else {
|
||||
result.validity = "Could not parse validity dates";
|
||||
}
|
||||
} else {
|
||||
result.validity = "Could not extract validity dates";
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("Error parsing openssl output:", e);
|
||||
result.error = "Error parsing certificate details.";
|
||||
result.validity = "Parsing Error"; // Spezifischer Status
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Einfache Domain-Validierung (grundlegend)
|
||||
function isValidDomain(domain) {
|
||||
// Erlaubt Buchstaben, Zahlen, Bindestriche und Punkte. Muss mit Buchstabe/Zahl beginnen/enden.
|
||||
// Nicht perfekt (z.B. IDNs), aber fängt grundlegende Fehler ab.
|
||||
const domainRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||
// Zusätzliche Längenprüfung
|
||||
return domain && domain.length <= 253 && domainRegex.test(domain);
|
||||
}
|
||||
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const domain = req.query.domain;
|
||||
|
||||
if (!domain) {
|
||||
return res.status(400).json({ error: 'Domain parameter is required' });
|
||||
}
|
||||
|
||||
// Grundlegende Validierung der Domain
|
||||
if (!isValidDomain(domain)) {
|
||||
return res.status(400).json({ error: 'Invalid domain format provided' });
|
||||
}
|
||||
|
||||
// Verwende Port 443 für HTTPS. Timeout nach 10 Sekunden.
|
||||
// Leite stderr nicht mehr nach /dev/null um, um Fehler von s_client zu sehen.
|
||||
// Verwende -brief für eine kompaktere Ausgabe, falls -text fehlschlägt
|
||||
const command = `echo "" | openssl s_client -servername ${domain} -connect ${domain}:443 -showcerts 2>&1 | openssl x509 -noout -text`;
|
||||
const timeoutMs = 10000; // 10 Sekunden
|
||||
|
||||
const child = exec(command, { timeout: timeoutMs }, (error, stdout, stderr) => {
|
||||
// WICHTIG: stderr wird hier durch 2>&1 im Befehl in stdout umgeleitet!
|
||||
// Daher prüfen wir stdout auf Fehlermuster und error auf Exit-Code.
|
||||
|
||||
const combinedOutput = stdout || ""; // stdout enthält jetzt auch stderr
|
||||
|
||||
if (error) {
|
||||
console.error(`exec error for domain ${domain}:`, error);
|
||||
let errorMessage = 'Failed to execute openssl command.';
|
||||
let errorDetails = combinedOutput || error.message; // Bevorzuge Output, wenn vorhanden
|
||||
|
||||
// Versuche, spezifischere Fehler aus der Ausgabe zu erkennen
|
||||
if (error.signal === 'SIGTERM' || (error.code === null && error.signal === os.constants.signals.SIGTERM)) { // Expliziter Timeout Check
|
||||
errorMessage = `Connection timed out after ${timeoutMs / 1000} seconds.`;
|
||||
errorDetails = `Timeout while trying to connect to ${domain}:443`;
|
||||
} else if (combinedOutput.includes("getaddrinfo: Name or service not known") || combinedOutput.includes("nodename nor servname provided, or not known") || combinedOutput.includes("failed to get server ip address")) {
|
||||
errorMessage = `Could not resolve domain: ${domain}`;
|
||||
} else if (combinedOutput.includes("connect: Connection refused")) {
|
||||
errorMessage = `Connection refused by ${domain}:443. Is the server running and accepting connections?`;
|
||||
} else if (combinedOutput.includes("connect:errno=") || combinedOutput.includes("SSL_connect:failed")) {
|
||||
errorMessage = `Could not establish SSL connection to ${domain}:443.`;
|
||||
} else if (combinedOutput.includes("unable to load certificate") || combinedOutput.includes("Expecting: TRUSTED CERTIFICATE")) {
|
||||
errorMessage = `Could not retrieve or parse certificate from ${domain}. Server might not be sending a valid certificate.`;
|
||||
} else if (error.code) {
|
||||
errorMessage = `OpenSSL command failed with exit code ${error.code}.`;
|
||||
}
|
||||
|
||||
return res.status(500).json({ error: errorMessage, details: errorDetails });
|
||||
}
|
||||
|
||||
// Wenn kein Fehler aufgetreten ist, aber stdout leer ist (sollte nicht passieren wegen 2>&1, aber sicherheitshalber)
|
||||
if (!combinedOutput.trim()) {
|
||||
console.warn(`Empty output received for domain ${domain}, although no exec error occurred.`);
|
||||
return res.status(500).json({ error: 'Received empty response from openssl command.' });
|
||||
}
|
||||
|
||||
// Versuche, das Zertifikat zu parsen
|
||||
const certInfo = parseSslOutput(combinedOutput); // Parse die kombinierte Ausgabe
|
||||
|
||||
// Wenn das Parsen fehlschlägt ODER keine relevanten Infos gefunden wurden
|
||||
if (certInfo.error || (!certInfo.issuer && !certInfo.subject && !certInfo.validTo)) {
|
||||
// Möglicherweise war die Ausgabe nur eine Fehlermeldung von s_client oder x509
|
||||
console.warn(`Could not parse certificate details for ${domain}. Raw output:`, combinedOutput);
|
||||
// Gib einen spezifischeren Fehler zurück, wenn möglich
|
||||
let parseErrorMsg = certInfo.error || `Could not extract certificate details from the server response.`;
|
||||
if (combinedOutput.includes("connect:errno=")) {
|
||||
parseErrorMsg = `Could not establish SSL connection to ${domain}:443.`;
|
||||
} else if (combinedOutput.toLowerCase().includes("no certificate")) {
|
||||
parseErrorMsg = `Server at ${domain}:443 did not present a certificate.`;
|
||||
}
|
||||
return res.status(500).json({ error: parseErrorMsg, details: combinedOutput });
|
||||
}
|
||||
|
||||
|
||||
// Einfache Bewertung hinzufügen
|
||||
let score = 0;
|
||||
let evaluation = [];
|
||||
if (certInfo.validity === "Valid") {
|
||||
score += 5; // Basispunktzahl für Gültigkeit
|
||||
evaluation.push("Certificate is currently valid.");
|
||||
|
||||
// Prüfe die verbleibende Gültigkeitsdauer
|
||||
try {
|
||||
const daysRemaining = Math.floor((new Date(certInfo.validTo) - new Date()) / (1000 * 60 * 60 * 24));
|
||||
if (!isNaN(daysRemaining)) {
|
||||
if (daysRemaining < 14) { // Strengere Warnung
|
||||
score -= 3;
|
||||
evaluation.push(`Warning: Certificate expires in ${daysRemaining} days (less than 14 days).`);
|
||||
} else if (daysRemaining < 30) {
|
||||
score -= 1;
|
||||
evaluation.push(`Warning: Certificate expires in ${daysRemaining} days (less than 30 days).`);
|
||||
} else {
|
||||
score += 2; // Bonus für gute Restlaufzeit
|
||||
evaluation.push(`Certificate expires in ${daysRemaining} days.`);
|
||||
}
|
||||
} else {
|
||||
evaluation.push("Could not calculate remaining days.");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Could not calculate remaining days:", e);
|
||||
evaluation.push("Could not calculate remaining days.");
|
||||
}
|
||||
} else {
|
||||
// Keine Punkte für ungültige Zertifikate
|
||||
evaluation.push(`Certificate is not valid (${certInfo.validity}).`);
|
||||
}
|
||||
|
||||
// Weitere Prüfungen könnten hier hinzugefügt werden
|
||||
|
||||
res.json({
|
||||
domain: domain,
|
||||
certificate: { // Nur relevante Infos senden, nicht die ganze Roh-Ausgabe im Hauptobjekt
|
||||
issuer: certInfo.issuer,
|
||||
subject: certInfo.subject,
|
||||
validFrom: certInfo.validFrom,
|
||||
validTo: certInfo.validTo,
|
||||
validity: certInfo.validity,
|
||||
details: certInfo.details // Roh-Details bleiben für die Anzeige im Frontend
|
||||
},
|
||||
evaluation: {
|
||||
score: Math.max(0, Math.min(10, score)), // Score zwischen 0 und 10 begrenzen
|
||||
summary: evaluation.join(' ')
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Timeout-Handling (falls das interne Timeout von exec nicht greift)
|
||||
const timer = setTimeout(() => {
|
||||
console.warn(`Forcing termination of openssl command for ${domain} after ${timeoutMs}ms`);
|
||||
child.kill('SIGTERM'); // Versuche, den Prozess sauber zu beenden
|
||||
}, timeoutMs + 1000); // Gib dem internen Timeout eine kleine Gnadenfrist
|
||||
|
||||
child.on('exit', () => {
|
||||
clearTimeout(timer); // Timer löschen, wenn der Prozess normal endet
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,166 @@
|
||||
// backend/routes/traceroute.js
|
||||
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.' });
|
||||
}
|
||||
|
||||
// Add specific context for this request to the current Sentry scope
|
||||
// Errors/Messages captured later in this request handler will have this context.
|
||||
Sentry.configureScope(scope => {
|
||||
scope.setContext("traceroute_details", { targetIp: targetIp, requestIp: requestIp });
|
||||
});
|
||||
|
||||
|
||||
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();
|
||||
// No manual transaction finishing needed here
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
// logger.debug({ requestIp, targetIp, hop: parsed.hop, ip: parsed.ip }, 'Sending hop data');
|
||||
sendEvent('hop', parsed);
|
||||
} else if (line.trim()) {
|
||||
// logger.debug({ requestIp, targetIp, message: line.trim() }, 'Sending info data');
|
||||
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();
|
||||
// No manual transaction finishing needed here
|
||||
});
|
||||
|
||||
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 });
|
||||
// Transaction status will be inferred by Sentry based on errors captured
|
||||
} else {
|
||||
logger.info({ requestIp, targetIp }, `Traceroute stream completed successfully.`);
|
||||
// Transaction status will likely be 'ok' if no errors were captured
|
||||
}
|
||||
sendEvent('end', { exitCode: code });
|
||||
if (!res.writableEnded) res.end();
|
||||
// No manual transaction finishing needed here
|
||||
});
|
||||
|
||||
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();
|
||||
// Sentry transaction might be marked as 'cancelled' automatically or based on timeout
|
||||
// No manual transaction finishing needed here
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// This catch handles errors during the initial setup (e.g., spawn fails immediately)
|
||||
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 } }); // Capture original error
|
||||
// No manual transaction finishing needed here
|
||||
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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 } = 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.' });
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -0,0 +1,212 @@
|
||||
// server.js
|
||||
// Load .env variables FIRST!
|
||||
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,
|
||||
});
|
||||
|
||||
// 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 sslCheckRoutes = require('./routes/sslCheck'); // <-- NEUE ROUTE IMPORTIERT
|
||||
|
||||
// --- 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: 5 * 60 * 1000, // 5 minutes
|
||||
max: parseInt(process.env.RATE_LIMIT_MAX || (process.env.NODE_ENV === 'production' ? '20' : '200'), 10), // Requests per window per IP, ensure integer
|
||||
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 5 minutes' },
|
||||
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 specific API routes that perform external actions
|
||||
// Note: /api/ipinfo and /api/version are often excluded as they are less resource-intensive
|
||||
app.use('/api/ping', generalLimiter);
|
||||
app.use('/api/traceroute', generalLimiter);
|
||||
app.use('/api/lookup', generalLimiter);
|
||||
app.use('/api/dns-lookup', generalLimiter);
|
||||
app.use('/api/whois-lookup', generalLimiter);
|
||||
app.use('/api/ssl-check', generalLimiter); // <-- RATE LIMITER FÜR NEUE ROUTE
|
||||
|
||||
|
||||
// --- 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/ssl-check', sslCheckRoutes); // <-- NEUE ROUTE REGISTRIERT
|
||||
|
||||
|
||||
// --- 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));
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
// 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;
|
||||
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)
|
||||
);
|
||||
} else if (ipVersion === 6) {
|
||||
const lowerCaseIp = ip.toLowerCase();
|
||||
return (
|
||||
lowerCaseIp === '::1' || // ::1/128 (Loopback)
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
isValidIp,
|
||||
isPrivateIp,
|
||||
isValidDomain,
|
||||
getCleanIp,
|
||||
executeCommand,
|
||||
parsePingOutput,
|
||||
parseTracerouteLine,
|
||||
// Note: logger is not exported, assuming it's managed globally or passed where needed
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
docker compose down
|
||||
export GIT_COMMIT_SHA=$(git rev-parse --short HEAD)
|
||||
docker compose -f compose.dev.yml up -d --build
|
||||
@@ -0,0 +1,3 @@
|
||||
docker compose down
|
||||
export GIT_COMMIT_SHA=$(git rev-parse --short HEAD)
|
||||
docker compose -f compose.yml up -d --build
|
||||
@@ -0,0 +1,54 @@
|
||||
services:
|
||||
# Backend Service (Node.js App)
|
||||
backend-dev:
|
||||
build:
|
||||
context: ./backend # Pfad zum Verzeichnis mit dem Backend-Dockerfile
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
# Übergibt den Git Commit Hash als Build-Argument.
|
||||
# Erwartet, dass GIT_COMMIT_SHA in der Shell-Umgebung gesetzt ist (z.B. export GIT_COMMIT_SHA=$(git rev-parse --short HEAD))
|
||||
- GIT_COMMIT_SHA=${GIT_COMMIT_SHA:-unknown}
|
||||
# Übergibt den Sentry DSN als Build-Argument (optional, falls im Code benötigt)
|
||||
- SENTRY_DSN="https://7ea70caba68f548fb96482a573006a7b@o447623.ingest.us.sentry.io/4509062020333568"
|
||||
container_name: utools_backend_dev # Eindeutiger Name für den Container
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Setze Umgebungsvariablen für das Backend
|
||||
NODE_ENV: production # Wichtig für Performance und Logging
|
||||
PORT: 3000 # Port innerhalb des Containers
|
||||
LOG_LEVEL: info # Oder 'warn' für weniger Logs in Produktion
|
||||
PING_COUNT: 4
|
||||
# Die DB-Pfade werden aus dem Backend-Dockerfile ENV genommen,
|
||||
# könnten hier aber überschrieben werden, falls nötig.
|
||||
# GEOIP_CITY_DB: ./data/GeoLite2-City.mmdb
|
||||
# GEOIP_ASN_DB: ./data/GeoLite2-ASN.mmdb
|
||||
# Sentry DSN aus der Umgebung/ .env Datei übernehmen
|
||||
SENTRY_DSN: "https://7ea70caba68f548fb96482a573006a7b@o447623.ingest.us.sentry.io/4509062020333568" # Wichtig für die Laufzeit
|
||||
dns:
|
||||
- 1.1.1.1 # Cloudflare DNS
|
||||
- 1.0.0.1 # Cloudflare DNS
|
||||
- 8.8.8.8 # Google DNS
|
||||
- 8.8.4.4 # Google DNS
|
||||
networks:
|
||||
- utools_network # Verbinde mit unserem benutzerdefinierten Netzwerk
|
||||
|
||||
# Frontend Service (Nginx)
|
||||
frontend-dev:
|
||||
build:
|
||||
context: ./frontend # Pfad zum Verzeichnis mit dem Frontend-Dockerfile
|
||||
dockerfile: Dockerfile.dev
|
||||
container_name: utools_frontend_dev
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
# Mappe Port 8080 vom Host auf Port 80 im Container (wo Nginx lauscht)
|
||||
# Zugriff von außen (Browser) erfolgt über localhost:8080
|
||||
- "127.0.0.1:5874:80"
|
||||
depends_on:
|
||||
- backend-dev # Stellt sicher, dass Backend gestartet wird (aber nicht unbedingt bereit ist)
|
||||
networks:
|
||||
- utools_network # Verbinde mit unserem benutzerdefinierten Netzwerk
|
||||
|
||||
# Definiere ein benutzerdefiniertes Netzwerk (gute Praxis)
|
||||
networks:
|
||||
utools_network:
|
||||
driver: bridge # Standard-Netzwerktreiber
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
services:
|
||||
# Backend Service (Node.js App)
|
||||
backend:
|
||||
build:
|
||||
context: ./backend # Pfad zum Verzeichnis mit dem Backend-Dockerfile
|
||||
args:
|
||||
# Übergibt den Git Commit Hash als Build-Argument.
|
||||
# Erwartet, dass GIT_COMMIT_SHA in der Shell-Umgebung gesetzt ist (z.B. export GIT_COMMIT_SHA=$(git rev-parse --short HEAD))
|
||||
- GIT_COMMIT_SHA=${GIT_COMMIT_SHA:-unknown}
|
||||
# Übergibt den Sentry DSN als Build-Argument (optional, falls im Code benötigt)
|
||||
- SENTRY_DSN="https://7ea70caba68f548fb96482a573006a7b@o447623.ingest.us.sentry.io/4509062020333568"
|
||||
container_name: utools_backend # Eindeutiger Name für den Container
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Setze Umgebungsvariablen für das Backend
|
||||
NODE_ENV: production # Wichtig für Performance und Logging
|
||||
PORT: 3000 # Port innerhalb des Containers
|
||||
LOG_LEVEL: info # Oder 'warn' für weniger Logs in Produktion
|
||||
PING_COUNT: 4
|
||||
# Die DB-Pfade werden aus dem Backend-Dockerfile ENV genommen,
|
||||
# könnten hier aber überschrieben werden, falls nötig.
|
||||
# GEOIP_CITY_DB: ./data/GeoLite2-City.mmdb
|
||||
# GEOIP_ASN_DB: ./data/GeoLite2-ASN.mmdb
|
||||
# Sentry DSN aus der Umgebung/ .env Datei übernehmen
|
||||
SENTRY_DSN: "https://7ea70caba68f548fb96482a573006a7b@o447623.ingest.us.sentry.io/4509062020333568" # Wichtig für die Laufzeit
|
||||
dns:
|
||||
- 1.1.1.1 # Cloudflare DNS
|
||||
- 1.0.0.1 # Cloudflare DNS
|
||||
- 8.8.8.8 # Google DNS
|
||||
- 8.8.4.4 # Google DNS
|
||||
networks:
|
||||
- utools_network # Verbinde mit unserem benutzerdefinierten Netzwerk
|
||||
|
||||
# Frontend Service (Nginx)
|
||||
frontend:
|
||||
build: ./frontend # Pfad zum Verzeichnis mit dem Frontend-Dockerfile
|
||||
container_name: utools_frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
# Mappe Port 8080 vom Host auf Port 80 im Container (wo Nginx lauscht)
|
||||
# Zugriff von außen (Browser) erfolgt über localhost:8080
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
- backend # Stellt sicher, dass Backend gestartet wird (aber nicht unbedingt bereit ist)
|
||||
networks:
|
||||
- utools_network # Verbinde mit unserem benutzerdefinierten Netzwerk
|
||||
|
||||
# Definiere ein benutzerdefiniertes Netzwerk (gute Praxis)
|
||||
networks:
|
||||
utools_network:
|
||||
driver: bridge # Standard-Netzwerktreiber
|
||||
@@ -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
|
||||
@@ -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.dev.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
|
||||
@@ -0,0 +1,108 @@
|
||||
<!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 */
|
||||
.loader {
|
||||
border: 4px solid rgba(168, 85, 247, 0.3); /* Lila transparent */
|
||||
border-left-color: #a855f7; /* Lila */
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
/* Ergebnis-Pre-Formatierung */
|
||||
.result-pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
background-color: #1f2937; /* Dunkelgrau */
|
||||
color: #d1d5db; /* Hellgrau */
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem; /* rounded-md */
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
}
|
||||
/* Navigations-Styling */
|
||||
nav ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 1rem; }
|
||||
nav a { color: #c4b5fd; text-decoration: none; white-space: nowrap; }
|
||||
nav a:hover { color: #a78bfa; text-decoration: underline; }
|
||||
header { background-color: #374151; padding: 1rem; margin-bottom: 1.5rem; border-radius: 0.5rem; display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
|
||||
@media (min-width: 768px) { header { flex-direction: row; justify-content: space-between; } }
|
||||
header h1 { font-size: 1.5rem; font-weight: bold; color: #e5e7eb; }
|
||||
.hidden { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-900 text-gray-200 font-sans p-4 md:p-8">
|
||||
|
||||
<header>
|
||||
<h1>uTools Network Suite</h1>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="index.html">IP Info & Tools</a></li>
|
||||
<li><a href="subnet-calculator.html">Subnetz Rechner</a></li>
|
||||
<li><a href="dns-lookup.html">DNS Lookup</a></li>
|
||||
<li><a href="whois-lookup.html">WHOIS Lookup</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container mx-auto max-w-4xl bg-gray-800 rounded-lg shadow-xl p-6">
|
||||
|
||||
<h1 class="text-3xl font-bold mb-6 text-purple-400 text-center">DNS Lookup</h1>
|
||||
|
||||
<!-- Bereich für DNS Lookup -->
|
||||
<div class="mt-8 p-4 bg-gray-700 rounded">
|
||||
<div class="flex flex-col sm:flex-row gap-2 mb-4">
|
||||
<input type="text" id="dns-domain-input" placeholder="Enter domain name (e.g., google.com)"
|
||||
class="flex-grow px-3 py-2 bg-gray-800 border border-gray-600 rounded text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono">
|
||||
<select id="dns-type-select" class="px-3 py-2 bg-gray-800 border border-gray-600 rounded text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
||||
<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-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded transition duration-150 ease-in-out">
|
||||
Lookup DNS
|
||||
</button>
|
||||
</div>
|
||||
<div id="dns-lookup-error" class="text-red-400 mb-4 hidden"></div>
|
||||
<div id="dns-lookup-results-section" class="hidden mt-4 border-t border-gray-600 pt-4">
|
||||
<h3 class="text-lg font-semibold text-purple-300 mb-2">DNS Results for: <span id="dns-lookup-query" class="font-mono text-purple-400"></span></h3>
|
||||
<div id="dns-lookup-loader" class="loader hidden mb-2"></div>
|
||||
<pre id="dns-lookup-output" class="result-pre"></pre> <!-- Ergebnisbereich -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Globaler Fehlerbereich -->
|
||||
<div id="global-error" class="mt-6 p-4 bg-red-800 text-red-100 rounded hidden"></div>
|
||||
|
||||
<!-- Footer für Version -->
|
||||
<footer class="mt-8 pt-4 border-t border-gray-600 text-center text-xs text-gray-500">
|
||||
<p>© 2025 <a href="https://johanneskr.de" class="text-purple-400 hover:underline">Johannes Krüger</a></p>
|
||||
<p>Version: <span id="commit-sha" class="font-mono">loading...</span></p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Eigene JS-Logik für diese Seite -->
|
||||
<script src="dns-lookup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,132 @@
|
||||
// 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) {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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;
|
||||
}
|
||||
fetchAndDisplay(
|
||||
'/dns-lookup',
|
||||
{ domain, type },
|
||||
dnsLookupResultsSection,
|
||||
dnsLookupLoader,
|
||||
dnsLookupErrorEl,
|
||||
dnsLookupQueryEl,
|
||||
dnsLookupOutputEl,
|
||||
displayDnsResults
|
||||
);
|
||||
}
|
||||
|
||||
// --- Initial Load & Event Listeners ---
|
||||
fetchVersionInfo(); // Lade Versionsinfo für Footer
|
||||
|
||||
dnsLookupButton.addEventListener('click', handleDnsLookupClick);
|
||||
dnsDomainInput.addEventListener('keypress', (event) => {
|
||||
if (event.key === 'Enter') handleDnsLookupClick();
|
||||
});
|
||||
|
||||
}); // End DOMContentLoaded
|
||||
@@ -0,0 +1,262 @@
|
||||
<!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.3); /* Lila transparent */
|
||||
border-left-color: #a855f7; /* Lila */
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Basis für Glitch-Effekt (Beispiel: Text-Schatten) */
|
||||
.glitch-text:hover {
|
||||
text-shadow:
|
||||
1px 1px 0px rgba(168, 85, 247, 0.7), /* Lila */
|
||||
-1px -1px 0px rgba(76, 29, 149, 0.7); /* Dunkleres Lila */
|
||||
}
|
||||
/* Klickbarer IP-Cursor und Link-Styling */
|
||||
#ip-address-link {
|
||||
cursor: pointer;
|
||||
text-decoration: none; /* Standard-Link-Unterstreichung entfernen */
|
||||
}
|
||||
#ip-address-link:hover {
|
||||
text-decoration: underline; /* Unterstreichung beim Hover */
|
||||
}
|
||||
|
||||
/* Traceroute Output Formatierung */
|
||||
#traceroute-output pre, .result-pre { /* Gemeinsamer Stil für <pre> */
|
||||
white-space: pre-wrap; /* Zeilenumbruch */
|
||||
word-break: break-all; /* Lange Zeilen umbrechen */
|
||||
font-family: monospace;
|
||||
background-color: #1f2937; /* Dunkelgrau */
|
||||
color: #d1d5db; /* Hellgrau */
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem; /* rounded-md */
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
}
|
||||
#traceroute-output .hop-line { margin-bottom: 0.25rem; }
|
||||
#traceroute-output .hop-number { display: inline-block; width: 30px; text-align: right; margin-right: 10px; color: #9ca3af; } /* Grau */
|
||||
#traceroute-output .hop-ip { color: #60a5fa; } /* Blau */
|
||||
#traceroute-output .hop-hostname { color: #a78bfa; } /* Lila */
|
||||
#traceroute-output .hop-rtt { color: #34d399; margin-left: 5px;} /* Grün */
|
||||
#traceroute-output .hop-timeout { color: #f87171; } /* Rot */
|
||||
#traceroute-output .info-line { color: #fbbf24; font-style: italic;} /* Gelb */
|
||||
#traceroute-output .error-line { color: #f87171; font-weight: bold;} /* Rot */
|
||||
#traceroute-output .end-line { color: #a78bfa; font-weight: bold; margin-top: 10px;} /* Lila */
|
||||
|
||||
/* Navigations-Styling */
|
||||
nav ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 1rem; } /* flex-wrap hinzugefügt */
|
||||
nav a { color: #c4b5fd; /* purple-300 */ text-decoration: none; white-space: nowrap; } /* nowrap hinzugefügt */
|
||||
nav a:hover { color: #a78bfa; /* purple-400 */ text-decoration: underline; }
|
||||
header { background-color: #374151; /* gray-700 */ padding: 1rem; margin-bottom: 1.5rem; border-radius: 0.5rem; /* rounded-lg */ display: flex; flex-direction: column; align-items: center; gap: 0.5rem; } /* Flex direction geändert */
|
||||
@media (min-width: 768px) { /* md breakpoint */
|
||||
header { flex-direction: row; justify-content: space-between; }
|
||||
}
|
||||
header h1 { font-size: 1.5rem; /* text-2xl */ font-weight: bold; color: #e5e7eb; /* gray-200 */ }
|
||||
|
||||
/* Hilfsklasse zum Verstecken */
|
||||
.hidden { display: none; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-900 text-gray-200 font-sans p-4 md:p-8">
|
||||
|
||||
<header>
|
||||
<h1>uTools Network Suite</h1> <!-- Name angepasst -->
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="index.html">IP Info & Tools</a></li> <!-- Angepasst -->
|
||||
<li><a href="subnet-calculator.html">Subnetz Rechner</a></li>
|
||||
<li><a href="dns-lookup.html">DNS Lookup</a></li>
|
||||
<li><a href="whois-lookup.html">WHOIS Lookup</a></li>
|
||||
<li><a href="ssl-check.html">SSL Check</a></li> <!-- <-- NEUER LINK -->
|
||||
<!-- REMOVED: MAC Lookup Link -->
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container mx-auto max-w-4xl bg-gray-800 rounded-lg shadow-xl p-6">
|
||||
|
||||
<h1 class="text-3xl font-bold mb-6 text-purple-400 glitch-text text-center">IP Information</h1> <!-- Titel angepasst -->
|
||||
|
||||
<!-- Bereich für EIGENE IP-Infos -->
|
||||
<div id="info-section" class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
|
||||
<!-- Linke Spalte: Eigene IP, Geo, ASN, rDNS -->
|
||||
<div class="space-y-4 p-4 bg-gray-700 rounded">
|
||||
<h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1">Your Public IP</h2>
|
||||
<div id="ip-info" class="min-h-[50px]">
|
||||
<div id="ip-loader" class="loader"></div>
|
||||
<!-- Geändert zu <a> Tag -->
|
||||
<a id="ip-address-link" href="#" class="text-2xl font-mono font-bold text-purple-400 break-all hidden" title="Go to WHOIS Lookup for this IP">
|
||||
<span id="ip-address"></span> <!-- Span für den eigentlichen Text -->
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1">Geolocation</h2>
|
||||
<div id="geo-info" class="min-h-[100px] space-y-1 text-sm">
|
||||
<div id="geo-loader" class="loader"></div>
|
||||
<div class="hidden"> <!-- Hide data until loaded -->
|
||||
<p><strong>Country:</strong> <span id="country">-</span></p>
|
||||
<p><strong>Region:</strong> <span id="region">-</span></p>
|
||||
<p><strong>City:</strong> <span id="city">-</span></p>
|
||||
<p><strong>Postal Code:</strong> <span id="postal">-</span></p>
|
||||
<p><strong>Coordinates:</strong> <span id="coords">-</span></p>
|
||||
<p><strong>Timezone:</strong> <span id="timezone">-</span></p>
|
||||
<p id="geo-error" class="text-red-400"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1">ASN</h2>
|
||||
<div id="asn-info" class="min-h-[50px] space-y-1 text-sm">
|
||||
<div id="asn-loader" class="loader"></div>
|
||||
<div class="hidden"> <!-- Hide data until loaded -->
|
||||
<p><strong>Number:</strong> <span id="asn-number">-</span></p>
|
||||
<p><strong>Organization:</strong> <span id="asn-org">-</span></p>
|
||||
<p id="asn-error" class="text-red-400"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1">Reverse DNS (rDNS)</h2>
|
||||
<div id="rdns-info" class="min-h-[50px] space-y-1 text-sm">
|
||||
<div id="rdns-loader" class="loader"></div>
|
||||
<div class="hidden"> <!-- Hide data until loaded -->
|
||||
<ul id="rdns-list" class="list-disc list-inside"><li>-</li></ul>
|
||||
<p id="rdns-error" class="text-red-400"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechte Spalte: Eigene Karte -->
|
||||
<div class="space-y-4 p-4 bg-gray-700 rounded">
|
||||
<h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1">Your Location Map</h2>
|
||||
<div id="map-container" class="bg-gray-600 rounded min-h-[300px] flex items-center justify-center relative">
|
||||
<div id="map-loader" class="loader absolute"></div>
|
||||
<div id="map" class="w-full rounded hidden"></div>
|
||||
<p id="map-message" class="text-gray-400 hidden absolute">Could not load map.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bereich für IP Lookup -->
|
||||
<div class="mt-8 p-4 bg-gray-700 rounded">
|
||||
<h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1 mb-4">IP Address Lookup</h2>
|
||||
<div class="flex flex-col sm:flex-row gap-2 mb-4">
|
||||
<input type="text" id="lookup-ip-input" placeholder="Enter IP address (e.g., 8.8.8.8)"
|
||||
class="flex-grow px-3 py-2 bg-gray-800 border border-gray-600 rounded text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono">
|
||||
<button id="lookup-button"
|
||||
class="bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded transition duration-150 ease-in-out">
|
||||
Lookup IP
|
||||
</button>
|
||||
</div>
|
||||
<div id="lookup-error" class="text-red-400 mb-4 hidden"></div>
|
||||
|
||||
<!-- Ergebnisse des Lookups (initial versteckt) -->
|
||||
<div id="lookup-results-section" class="hidden grid grid-cols-1 md:grid-cols-2 gap-6 mt-4 border-t border-gray-600 pt-4">
|
||||
<!-- Linke Spalte: IP, Geo, ASN, rDNS -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-purple-300">Lookup Result for: <span id="lookup-ip-address" class="font-mono text-purple-400"></span></h3>
|
||||
<div id="lookup-result-loader" class="loader hidden"></div> <!-- Loader für den gesamten Block -->
|
||||
|
||||
<div id="lookup-geo-info" class="space-y-1 text-sm">
|
||||
<h4 class="font-semibold text-purple-300">Geolocation</h4>
|
||||
<p><strong>Country:</strong> <span id="lookup-country">-</span></p>
|
||||
<p><strong>Region:</strong> <span id="lookup-region">-</span></p>
|
||||
<p><strong>City:</strong> <span id="lookup-city">-</span></p>
|
||||
<p><strong>Postal Code:</strong> <span id="lookup-postal">-</span></p>
|
||||
<p><strong>Coordinates:</strong> <span id="lookup-coords">-</span></p>
|
||||
<p><strong>Timezone:</strong> <span id="lookup-timezone">-</span></p>
|
||||
<p id="lookup-geo-error" class="text-red-400"></p>
|
||||
</div>
|
||||
|
||||
<div id="lookup-asn-info" class="space-y-1 text-sm">
|
||||
<h4 class="font-semibold text-purple-300">ASN</h4>
|
||||
<p><strong>Number:</strong> <span id="lookup-asn-number">-</span></p>
|
||||
<p><strong>Organization:</strong> <span id="lookup-asn-org">-</span></p>
|
||||
<p id="lookup-asn-error" class="text-red-400"></p>
|
||||
</div>
|
||||
|
||||
<div id="lookup-rdns-info" class="space-y-1 text-sm">
|
||||
<h4 class="font-semibold text-purple-300">Reverse DNS (rDNS)</h4>
|
||||
<ul id="lookup-rdns-list" class="list-disc list-inside"><li>-</li></ul>
|
||||
<p id="lookup-rdns-error" class="text-red-400"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechte Spalte: Karte & Aktionen -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-semibold text-purple-300">Location Map</h4>
|
||||
<div id="lookup-map-container" class="bg-gray-600 rounded min-h-[250px] flex items-center justify-center relative">
|
||||
<div id="lookup-map-loader" class="loader hidden absolute"></div>
|
||||
<div id="lookup-map" class="w-full rounded hidden"></div> <!-- Höhe via CSS -->
|
||||
<p id="lookup-map-message" class="text-gray-400 hidden absolute">Could not load map.</p>
|
||||
</div>
|
||||
<!-- Optional: Buttons für Ping/Trace für diese IP -->
|
||||
<div class="mt-4 space-x-2">
|
||||
<button id="lookup-ping-button" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-1 px-3 rounded text-sm transition duration-150 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed" disabled>Ping this IP</button>
|
||||
<button id="lookup-trace-button" class="bg-teal-600 hover:bg-teal-700 text-white font-bold py-1 px-3 rounded text-sm transition duration-150 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed" disabled>Trace this IP</button>
|
||||
</div>
|
||||
<!-- Bereich für Ping-Ergebnisse (Lookup) -->
|
||||
<div id="lookup-ping-results" class="mt-2 text-sm hidden">
|
||||
<h4 class="font-semibold text-purple-300">Ping Results</h4>
|
||||
<div id="lookup-ping-loader" class="loader hidden"></div>
|
||||
<pre id="lookup-ping-output" class="mt-1 whitespace-pre-wrap break-all font-mono bg-gray-900 text-gray-300 p-2 rounded text-xs"></pre>
|
||||
<p id="lookup-ping-error" class="text-red-400"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bereich für Traceroute -->
|
||||
<div id="traceroute-section" class="mt-8 p-4 bg-gray-700 rounded hidden">
|
||||
<h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1 mb-4">Traceroute Results</h2>
|
||||
<div id="traceroute-status" class="flex items-center mb-2">
|
||||
<div id="traceroute-loader" class="loader mr-2 hidden"></div>
|
||||
<span id="traceroute-message" class="text-gray-400"></span>
|
||||
</div>
|
||||
<div id="traceroute-output"><pre></pre></div>
|
||||
</div>
|
||||
|
||||
<!-- Globaler Fehlerbereich -->
|
||||
<div id="global-error" class="mt-6 p-4 bg-red-800 text-red-100 rounded hidden"></div>
|
||||
|
||||
<!-- Footer für Version -->
|
||||
<footer class="mt-8 pt-4 border-t border-gray-600 text-center text-xs text-gray-500">
|
||||
<p>© 2025 <a href="https://johanneskr.de" class="text-purple-400 hover:underline">Johannes Krüger</a></p>
|
||||
<p>Version: <span id="commit-sha" class="font-mono">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>
|
||||
@@ -0,0 +1,736 @@
|
||||
// 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 (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
|
||||
|
||||
// --- 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}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).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
|
||||
|
||||
updateField(asnNumberEl, data.asn?.number, null, asnErrorEl);
|
||||
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 ---
|
||||
|
||||
/** 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 = '';
|
||||
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;
|
||||
currentLookupIp = null;
|
||||
|
||||
// Remove lookup map instance if it exists
|
||||
if (window['lookup-map_instance']) {
|
||||
window['lookup-map_instance'].remove();
|
||||
window['lookup-map_instance'] = 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);
|
||||
|
||||
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;
|
||||
|
||||
} 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.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');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// --- 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-lookup.html?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();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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);
|
||||
|
||||
// Der Event Listener für den IP-Link wird jetzt in fetchIpInfo() hinzugefügt,
|
||||
// nachdem die IP erfolgreich abgerufen wurde.
|
||||
|
||||
}); // End DOMContentLoaded
|
||||
@@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SSL Certificate Check - uTools</title>
|
||||
<!-- Tailwind CSS Play CDN -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Font Awesome (für Icons) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<!-- Eigene Styles (ähnlich wie index.html) -->
|
||||
<style>
|
||||
/* Einfacher Lade-Spinner (Tailwind animiert) */
|
||||
.loader {
|
||||
border: 4px solid rgba(168, 85, 247, 0.3); /* Lila transparent */
|
||||
border-left-color: #a855f7; /* Lila */
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
/* Navigations-Styling (aus index.html übernommen) */
|
||||
nav ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 1rem; }
|
||||
nav a { color: #c4b5fd; /* purple-300 */ text-decoration: none; white-space: nowrap; }
|
||||
nav a:hover { color: #a78bfa; /* purple-400 */ text-decoration: underline; }
|
||||
header { background-color: #374151; /* gray-700 */ padding: 1rem; margin-bottom: 1.5rem; border-radius: 0.5rem; /* rounded-lg */ display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
|
||||
@media (min-width: 768px) { /* md breakpoint */
|
||||
header { flex-direction: row; justify-content: space-between; }
|
||||
}
|
||||
header h1 { font-size: 1.5rem; /* text-2xl */ font-weight: bold; color: #e5e7eb; /* gray-200 */ }
|
||||
/* Ergebnis-Box */
|
||||
.result-box pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
background-color: #1f2937; /* Dunkelgrau */
|
||||
color: #d1d5db; /* Hellgrau */
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem; /* rounded-md */
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
}
|
||||
/* Score Bar */
|
||||
.score-bar { height: 20px; background-color: #4b5563; /* gray-600 */ border-radius: 0.25rem; overflow: hidden; }
|
||||
.score-bar-inner { height: 100%; background-color: #ef4444; /* red-500 */ transition: width 0.5s ease-in-out, background-color 0.5s ease-in-out; }
|
||||
/* Hilfsklasse zum Verstecken */
|
||||
.hidden { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-900 text-gray-200 font-sans p-4 md:p-8">
|
||||
|
||||
<header>
|
||||
<h1><a href="index.html" class="hover:text-purple-300"><i class="fas fa-network-wired mr-2"></i>uTools Network Suite</a></h1>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="index.html">IP Info & Tools</a></li>
|
||||
<li><a href="subnet-calculator.html">Subnetz Rechner</a></li>
|
||||
<li><a href="dns-lookup.html">DNS Lookup</a></li>
|
||||
<li><a href="whois-lookup.html">WHOIS Lookup</a></li>
|
||||
<li><a href="ssl-check.html" class="text-purple-400 font-bold">SSL Check</a></li> <!-- Aktive Seite hervorheben -->
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container mx-auto max-w-4xl bg-gray-800 rounded-lg shadow-xl p-6">
|
||||
<h1 class="text-3xl font-bold mb-6 text-purple-400 text-center"><i class="fas fa-shield-alt mr-2"></i>SSL Certificate Check</h1>
|
||||
<p class="text-center text-gray-400 mb-6">Enter a domain name to check its SSL/TLS certificate details and validity.</p>
|
||||
|
||||
<form id="ssl-check-form" class="mb-6">
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<input type="text" id="domain-input" placeholder="e.g., google.com" required
|
||||
class="flex-grow px-3 py-2 bg-gray-700 border border-gray-600 rounded text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono">
|
||||
<button type="submit" id="submit-button"
|
||||
class="bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded transition duration-150 ease-in-out flex items-center justify-center">
|
||||
<span id="button-text">Check Certificate</span>
|
||||
<div id="loading-spinner" class="loader ml-2 hidden"></div>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Ergebnisbereich -->
|
||||
<div id="result" class="result-box bg-gray-700 p-4 rounded border border-gray-600 hidden">
|
||||
<h2 class="text-xl font-semibold text-purple-300 mb-4">Result for <span id="result-domain" class="font-bold font-mono"></span></h2>
|
||||
|
||||
<!-- Fehleranzeige -->
|
||||
<div id="error-message" class="bg-red-800 text-red-100 p-3 rounded mb-4 hidden"></div>
|
||||
|
||||
<!-- Auswertung (nur bei Erfolg) -->
|
||||
<div id="evaluation" class="mb-4 hidden">
|
||||
<h4 class="text-lg font-semibold text-purple-300 mb-2">Evaluation</h4>
|
||||
<div class="score-bar mb-2">
|
||||
<div id="score-bar-inner" class="score-bar-inner"></div>
|
||||
</div>
|
||||
<p class="text-sm">Score: <span id="score-value" class="font-bold"></span>/10</p>
|
||||
<p class="text-sm font-semibold mt-1" id="evaluation-summary"></p>
|
||||
</div>
|
||||
|
||||
<!-- Zertifikatsdetails (nur bei Erfolg) -->
|
||||
<div id="certificate-details" class="hidden">
|
||||
<h4 class="text-lg font-semibold text-purple-300 mb-2">Certificate Details</h4>
|
||||
<pre id="cert-output"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer (aus index.html übernommen) -->
|
||||
<footer class="mt-8 pt-4 border-t border-gray-600 text-center text-xs text-gray-500">
|
||||
<p>© 2025 <a href="https://johanneskr.de" class="text-purple-400 hover:underline">Johannes Krüger</a></p>
|
||||
<p>Version: <span id="commit-sha" class="font-mono">loading...</span></p> <!-- ID beibehalten für script.js -->
|
||||
</footer>
|
||||
|
||||
<!-- Eigene JS-Logik -->
|
||||
<script src="/app/ssl-check.js"></script>
|
||||
<!-- Gemeinsames Skript für Version etc. (falls benötigt, sonst entfernen) -->
|
||||
<script>
|
||||
// Minimales Skript, um die Version zu laden (aus index.html's script.js extrahiert)
|
||||
async function fetchVersion() {
|
||||
try {
|
||||
const response = await fetch('/api/version');
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const data = await response.json();
|
||||
const commitShaSpan = document.getElementById('commit-sha');
|
||||
if (commitShaSpan && data.commitSha) {
|
||||
commitShaSpan.textContent = data.commitSha.substring(0, 7); // Kurze SHA
|
||||
} else if (commitShaSpan) {
|
||||
commitShaSpan.textContent = 'N/A';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching version:', error);
|
||||
const commitShaSpan = document.getElementById('commit-sha');
|
||||
if (commitShaSpan) commitShaSpan.textContent = 'Error';
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', fetchVersion);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,117 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const form = document.getElementById('ssl-check-form');
|
||||
const domainInput = document.getElementById('domain-input');
|
||||
const resultDiv = document.getElementById('result');
|
||||
const resultDomainSpan = document.getElementById('result-domain');
|
||||
const evaluationDiv = document.getElementById('evaluation');
|
||||
const scoreValueSpan = document.getElementById('score-value');
|
||||
const scoreBarInner = document.getElementById('score-bar-inner');
|
||||
const evaluationSummaryP = document.getElementById('evaluation-summary');
|
||||
const certificateDetailsDiv = document.getElementById('certificate-details');
|
||||
const certOutputPre = document.getElementById('cert-output');
|
||||
const errorMessageDiv = document.getElementById('error-message');
|
||||
const loadingSpinner = document.getElementById('loading-spinner'); // Geändert
|
||||
const submitButton = document.getElementById('submit-button');
|
||||
const buttonTextSpan = document.getElementById('button-text'); // Geändert
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const domain = domainInput.value.trim();
|
||||
|
||||
if (!domain) {
|
||||
showError('Please enter a domain name.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset UI
|
||||
hideError();
|
||||
resultDiv.classList.add('hidden');
|
||||
evaluationDiv.classList.add('hidden');
|
||||
certificateDetailsDiv.classList.add('hidden');
|
||||
loadingSpinner.classList.remove('hidden'); // Spinner anzeigen
|
||||
submitButton.disabled = true;
|
||||
buttonTextSpan.textContent = 'Checking...'; // Text im Button ändern
|
||||
|
||||
|
||||
try {
|
||||
// Verwende /api/ Relative Pfad, da Nginx als Proxy dient
|
||||
const apiUrl = `/api/ssl-check?domain=${encodeURIComponent(domain)}`;
|
||||
console.log(`Fetching: ${apiUrl}`); // Debugging
|
||||
const response = await fetch(apiUrl);
|
||||
const data = await response.json();
|
||||
console.log("API Response:", data); // Debugging
|
||||
|
||||
resultDiv.classList.remove('hidden'); // Ergebnisbereich anzeigen
|
||||
resultDomainSpan.textContent = domain;
|
||||
|
||||
if (!response.ok || data.error) {
|
||||
// API-Fehler oder Fehler in der JSON-Antwort behandeln
|
||||
const errorMsg = data.error || `HTTP error! Status: ${response.status}`;
|
||||
const errorDetails = data.details ? ` Details: ${data.details}` : (data.raw_output ? ` Raw Output: ${data.raw_output}` : '');
|
||||
console.error("API Error:", errorMsg, errorDetails); // Debugging
|
||||
showError(`${errorMsg}${errorDetails}`);
|
||||
evaluationDiv.classList.add('hidden'); // Auswertung ausblenden bei Fehler
|
||||
certificateDetailsDiv.classList.add('hidden'); // Details ausblenden bei Fehler
|
||||
} else if (!data.certificate || !data.evaluation) {
|
||||
// Unerwartete, aber erfolgreiche Antwort
|
||||
console.error("Unexpected API response structure:", data); // Debugging
|
||||
showError("Received an unexpected response from the server.");
|
||||
evaluationDiv.classList.add('hidden');
|
||||
certificateDetailsDiv.classList.add('hidden');
|
||||
}
|
||||
else {
|
||||
// Erfolgreiches Ergebnis anzeigen
|
||||
evaluationDiv.classList.remove('hidden');
|
||||
certificateDetailsDiv.classList.remove('hidden');
|
||||
|
||||
// Auswertung
|
||||
scoreValueSpan.textContent = data.evaluation.score;
|
||||
evaluationSummaryP.textContent = data.evaluation.summary;
|
||||
updateScoreBar(data.evaluation.score);
|
||||
|
||||
// Zertifikatsdetails formatieren
|
||||
let formattedDetails = `Issuer: ${data.certificate.issuer || 'N/A'}\n`;
|
||||
formattedDetails += `Subject: ${data.certificate.subject || 'N/A'}\n`;
|
||||
formattedDetails += `Valid From: ${data.certificate.validFrom ? new Date(data.certificate.validFrom).toLocaleString() : 'N/A'}\n`;
|
||||
formattedDetails += `Valid To: ${data.certificate.validTo ? new Date(data.certificate.validTo).toLocaleString() : 'N/A'}\n`;
|
||||
formattedDetails += `Validity Status: ${data.certificate.validity || 'N/A'}\n\n`;
|
||||
formattedDetails += `--- Raw OpenSSL Output ---\n${data.certificate.details || 'N/A'}`;
|
||||
certOutputPre.textContent = formattedDetails;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch or processing error:', error); // Debugging
|
||||
showError(`An error occurred: ${error.message}. Check the browser console for more details.`);
|
||||
evaluationDiv.classList.add('hidden');
|
||||
certificateDetailsDiv.classList.add('hidden');
|
||||
} finally {
|
||||
loadingSpinner.classList.add('hidden'); // Spinner ausblenden
|
||||
submitButton.disabled = false;
|
||||
buttonTextSpan.textContent = 'Check Certificate'; // Button-Text zurücksetzen
|
||||
}
|
||||
});
|
||||
|
||||
function showError(message) {
|
||||
errorMessageDiv.textContent = message;
|
||||
errorMessageDiv.classList.remove('hidden');
|
||||
resultDiv.classList.remove('hidden'); // Sicherstellen, dass der Ergebnisbereich sichtbar ist, um den Fehler anzuzeigen
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
errorMessageDiv.classList.add('hidden');
|
||||
errorMessageDiv.textContent = '';
|
||||
}
|
||||
|
||||
function updateScoreBar(score) {
|
||||
const percentage = Math.max(0, Math.min(100, score * 10)); // Sicherstellen, dass der Wert zwischen 0 und 100 liegt
|
||||
scoreBarInner.style.width = `${percentage}%`;
|
||||
|
||||
// Farbwechsel basierend auf dem Score
|
||||
if (score >= 8) {
|
||||
scoreBarInner.style.backgroundColor = '#22c55e'; // green-500
|
||||
} else if (score >= 5) {
|
||||
scoreBarInner.style.backgroundColor = '#facc15'; // yellow-400
|
||||
} else {
|
||||
scoreBarInner.style.backgroundColor = '#ef4444'; // red-500
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,259 @@
|
||||
<!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>
|
||||
/* Navigations-Styling */
|
||||
nav ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 1rem; } /* flex-wrap hinzugefügt */
|
||||
nav a { color: #c4b5fd; /* purple-300 */ text-decoration: none; white-space: nowrap; } /* nowrap hinzugefügt */
|
||||
nav a:hover { color: #a78bfa; /* purple-400 */ text-decoration: underline; }
|
||||
header { background-color: #374151; /* gray-700 */ padding: 1rem; margin-bottom: 1.5rem; border-radius: 0.5rem; /* rounded-lg */ display: flex; flex-direction: column; align-items: center; gap: 0.5rem; } /* Flex direction geändert */
|
||||
@media (min-width: 768px) { /* md breakpoint */
|
||||
header { flex-direction: row; justify-content: space-between; }
|
||||
}
|
||||
header h1 { font-size: 1.5rem; /* text-2xl */ font-weight: bold; color: #e5e7eb; /* gray-200 */ }
|
||||
|
||||
/* Styling für Formular und Ergebnisse */
|
||||
label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: #d1d5db; /* gray-300 */ }
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
background-color: #4b5563; /* gray-600 */
|
||||
border: 1px solid #6b7280; /* gray-500 */
|
||||
border-radius: 0.375rem; /* rounded-md */
|
||||
color: #e5e7eb; /* gray-200 */
|
||||
font-family: monospace;
|
||||
}
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #a78bfa; /* purple-400 */
|
||||
box-shadow: 0 0 0 2px rgba(167, 139, 250, 0.5);
|
||||
}
|
||||
button[type="submit"] {
|
||||
background-color: #8b5cf6; /* purple-500 */
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem; /* rounded-md */
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
button[type="submit"]:hover {
|
||||
background-color: #7c3aed; /* purple-600 */
|
||||
}
|
||||
#results, #examples {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background-color: #374151; /* gray-700 */
|
||||
border-radius: 0.5rem; /* rounded-lg */
|
||||
}
|
||||
#results h3, #examples h3 {
|
||||
font-size: 1.25rem; /* text-xl */
|
||||
font-weight: 600;
|
||||
color: #c4b5fd; /* purple-300 */
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid #6b7280; /* gray-500 */
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
#results p {
|
||||
margin-bottom: 0.75rem;
|
||||
color: #d1d5db; /* gray-300 */
|
||||
}
|
||||
#results p strong {
|
||||
color: #e5e7eb; /* gray-200 */
|
||||
min-width: 150px; /* Für bessere Ausrichtung */
|
||||
display: inline-block;
|
||||
}
|
||||
#results span {
|
||||
font-family: monospace;
|
||||
color: #a78bfa; /* purple-400 */
|
||||
}
|
||||
/* Styling für Beispiel-Tabelle */
|
||||
#examples table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#examples th, #examples td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #4b5563; /* gray-600 */
|
||||
color: #d1d5db; /* gray-300 */
|
||||
}
|
||||
#examples th {
|
||||
color: #e5e7eb; /* gray-200 */
|
||||
font-weight: 600;
|
||||
}
|
||||
#examples td code {
|
||||
font-family: monospace;
|
||||
background-color: #4b5563; /* gray-600 */
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
color: #c4b5fd; /* purple-300 */
|
||||
}
|
||||
#examples .example-link {
|
||||
color: #a78bfa; /* purple-400 */
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
#examples .example-link:hover {
|
||||
color: #c4b5fd; /* purple-300 */
|
||||
}
|
||||
.hidden { display: none; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-900 text-gray-200 font-sans p-4 md:p-8">
|
||||
|
||||
<header>
|
||||
<h1>uTools Network Suite</h1> <!-- Titel angepasst -->
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="index.html">IP Info & Tools</a></li> <!-- Angepasst -->
|
||||
<li><a href="subnet-calculator.html">Subnetz Rechner</a></li>
|
||||
<li><a href="dns-lookup.html">DNS Lookup</a></li> <!-- Neu -->
|
||||
<li><a href="whois-lookup.html">WHOIS Lookup</a></li> <!-- Neu -->
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container mx-auto max-w-4xl bg-gray-800 rounded-lg shadow-xl p-6">
|
||||
|
||||
<h2 class="text-2xl font-bold mb-6 text-purple-400 text-center">IP Subnetz Rechner</h2>
|
||||
|
||||
<form id="subnet-form" class="mb-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label for="ip-address">IP Adresse:</label>
|
||||
<input type="text" id="ip-address" name="ip-address" placeholder="z.B. 192.168.1.1" required
|
||||
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cidr">CIDR / Subnetzmaske:</label>
|
||||
<input type="text" id="cidr" name="cidr" placeholder="z.B. 24 oder 255.255.255.0" required
|
||||
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded transition duration-150 ease-in-out">
|
||||
Berechnen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="results" class="bg-gray-700 rounded p-6 hidden"> <!-- Ergebnisse initial verstecken -->
|
||||
<h3 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-2 mb-4">Ergebnisse:</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<p><strong>Netzwerkadresse:</strong> <span id="network-address" class="font-mono text-purple-400">-</span></p>
|
||||
<p><strong>Broadcast-Adresse:</strong> <span id="broadcast-address" class="font-mono text-purple-400">-</span></p>
|
||||
<p><strong>Subnetzmaske:</strong> <span id="subnet-mask" class="font-mono text-purple-400">-</span></p>
|
||||
<p><strong>Anzahl der Hosts:</strong> <span id="host-count" class="font-mono text-purple-400">-</span></p>
|
||||
<p><strong>Erste Host-Adresse:</strong> <span id="first-host" class="font-mono text-purple-400">-</span></p>
|
||||
<p><strong>Letzte Host-Adresse:</strong> <span id="last-host" class="font-mono text-purple-400">-</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Beispiel-Subnetze -->
|
||||
<div id="examples" class="bg-gray-700 rounded p-6 mt-8">
|
||||
<h3 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-2 mb-4">Beispiel-Subnetze (Private Adressbereiche)</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bereich</th>
|
||||
<th>CIDR</th>
|
||||
<th>Subnetzmaske</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-600">
|
||||
<tr>
|
||||
<td><code>192.168.0.0 - 192.168.255.255</code></td>
|
||||
<td><code>/16</code> (Gesamt)</td>
|
||||
<td><code>255.255.0.0</code></td>
|
||||
<td>Klasse C (oft als /24 genutzt)</td>
|
||||
<td><span class="example-link" data-ip="192.168.1.1" data-cidr="24">Beispiel /24</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>172.16.0.0 - 172.31.255.255</code></td>
|
||||
<td><code>/12</code> (Gesamt)</td>
|
||||
<td><code>255.240.0.0</code></td>
|
||||
<td>Klasse B</td>
|
||||
<td><span class="example-link" data-ip="172.16.10.5" data-cidr="16">Beispiel /16</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>10.0.0.0 - 10.255.255.255</code></td>
|
||||
<td><code>/8</code> (Gesamt)</td>
|
||||
<td><code>255.0.0.0</code></td>
|
||||
<td>Klasse A</td>
|
||||
<td><span class="example-link" 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-400">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-800 text-red-100 rounded hidden"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="mt-8 pt-4 border-t border-gray-600 text-center text-xs text-gray-500">
|
||||
<p>© 2025 <a href="https://johanneskr.de" class="text-purple-400 hover:underline">Johannes Krüger</a></p>
|
||||
<p>Version: <span id="commit-sha" class="font-mono">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>
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<!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 */
|
||||
.loader {
|
||||
border: 4px solid rgba(168, 85, 247, 0.3); /* Lila transparent */
|
||||
border-left-color: #a855f7; /* Lila */
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
/* Ergebnis-Pre-Formatierung */
|
||||
.result-pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
background-color: #1f2937; /* Dunkelgrau */
|
||||
color: #d1d5db; /* Hellgrau */
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem; /* rounded-md */
|
||||
max-height: 600px; /* Mehr Höhe für WHOIS */
|
||||
overflow-y: auto;
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
}
|
||||
/* Navigations-Styling */
|
||||
nav ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 1rem; }
|
||||
nav a { color: #c4b5fd; text-decoration: none; white-space: nowrap; }
|
||||
nav a:hover { color: #a78bfa; text-decoration: underline; }
|
||||
header { background-color: #374151; padding: 1rem; margin-bottom: 1.5rem; border-radius: 0.5rem; display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
|
||||
@media (min-width: 768px) { header { flex-direction: row; justify-content: space-between; } }
|
||||
header h1 { font-size: 1.5rem; font-weight: bold; color: #e5e7eb; }
|
||||
.hidden { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-900 text-gray-200 font-sans p-4 md:p-8">
|
||||
|
||||
<header>
|
||||
<h1>uTools Network Suite</h1>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="index.html">IP Info & Tools</a></li>
|
||||
<li><a href="subnet-calculator.html">Subnetz Rechner</a></li>
|
||||
<li><a href="dns-lookup.html">DNS Lookup</a></li>
|
||||
<li><a href="whois-lookup.html">WHOIS Lookup</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container mx-auto max-w-4xl bg-gray-800 rounded-lg shadow-xl p-6">
|
||||
|
||||
<h1 class="text-3xl font-bold mb-6 text-purple-400 text-center">WHOIS Lookup</h1>
|
||||
|
||||
<!-- Bereich für WHOIS Lookup -->
|
||||
<div class="mt-8 p-4 bg-gray-700 rounded">
|
||||
<div class="flex flex-col sm:flex-row gap-2 mb-4">
|
||||
<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-3 py-2 bg-gray-800 border border-gray-600 rounded text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono">
|
||||
<button id="whois-lookup-button"
|
||||
class="bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded transition duration-150 ease-in-out">
|
||||
Lookup WHOIS
|
||||
</button>
|
||||
</div>
|
||||
<div id="whois-lookup-error" class="text-red-400 mb-4 hidden"></div>
|
||||
<div id="whois-lookup-results-section" class="hidden mt-4 border-t border-gray-600 pt-4">
|
||||
<h3 class="text-lg font-semibold text-purple-300 mb-2">WHOIS Results for: <span id="whois-lookup-query" class="font-mono text-purple-400"></span></h3>
|
||||
<div id="whois-lookup-loader" class="loader hidden mb-2"></div>
|
||||
<pre id="whois-lookup-output" class="result-pre"></pre> <!-- Ergebnisbereich -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Globaler Fehlerbereich -->
|
||||
<div id="global-error" class="mt-6 p-4 bg-red-800 text-red-100 rounded hidden"></div>
|
||||
|
||||
<!-- Footer für Version -->
|
||||
<footer class="mt-8 pt-4 border-t border-gray-600 text-center text-xs text-gray-500">
|
||||
<p>© 2025 <a href="https://johanneskr.de" class="text-purple-400 hover:underline">Johannes Krüger</a></p>
|
||||
<p>Version: <span id="commit-sha" class="font-mono">loading...</span></p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Eigene JS-Logik für diese Seite -->
|
||||
<script src="whois-lookup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,146 @@
|
||||
// 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) {
|
||||
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
|
||||
@@ -0,0 +1,42 @@
|
||||
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;
|
||||
|
||||
# Statische Dateien direkt ausliefern
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html; # Wichtig für Single-Page-Apps (auch wenn wir keine sind)
|
||||
}
|
||||
|
||||
# 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;
|
||||
@@ -0,0 +1,42 @@
|
||||
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;
|
||||
|
||||
# Statische Dateien direkt ausliefern
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html; # Wichtig für Single-Page-Apps (auch wenn wir keine sind)
|
||||
}
|
||||
|
||||
# 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-dev: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;
|
||||
@@ -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 © 2022</p>
|
||||
</form>');
|
||||
Vendored
-7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
}
|
||||
@@ -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">
|
||||
© 2021
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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">
|
||||
© 2021
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
";
|
||||
}
|
||||
|
||||
?>
|
||||
Reference in New Issue
Block a user