8 Commits

Author SHA1 Message Date
MrUnknownDE c698aa9692 change design on ssl_checker 2025-03-29 19:41:37 +01:00
MrUnknownDE ea15c7e5b6 start with ssl_check 2025-03-29 19:37:27 +01:00
MrUnknownDE eaf1c639d6 fix nginx issue in dev env 2025-03-29 19:28:58 +01:00
MrUnknownDE fe70962fe8 bind http port local 2025-03-29 19:26:35 +01:00
MrUnknownDE e71fd45b66 change port on dev-compose 2025-03-29 19:26:15 +01:00
MrUnknownDE 538ec0ca86 bladt 2025-03-29 19:24:01 +01:00
MrUnknownDE a985f591c4 fix dev-compose-file 2025-03-29 19:23:42 +01:00
MrUnknownDE 6dd2324624 push dev-env 2025-03-29 19:22:58 +01:00
42 changed files with 2010 additions and 4244 deletions
-1
View File
@@ -1 +0,0 @@
backend/data/*.mmdb filter=lfs diff=lfs merge=lfs -text
+76 -68
View File
@@ -1,89 +1,97 @@
name: Docker Build and Push (Docker Hub, Multi-Arch) name: Build and Push Docker Images
# Trigger: Wann soll der Workflow laufen?
on: on:
push: workflow_dispatch: # Ermöglicht manuelles Starten über die GitHub UI
branches:
- main
workflow_dispatch:
inputs:
extra_tag:
description: "Optionaler Zusatz-Tag (z.B. v1.2.3). Kommt zusätzlich zu :latest und :<sha>."
required: false
default: ""
jobs: jobs:
build-and-push: build-and-push:
runs-on: ubuntu-latest runs-on: ubuntu-latest # Verwendet den neuesten Ubuntu-Runner von GitHub
env:
REGISTRY: docker.io # Berechtigungen für den GITHUB_TOKEN, um nach GHCR pushen zu können
DOCKERHUB_USER_LC: ${{ secrets.DOCKERHUB_USERNAME }}
permissions: permissions:
contents: read contents: read # Zum Auschecken des Codes
packages: write # Zum Pushen nach GitHub Packages (GHCR)
steps: steps:
- name: Checkout # 1. Code auschecken
- name: Checkout repository
uses: actions/checkout@v4 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: with:
fetch-depth: 0 images: |
lfs: true # WICHTIG: Lädt die echten LFS-Dateien (MaxMind DBs) herunter ghcr.io/${{ github.repository_owner }}/utools-backend
ghcr.io/${{ github.repository_owner }}/utools-frontend
- name: Get short SHA # Tags generieren:
id: vars # - Typ 'sha' -> Kurzer Commit-Hash (z.B. sha-a1b2c3d)
run: echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT # - 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)
- name: Login to Docker Hub tags: |
uses: docker/login-action@v3 type=sha,prefix=sha-
with: type=ref,event=branch
registry: docker.io type=ref,event=tag
username: ${{ secrets.DOCKERHUB_USERNAME }} type=ref,event=pr # Nur für PR-Events
password: ${{ secrets.DOCKERHUB_TOKEN }}
# 3. QEMU für Multi-Plattform Builds (optional, aber gute Praxis)
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Buildx # 4. Docker Buildx einrichten (verbesserter Builder)
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
# -------- BACKEND -------- # 5. Login bei GitHub Container Registry (GHCR)
- name: Build & Push backend (multi-arch) # Verwendet den automatisch generierten GITHUB_TOKEN
uses: docker/build-push-action@v6 - name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with: with:
context: ./backend registry: ghcr.io
file: ./backend/Dockerfile username: ${{ github.actor }} # Benutzer oder Organisation, dem das Repo gehört
push: true password: ${{ secrets.GHCR_PUSH_TOKEN }}
platforms: linux/amd64,linux/arm64
tags: |
${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-backend:latest
${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-backend:${{ steps.vars.outputs.sha }}
build-args: |
GIT_COMMIT_SHA=${{ steps.vars.outputs.sha }}
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
- name: Tag backend with extra_tag (manifest retag) # 6. Backend Image bauen und pushen
if: ${{ github.event.inputs.extra_tag && github.event.inputs.extra_tag != '' }} - name: Build and push Backend image
run: | id: build-backend
docker buildx imagetools create \ uses: docker/build-push-action@v5
-t ${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-backend:${{ github.event.inputs.extra_tag }} \
${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-backend:${{ steps.vars.outputs.sha }}
# -------- FRONTEND --------
- name: Build & Push frontend (multi-arch)
uses: docker/build-push-action@v6
with: with:
context: ./frontend context: ./backend # Pfad zum Backend-Dockerfile
file: ./frontend/Dockerfile # Nur pushen, wenn es ein Push zum main-Branch oder ein Git-Tag ist
push: true push: ${{ github.event_name == 'push' || github.event_name == 'create' && startsWith(github.ref, 'refs/tags/') }}
platforms: linux/amd64,linux/arm64 # Tags und Labels aus dem Metadaten-Schritt verwenden (gefiltert für Backend)
tags: | tags: ${{ steps.meta.outputs.tags }}
${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-frontend:latest labels: ${{ steps.meta.outputs.labels }}
${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-frontend:${{ steps.vars.outputs.sha }} # Filter für das spezifische Backend-Image (Index 0 in der 'images'-Liste oben)
build-args: | # Wichtig: Passe den Index an, falls du die Reihenfolge änderst!
GIT_COMMIT_SHA=${{ steps.vars.outputs.sha }} image-name-index: 0 # Index des Backend-Images in der 'images'-Liste
SENTRY_DSN=${{ secrets.SENTRY_DSN }} cache-from: type=gha # GitHub Actions Cache verwenden (Lesen)
cache-to: type=gha,mode=max # GitHub Actions Cache verwenden (Schreiben)
- name: Tag frontend with extra_tag (manifest retag) # 7. Frontend Image bauen und pushen
if: ${{ github.event.inputs.extra_tag && github.event.inputs.extra_tag != '' }} - 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: | run: |
docker buildx imagetools create \ echo "Backend Image Tags: ${{ steps.meta.outputs.tags }}"
-t ${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-frontend:${{ github.event.inputs.extra_tag }} \ echo "Frontend Image Tags: ${{ steps.meta.outputs.tags }}"
${{ env.REGISTRY }}/${{ env.DOCKERHUB_USER_LC }}/utools-frontend:${{ steps.vars.outputs.sha }} echo "Backend Image Digest: ${{ steps.build-backend.outputs.digest }}"
echo "Frontend Image Digest: ${{ steps.build-frontend.outputs.digest }}"
+34 -27
View File
@@ -3,7 +3,8 @@ name: Update MaxMind GeoLite2 DBs
on: on:
workflow_dispatch: # Ermöglicht manuelles Starten workflow_dispatch: # Ermöglicht manuelles Starten
schedule: schedule:
- cron: '0 0 1 * *' # Läuft jeden Dienstag um 05:00 UTC (anpassbar)
- cron: '0 5 * * 2'
jobs: jobs:
update-db: update-db:
@@ -15,13 +16,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
lfs: true # Wichtig: LFS-Dateien beim Checkout herunterladen
- name: Download geoipupdate tool - name: Download geoipupdate tool
run: | run: |
# Lade eine spezifische Version oder die neueste herunter # Lade eine spezifische Version oder die neueste herunter
GEOIPUPDATE_VERSION="4.11.1" 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" 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" tar -zxvf "geoipupdate_${GEOIPUPDATE_VERSION}_linux_amd64.tar.gz"
# Verschiebe das Binary in einen bekannten Pfad und mache es ausführbar # Verschiebe das Binary in einen bekannten Pfad und mache es ausführbar
@@ -32,10 +31,12 @@ jobs:
- name: Create GeoIP.conf - name: Create GeoIP.conf
# Erstellt die Konfigurationsdatei für geoipupdate mit den Secrets # Erstellt die Konfigurationsdatei für geoipupdate mit den Secrets
# Wichtig: Secrets nicht direkt im Log ausgeben!
run: | run: |
echo "Creating GeoIP.conf..." echo "Creating GeoIP.conf..."
cat << EOF > GeoIP.conf cat << EOF > GeoIP.conf
# GeoIP.conf file for geoipupdate # GeoIP.conf file for geoipupdate
# Replace with your actual AccountID and LicenseKey from GitHub Secrets
AccountID ${{ secrets.MAXMIND_ACCOUNT_ID }} AccountID ${{ secrets.MAXMIND_ACCOUNT_ID }}
LicenseKey ${{ secrets.MAXMIND_LICENSE_KEY }} LicenseKey ${{ secrets.MAXMIND_LICENSE_KEY }}
@@ -43,6 +44,7 @@ jobs:
EditionIDs GeoLite2-ASN GeoLite2-City EditionIDs GeoLite2-ASN GeoLite2-City
EOF EOF
echo "GeoIP.conf created." echo "GeoIP.conf created."
# Umgebungsvariablen für Sicherheit (werden nicht geloggt)
env: env:
MAXMIND_ACCOUNT_ID: ${{ secrets.MAXMIND_ACCOUNT_ID }} MAXMIND_ACCOUNT_ID: ${{ secrets.MAXMIND_ACCOUNT_ID }}
MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }} MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }}
@@ -50,37 +52,42 @@ jobs:
- name: Run geoipupdate - name: Run geoipupdate
run: | run: |
echo "Running geoipupdate..." echo "Running geoipupdate..."
# Lädt die Datenbanken nach ./backend/data herunter # -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 geoipupdate -f GeoIP.conf -d ./backend/data -v
echo "geoipupdate finished." echo "geoipupdate finished."
- name: Configure Git and LFS - name: Check for changes
id: check_changes
run: | 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.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com' git config --global user.email 'github-actions[bot]@users.noreply.github.com'
# Installiere Git LFS Hooks für diesen Run
git lfs install
- name: Track and Commit changes (LFS)
run: |
# Sage Git, dass .mmdb Dateien mit LFS verwaltet werden sollen
git lfs track "backend/data/*.mmdb"
# Füge .gitattributes (hier steht die LFS-Konfiguration drin) hinzu
git add .gitattributes
# Füge die eigentlichen Datenbank-Dateien hinzu
git add ./backend/data/*.mmdb git add ./backend/data/*.mmdb
# Erstelle Commit-Nachricht mit Datum
# Prüfe, ob Änderungen zum Committen vorliegen (staged changes)
if git diff --staged --quiet; then
echo "No changes detected in MaxMind databases."
else
echo "Changes detected. Committing..."
COMMIT_DATE=$(date -u +"%Y-%m-%d") COMMIT_DATE=$(date -u +"%Y-%m-%d")
git commit -m "Update MaxMind GeoLite2 databases (LFS) (${COMMIT_DATE})" git commit -m "Update MaxMind GeoLite2 databases (${COMMIT_DATE})"
# Pushe zum aktuellen Branch (z.B. main)
git push git push
echo "Changes pushed via LFS." echo "Changes pushed."
fi # Umgebungsvariable für den Token (wird automatisch von GitHub bereitgestellt)
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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."
-69
View File
@@ -1,69 +0,0 @@
# Code of Conduct - uTools
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behaviour that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologising to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behaviour include:
* The use of sexualised language or imagery, and sexual attention or advances
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying and enforcing our standards of
acceptable behaviour and will take appropriate and fair corrective action in
response to any instances of unacceptable behaviour.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, or to ban
temporarily or permanently any contributor for other behaviours that they deem
inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behaviour may be
reported to the community leaders responsible for enforcement at .
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org/), version
[1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.md) and
[2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct/code_of_conduct.md),
and was generated by [contributing.md](https://contributing.md/generator).
-159
View File
@@ -1,159 +0,0 @@
<!-- omit in toc -->
# Contributing to uTools
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> - Star the project
> - Tweet about it
> - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues
<!-- omit in toc -->
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
- [Improving The Documentation](#improving-the-documentation)
- [Styleguides](#styleguides)
- [Commit Messages](#commit-messages)
- [Join The Project Team](#join-the-project-team)
## Code of Conduct
This project and everyone participating in it is governed by the
[uTools Code of Conduct](https://github.com/MrUnknownDE/utools/blob/main/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable behavior
to .
## I Have a Question
> If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/MrUnknownDE/utools).
Before you ask a question, it is best to search for existing [Issues](https://github.com/MrUnknownDE/utools/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an [Issue](https://github.com/MrUnknownDE/utools/issues/new).
- Provide as much context as you can about what you're running into.
- Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant.
We will then take care of the issue as soon as possible.
<!--
You might want to create a separate issue tag for questions and include it in this description. People should then tag their issues accordingly.
Depending on how large the project is, you may want to outsource the questioning, e.g. to Stack Overflow or Gitter. You may add additional contact and information possibilities:
- IRC
- Slack
- Gitter
- Stack Overflow tag
- Blog
- FAQ
- Roadmap
- E-Mail List
- Forum
-->
## I Want To Contribute
> ### Legal Notice <!-- omit in toc -->
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project licence.
### Reporting Bugs
<!-- omit in toc -->
#### Before Submitting a Bug Report
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/MrUnknownDE/utools). If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/MrUnknownDE/utools/issues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
<!-- omit in toc -->
#### How Do I Submit a Good Bug Report?
> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to .
<!-- You may add a PGP key to allow the messages to be sent encrypted as well. -->
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](https://github.com/MrUnknownDE/utools/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
- Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section.
Once it's filed:
- The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced.
- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution).
<!-- You might want to create an issue template for bugs and errors that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion for uTools, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
<!-- omit in toc -->
#### Before Submitting an Enhancement
- Make sure that you are using the latest version.
- Read the [documentation](https://github.com/MrUnknownDE/utools) carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](https://github.com/MrUnknownDE/utools/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
<!-- omit in toc -->
#### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](https://github.com/MrUnknownDE/utools/issues).
- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- You may want to **include screenshots or screen recordings** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [LICEcap](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and the built-in [screen recorder in GNOME](https://help.gnome.org/users/gnome-help/stable/screen-shot-record.html.en) or [SimpleScreenRecorder](https://github.com/MaartenBaert/ssr) on Linux. <!-- this should only be included if the project has a GUI -->
- **Explain why this enhancement would be useful** to most uTools users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
<!-- You might want to create an issue template for enhancement suggestions that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->
### Your First Code Contribution
<!-- TODO
include Setup of env, IDE and typical getting started instructions?
-->
### Improving The Documentation
<!-- TODO
Updating, improving and correcting the documentation
-->
## Styleguides
### Commit Messages
<!-- TODO
-->
## Join The Project Team
<!-- TODO -->
<!-- omit in toc -->
## Attribution
This guide is based on the [contributing.md](https://contributing.md/generator)!
+126 -57
View File
@@ -5,7 +5,10 @@
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. A modern web application that displays detailed information about a client's IP address, including geolocation, ASN, rDNS, and provides network diagnostic tools like Ping, Traceroute, DNS Lookup, Subnet Calculation, and WHOIS Lookup. It also allows looking up information for any public IP address.
### Preview: https://utools.mrunk.de <!-- Optional: Füge hier einen Screenshot hinzu -->
<!-- ![Screenshot](link/to/your/screenshot.png) -->
### Livedemo: https://utools.johanneskr.de
## 🚀 Features ## 🚀 Features
@@ -13,84 +16,150 @@ A modern web application that displays detailed information about a client's IP
* **Geolocation:** Shows Country, Region, City, Postal Code, Coordinates, and Timezone based on the IP. * **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. * **ASN Information:** Displays the Autonomous System Number (ASN) and organization name.
* **Reverse DNS (rDNS):** Performs a reverse DNS lookup for the IP address. * **Reverse DNS (rDNS):** Performs a reverse DNS lookup for the IP address.
* **Interactive Dark Mode Map:** Visualizes the geolocation on a dark-themed OpenStreetMap/CartoDB map. * **Interactive Map:** Visualizes the geolocation on an OpenStreetMap.
* **Glassmorphism UI:** Features a premium, modern transparent design with animated gradients.
* **IP Lookup:** Allows users to enter any public IP address to retrieve its Geo, ASN, and rDNS information. * **IP Lookup:** Allows users to enter any public IP address to retrieve its Geo, ASN, and rDNS information.
* **Traceroute:** Initiates a server-side traceroute (via SSE stream) to the client's IP or a looked-up IP. * **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. * **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. * **DNS Lookup:** Performs various DNS record lookups (A, AAAA, MX, NS, TXT, SOA) for a given domain.
* **Subnet Calculator:** Calculates network details (address ranges, usable hosts) for IPv4 subnets. * **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. * **WHOIS Lookup:** Retrieves WHOIS information for a given domain or IP address.
* **MAC Address Lookup:** Identifies the vendor/manufacturer of a network interface using OUI data. * **Dockerized:** Both frontend and backend are containerized for easy deployment.
* **Port Scan:** Scans common ports of a target IP (via SSE stream). * **Modern UI:** Built with Tailwind CSS for a clean and responsive interface.
* **Dockerized:** both frontend and backend are containerized for easy deployment.
## 📚 API Usage
The backend exposes several RESTful endpoints that return JSON data.
| Endpoint | Method | Params | Description |
| :--- | :--- | :--- | :--- |
| `/api/ipinfo` | `GET` | None | Returns info for the requestor's IP. |
| `/api/lookup` | `GET` | `targetIp` | Returns Geo/ASN info for a specific IP. |
| `/api/dns-lookup` | `GET` | `domain`, `type` | Resolves DNS records (A, AAAA, MX, etc.). |
| `/api/whois-lookup` | `GET` | `query` | Performs a WHOIS lookup for a domain or IP. |
| `/api/mac-lookup` | `GET` | `mac` | Returns the vendor for a MAC address. |
| `/api/ping` | `GET` | `targetIp` | Pings an IP address (returns 4 packets). |
| `/api/traceroute` | `GET` | `targetIp` | Streams traceroute hops via Server-Sent Events (SSE). |
| `/api/port-scan` | `GET` | `targetIp` | Streams port scan results via Server-Sent Events (SSE). |
### Examples
**Lookup an IP:**
```bash
curl "http://localhost:3000/api/lookup?targetIp=8.8.8.8"
```
**DNS Lookup:**
```bash
curl "http://localhost:3000/api/dns-lookup?domain=google.com&type=A"
```
**MAC Vendor Lookup:**
```bash
curl "http://localhost:3000/api/mac-lookup?mac=00:50:56:C0:00:08"
```
## 🛠️ Tech Stack ## 🛠️ Tech Stack
* **Backend:** Node.js, Express.js, MaxMind GeoLite2, `oui`, `whois-json`, `@sentry/node`. * **Backend:**
* **Frontend:** Vanilla JS, Tailwind CSS, Leaflet.js. * Node.js
* **Deployment:** Docker, GitHub Actions. * 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 ## 🏁 Getting Started
### Using Pre-built Images (Recommended) You can run this application easily using Docker and Docker Compose.
1. **Create `compose.yml`:** ### Prerequisites
(See provided `compose.yml` in repository)
2. **Start:** * [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 ```bash
docker compose up -d 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:** `http://localhost:8080` 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 ## ⚙️ Configuration
Key environment variables for the backend: The application is configured mainly through environment variables set in the `docker-compose.yml` file for the `backend` service:
* `NODE_ENV`: `production` or `development`.
* `PORT`: Internal port (default 3000). * `NODE_ENV`: Set to `production` for optimal performance and JSON logging.
* `RATE_LIMIT_MAX`: Requests per window (e.g., 50). * `PORT`: The internal port the Node.js application listens on (default: `3000`).
* `SENTRY_DSN`: Optional Sentry integration. * `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 ## 🌐 Data Sources
* **Geolocation:** [MaxMind GeoLite2](https://www.maxmind.com). * **Geolocation & ASN:** This tool uses GeoLite2 data created by MaxMind, available from [https://www.maxmind.com](https://www.maxmind.com).
* **Map Tiles:** [OpenStreetMap](https://www.openstreetmap.org) & [CartoDB](https://carto.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`).
* **MAC Data:** [IEEE OUI](https://standards.ieee.org/products-services/regauth/oui/index.html). * **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 ## 📜 License
MIT License. This project is licensed under the MIT License. See the `LICENSE` file for details.
+4 -9
View File
@@ -1,6 +1,6 @@
# Stage 1: Build Dependencies # Stage 1: Build Dependencies
# Use an official Node.js runtime as a parent image # Use an official Node.js runtime as a parent image
FROM node:24-alpine AS builder FROM node:18-alpine AS builder
WORKDIR /app WORKDIR /app
@@ -18,7 +18,7 @@ RUN npm ci --only=production
# REMOVED: RUN npm i oui (should be installed by npm ci now) # REMOVED: RUN npm i oui (should be installed by npm ci now)
# Stage 2: Production Image # Stage 2: Production Image
FROM node:24-alpine FROM node:18-alpine
WORKDIR /app WORKDIR /app
@@ -37,13 +37,8 @@ COPY ./data ./data
# Create a non-root user and group # Create a non-root user and group
RUN addgroup -S appgroup && adduser -S appuser -G appgroup RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Optional: Change ownership of app files to the new user
# Create ASN cache directory and set correct ownership BEFORE switching user # RUN chown -R appuser:appgroup /app
# This ensures the Docker volume mount is writable by appuser
RUN mkdir -p /app/asn-cache && chown -R appuser:appgroup /app/asn-cache
# Change ownership of all app files to the new user
RUN chown -R appuser:appgroup /app
# Switch to the non-root user # Switch to the non-root user
USER appuser USER appuser
+72
View File
@@ -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.

Before

Width:  |  Height:  |  Size: 133 B

After

Width:  |  Height:  |  Size: 58 MiB

-2
View File
@@ -5,5 +5,3 @@ PORT=3000
LOG_LEVEL=debug # z.B. für mehr Details im Development LOG_LEVEL=debug # z.B. für mehr Details im Development
PING_COUNT=4 PING_COUNT=4
# NODE_ENV=development # Setze dies ggf. für pino-pretty # NODE_ENV=development # Setze dies ggf. für pino-pretty
RATE_LIMIT_MAX=200
RATE_LIMIT_WINDOW_MS=300000 # 5 Minuten
+525 -664
View File
File diff suppressed because it is too large Load Diff
+1 -6
View File
@@ -11,19 +11,14 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@maxmind/geoip2-node": "^6.0.0", "@maxmind/geoip2-node": "^6.0.0",
"@sentry/node": "^10.42.0", "@sentry/node": "^8.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.21.2", "express": "^4.21.2",
"express-rate-limit": "^7.5.0", "express-rate-limit": "^7.5.0",
"mac-oui-lookup": "^1.1.4",
"macaddress": "^0.5.3", "macaddress": "^0.5.3",
"pino": "^9.6.0", "pino": "^9.6.0",
"pino-pretty": "^13.0.0", "pino-pretty": "^13.0.0",
"qs": "^6.14.2",
"whois-json": "^2.0.4" "whois-json": "^2.0.4"
},
"overrides": {
"underscore": "1.13.8"
} }
} }
-275
View File
@@ -1,275 +0,0 @@
// backend/routes/asnLookup.js
const express = require('express');
const https = require('https');
const fs = require('fs');
const path = require('path');
const pino = require('pino');
const Sentry = require('@sentry/node');
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router();
// ─── Filesystem Cache (24h TTL) ───────────────────────────────────────────────
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
const CACHE_DIR = process.env.ASN_CACHE_DIR || path.join(__dirname, '..', 'data', 'asn-cache');
// Ensure cache directory exists
try {
fs.mkdirSync(CACHE_DIR, { recursive: true });
} catch (e) {
logger.warn({ error: e.message }, 'Could not create ASN cache directory');
}
function cacheFilePath(key) {
// Sanitize key to safe filename
return path.join(CACHE_DIR, key.replace(/[^a-zA-Z0-9_:-]/g, '_') + '.json');
}
function getCached(key) {
try {
const file = cacheFilePath(key);
const raw = fs.readFileSync(file, 'utf8');
const entry = JSON.parse(raw);
if (Date.now() > entry.expiresAt) {
fs.unlinkSync(file);
return null;
}
return entry.data;
} catch {
return null; // File doesn't exist or parse failed
}
}
function setCache(key, data) {
try {
const entry = { data, expiresAt: Date.now() + CACHE_TTL_MS };
fs.writeFileSync(cacheFilePath(key), JSON.stringify(entry), 'utf8');
} catch (e) {
logger.warn({ key, error: e.message }, 'ASN cache write failed');
}
}
// ─── HTTP Helper ──────────────────────────────────────────────────────────────
function fetchJson(url) {
return new Promise((resolve, reject) => {
const req = https.get(url, {
headers: {
'User-Agent': 'uTools-Network-Suite/1.0 (https://github.com/MrUnknownDE/utools)',
'Accept': 'application/json',
},
timeout: 15000,
}, (res) => {
let raw = '';
res.on('data', (chunk) => { raw += chunk; });
res.on('end', () => {
if (res.statusCode < 200 || res.statusCode >= 300) {
return reject(new Error(`HTTP ${res.statusCode} from ${url}`));
}
try { resolve(JSON.parse(raw)); }
catch (e) { reject(new Error(`JSON parse error: ${e.message}`)); }
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error(`Timeout: ${url}`)); });
});
}
// ─── ASN Validation ───────────────────────────────────────────────────────────
function parseAsn(raw) {
if (!raw || typeof raw !== 'string') return null;
const cleaned = raw.trim().toUpperCase().replace(/^AS/, '');
const n = parseInt(cleaned, 10);
if (isNaN(n) || n < 1 || n > 4294967295 || String(n) !== cleaned) return null;
return n;
}
// ─── RIPE Stat Fetchers ───────────────────────────────────────────────────────
async function fetchOverview(asn) {
const key = `overview:${asn}`;
const cached = getCached(key);
if (cached) return cached;
const json = await fetchJson(`https://stat.ripe.net/data/as-overview/data.json?resource=AS${asn}`);
const d = json?.data;
const result = {
asn,
name: d?.holder || null,
announced: d?.announced ?? false,
type: d?.type || null,
block: d?.block || null,
};
setCache(key, result);
return result;
}
async function fetchNeighbours(asn) {
const key = `neighbours:${asn}`;
const cached = getCached(key);
if (cached) return cached;
const json = await fetchJson(`https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS${asn}`);
const neighbours = (json?.data?.neighbours || []).map(n => ({
asn: n.asn,
type: n.type, // 'left' = upstream, 'right' = downstream
power: n.power || 0,
v4_peers: n.v4_peers || 0,
v6_peers: n.v6_peers || 0,
}));
setCache(key, neighbours);
return neighbours;
}
async function fetchPrefixes(asn) {
const key = `prefixes:${asn}`;
const cached = getCached(key);
if (cached) return cached;
const json = await fetchJson(`https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS${asn}`);
const prefixes = (json?.data?.prefixes || []).map(p => p.prefix);
setCache(key, prefixes);
return prefixes;
}
async function fetchPeeringDb(asn) {
const key = `peeringdb:${asn}`;
const cached = getCached(key);
if (cached !== null) return cached;
try {
const json = await fetchJson(`https://www.peeringdb.com/api/net?asn=${asn}&depth=2`);
const net = json?.data?.[0];
if (!net) { setCache(key, null); return null; }
const result = {
peeringPolicy: net.policy_general || null,
infoType: net.info_type || null,
infoTraffic: net.info_traffic || null,
infoRatio: net.info_ratio || null,
infoScope: net.info_scope || null,
website: net.website || null,
ixps: (net.netixlan_set || []).map(ix => ({
name: ix.name,
speed: ix.speed,
ipv4: ix.ipaddr4 || null,
ipv6: ix.ipaddr6 || null,
})).slice(0, 20),
};
setCache(key, result);
return result;
} catch (e) {
logger.warn({ asn, error: e.message }, 'PeeringDB fetch failed');
return null;
}
}
// ─── Resolve names for a list of ASNs ────────────────────────────────────────
async function resolveNames(asnList) {
const results = await Promise.allSettled(asnList.map(a => fetchOverview(a)));
const map = {};
results.forEach((r, i) => {
map[asnList[i]] = r.status === 'fulfilled' ? (r.value.name || null) : null;
});
return map;
}
// ─── Route ────────────────────────────────────────────────────────────────────
router.get('/', async (req, res, next) => {
const rawAsn = req.query.asn;
const requestIp = req.ip;
const asn = parseAsn(String(rawAsn || ''));
if (!asn) {
return res.status(400).json({
success: false,
error: 'Invalid ASN. Please provide a number between 1 and 4294967295, e.g. ?asn=15169'
});
}
logger.info({ requestIp, asn }, 'ASN lookup request');
try {
// Level 1 + Level 2: fetch all base data in parallel (allSettled = one failure won't crash everything)
const [overviewResult, neighboursResult, prefixesResult, peeringdbResult] = await Promise.allSettled([
fetchOverview(asn),
fetchNeighbours(asn),
fetchPrefixes(asn),
fetchPeeringDb(asn),
]);
const overview = overviewResult.status === 'fulfilled' ? overviewResult.value : { asn, name: null, announced: false, type: null };
const neighbours = neighboursResult.status === 'fulfilled' ? neighboursResult.value : [];
const prefixes = prefixesResult.status === 'fulfilled' ? prefixesResult.value : [];
const peeringdb = peeringdbResult.status === 'fulfilled' ? peeringdbResult.value : null;
if (overviewResult.status === 'rejected') logger.warn({ asn, error: overviewResult.reason?.message }, 'Overview fetch failed, continuing with partial data');
if (neighboursResult.status === 'rejected') logger.warn({ asn, error: neighboursResult.reason?.message }, 'Neighbours fetch failed, continuing with partial data');
if (prefixesResult.status === 'rejected') logger.warn({ asn, error: prefixesResult.reason?.message }, 'Prefixes fetch failed, continuing with partial data');
// Split neighbours (keep ALL of them, sorted by power)
const allUpstreams = neighbours.filter(n => n.type === 'left').sort((a, b) => b.power - a.power);
const allDownstreams = neighbours.filter(n => n.type === 'right').sort((a, b) => b.power - a.power);
// Resolve names for only the Top 25 of each, to prevent hammering the RIPE API (rate limits)
const topLevel2Asns = [...new Set([
...allUpstreams.slice(0, 25),
...allDownstreams.slice(0, 25)
].map(n => n.asn))];
const level2Names = await resolveNames(topLevel2Asns);
// Level 3: fetch upstreams-of-upstreams for top 5 Level 2 upstreams
const level3Raw = await Promise.allSettled(
allUpstreams.slice(0, 5).map(async (upstreamNode) => {
const theirNeighbours = await fetchNeighbours(upstreamNode.asn);
const theirUpstreams = theirNeighbours
.filter(n => n.type === 'left')
.sort((a, b) => b.power - a.power)
.slice(0, 3);
return { parentAsn: upstreamNode.asn, theirUpstreams };
})
);
const level3Data = level3Raw
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
// Resolve names for Level 3 nodes
const level3Asns = [...new Set(level3Data.flatMap(d => d.theirUpstreams.map(n => n.asn)))];
const level3Names = await resolveNames(level3Asns);
// ── Build graph ───────────────────────────────────────────────────────
const graph = {
center: { asn, name: overview.name },
level2: {
upstreams: allUpstreams.map(n => ({ asn: n.asn, name: level2Names[n.asn] || null, power: n.power, v4: n.v4_peers, v6: n.v6_peers })),
downstreams: allDownstreams.map(n => ({ asn: n.asn, name: level2Names[n.asn] || null, power: n.power, v4: n.v4_peers, v6: n.v6_peers })),
},
level3: level3Data.map(d => ({
parentAsn: d.parentAsn,
parentName: level2Names[d.parentAsn] || null,
upstreams: d.theirUpstreams.map(n => ({
asn: n.asn,
name: level3Names[n.asn] || null,
power: n.power,
})),
})),
};
res.json({
success: true,
asn,
name: overview.name,
announced: overview.announced,
type: overview.type,
prefixes: prefixes, // Export all prefixes without limit
peeringdb,
graph,
});
} catch (error) {
logger.error({ asn, requestIp, error: error.message }, 'ASN lookup failed');
Sentry.captureException(error, { extra: { asn, requestIp } });
next(error);
}
});
module.exports = router;
-39
View File
@@ -1,39 +0,0 @@
const express = require('express');
const Sentry = require("@sentry/node");
const pino = require('pino');
const { getVendor } = require('mac-oui-lookup');
const { isValidMacAddress } = require('../utils');
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router();
router.get('/', async (req, res) => {
const macRaw = req.query.mac;
const mac = typeof macRaw === 'string' ? macRaw.trim() : macRaw;
const requestIp = req.ip || req.socket.remoteAddress;
logger.info({ requestIp, mac }, 'MAC lookup request received');
if (!isValidMacAddress(mac)) {
logger.warn({ requestIp, mac }, 'Invalid MAC address for lookup');
return res.status(400).json({ success: false, error: 'Invalid MAC address format provided.' });
}
try {
const vendor = getVendor(mac);
if (vendor) {
logger.info({ requestIp, mac, vendor }, 'MAC lookup successful');
res.json({ success: true, mac, vendor });
} else {
logger.info({ requestIp, mac }, 'MAC address not found in OUI database');
res.status(404).json({ success: false, error: 'Vendor not found for this MAC address.' });
}
} catch (error) {
logger.error({ requestIp, mac, error: error.message }, 'MAC lookup failed');
Sentry.captureException(error, { extra: { requestIp, mac } });
res.status(500).json({ success: false, error: 'An unexpected error occurred during the MAC lookup.' });
}
});
module.exports = router;
-76
View File
@@ -1,76 +0,0 @@
const express = require('express');
const Sentry = require("@sentry/node");
const pino = require('pino');
const { isValidIp, isPrivateIp, checkPort } = require('../utils');
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router();
const COMMON_PORTS_TO_SCAN = [
21, 22, 25, 53, 80, 110, 143, 443, 3306, 3389, 5432, 8080, 8443
];
router.get('/', (req, res) => {
const targetIpRaw = req.query.targetIp;
const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw;
const requestIp = req.ip || req.socket.remoteAddress;
logger.info({ requestIp, targetIp }, 'Port scan stream request received');
if (!isValidIp(targetIp)) {
logger.warn({ requestIp, targetIp }, 'Invalid target IP for port scan');
return res.status(400).json({ success: false, error: 'Invalid target IP address provided.' });
}
if (isPrivateIp(targetIp)) {
logger.warn({ requestIp, targetIp }, 'Attempt to scan private IP blocked');
return res.status(403).json({ success: false, error: 'Operations on private or local IP addresses are not allowed.' });
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders();
const sendEvent = (event, data) => {
if (!res.writableEnded) {
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
}
};
let isConnectionClosed = false;
req.on('close', () => {
logger.info({ requestIp, targetIp }, 'Client disconnected from port scan stream.');
isConnectionClosed = true;
});
(async () => {
try {
sendEvent('info', { message: `Starting scan of ${COMMON_PORTS_TO_SCAN.length} common ports on ${targetIp}...` });
for (const port of COMMON_PORTS_TO_SCAN) {
if (isConnectionClosed) break;
const result = await checkPort(port, targetIp, 2000);
sendEvent('port_status', result);
}
if (!isConnectionClosed) {
logger.info({ requestIp, targetIp }, 'Port scan stream completed successfully.');
sendEvent('end', { message: 'Scan complete.' });
}
} catch (error) {
logger.error({ requestIp, targetIp, error: error.message }, 'Error during port scan stream');
// Add context directly to the captureException call
Sentry.captureException(error, { extra: { requestIp, targetIp } });
if (!isConnectionClosed) {
sendEvent('error', { error: 'An unexpected error occurred during the scan.' });
}
} finally {
if (!res.writableEnded) {
res.end();
}
}
})();
});
module.exports = router;
+219
View File
@@ -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;
+20 -1
View File
@@ -1,3 +1,4 @@
// backend/routes/traceroute.js
const express = require('express'); const express = require('express');
const Sentry = require("@sentry/node"); const Sentry = require("@sentry/node");
const { spawn } = require('child_process'); const { spawn } = require('child_process');
@@ -36,6 +37,13 @@ router.get('/', (req, res) => {
return res.status(403).json({ success: false, error: 'Operations on private or local IP addresses are not allowed.' }); 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 { try {
logger.info({ requestIp, targetIp }, `Starting traceroute stream...`); logger.info({ requestIp, targetIp }, `Starting traceroute stream...`);
res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Content-Type', 'text/event-stream');
@@ -69,6 +77,7 @@ router.get('/', (req, res) => {
Sentry.captureException(e, { level: 'warning', extra: { requestIp, targetIp, event } }); Sentry.captureException(e, { level: 'warning', extra: { requestIp, targetIp, event } });
if (proc && !proc.killed) proc.kill(); if (proc && !proc.killed) proc.kill();
if (!res.writableEnded) res.end(); if (!res.writableEnded) res.end();
// No manual transaction finishing needed here
} }
}; };
@@ -79,8 +88,10 @@ router.get('/', (req, res) => {
lines.forEach(line => { lines.forEach(line => {
const parsed = parseTracerouteLine(line); const parsed = parseTracerouteLine(line);
if (parsed) { if (parsed) {
// logger.debug({ requestIp, targetIp, hop: parsed.hop, ip: parsed.ip }, 'Sending hop data');
sendEvent('hop', parsed); sendEvent('hop', parsed);
} else if (line.trim()) { } else if (line.trim()) {
// logger.debug({ requestIp, targetIp, message: line.trim() }, 'Sending info data');
sendEvent('info', { message: line.trim() }); sendEvent('info', { message: line.trim() });
} }
}); });
@@ -99,6 +110,7 @@ router.get('/', (req, res) => {
Sentry.captureException(err, { extra: { requestIp, targetIp } }); // Capture original error Sentry.captureException(err, { extra: { requestIp, targetIp } }); // Capture original error
sendEvent('error', { error: `Failed to start traceroute: ${errorMsg}` }); sendEvent('error', { error: `Failed to start traceroute: ${errorMsg}` });
if (!res.writableEnded) res.end(); if (!res.writableEnded) res.end();
// No manual transaction finishing needed here
}); });
proc.on('close', (code) => { proc.on('close', (code) => {
@@ -113,23 +125,30 @@ router.get('/', (req, res) => {
logger.error({ requestIp, targetIp, exitCode: code }, errorMsg); logger.error({ requestIp, targetIp, exitCode: code }, errorMsg);
Sentry.captureMessage('Traceroute command failed', { level: 'error', extra: { requestIp, targetIp, exitCode: code } }); Sentry.captureMessage('Traceroute command failed', { level: 'error', extra: { requestIp, targetIp, exitCode: code } });
sendEvent('error', { error: errorMsg }); sendEvent('error', { error: errorMsg });
// Transaction status will be inferred by Sentry based on errors captured
} else { } else {
logger.info({ requestIp, targetIp }, `Traceroute stream completed successfully.`); logger.info({ requestIp, targetIp }, `Traceroute stream completed successfully.`);
// Transaction status will likely be 'ok' if no errors were captured
} }
sendEvent('end', { exitCode: code }); sendEvent('end', { exitCode: code });
if (!res.writableEnded) res.end(); if (!res.writableEnded) res.end();
// No manual transaction finishing needed here
}); });
req.on('close', () => { req.on('close', () => {
logger.info({ requestIp, targetIp }, 'Client disconnected from traceroute stream, killing process.'); logger.info({ requestIp, targetIp }, 'Client disconnected from traceroute stream, killing process.');
if (proc && !proc.killed) proc.kill(); if (proc && !proc.killed) proc.kill();
if (!res.writableEnded) res.end(); 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) { } 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.'); 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'); logger.error({ requestIp, targetIp, error: errorMsg, stack: error.stack }, 'Error setting up traceroute stream');
Sentry.captureException(error, { extra: { requestIp, targetIp } }); Sentry.captureException(error, { extra: { requestIp, targetIp } }); // Capture original error
// No manual transaction finishing needed here
if (!res.headersSent) { if (!res.headersSent) {
res.status(500).json({ success: false, error: `Failed to initiate traceroute: ${errorMsg}` }); res.status(500).json({ success: false, error: `Failed to initiate traceroute: ${errorMsg}` });
+1 -6
View File
@@ -5,7 +5,7 @@ const whois = require('whois-json');
const pino = require('pino'); const pino = require('pino');
// Import utilities // Import utilities
const { isValidIp, isValidDomain, isPrivateIp } = require('../utils'); const { isValidIp, isValidDomain } = require('../utils');
// Logger for this module // Logger for this module
const logger = pino({ level: process.env.LOG_LEVEL || 'info' }); const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
@@ -26,11 +26,6 @@ router.get('/', async (req, res, next) => {
return res.status(400).json({ success: false, error: 'Invalid domain name or IP address provided for WHOIS lookup.' }); return res.status(400).json({ success: false, error: 'Invalid domain name or IP address provided for WHOIS lookup.' });
} }
if (isValidIp(query) && isPrivateIp(query)) {
logger.warn({ requestIp, query }, 'Attempt to WHOIS lookup private IP blocked');
return res.status(403).json({ success: false, error: 'WHOIS lookup for private or local IP addresses is not supported.' });
}
// Note: No isPrivateIp check here, as WHOIS for IPs might be desired regardless of range, // Note: No isPrivateIp check here, as WHOIS for IPs might be desired regardless of range,
// and domain lookups don't involve IP ranges. // and domain lookups don't involve IP ranges.
+15 -17
View File
@@ -1,3 +1,5 @@
// server.js
// Load .env variables FIRST!
require('dotenv').config(); require('dotenv').config();
// --- Sentry Initialisierung (GANZ OBEN, nach dotenv) --- // --- Sentry Initialisierung (GANZ OBEN, nach dotenv) ---
@@ -9,12 +11,6 @@ Sentry.init({
dsn: process.env.SENTRY_DSN || "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@oooooooooooooooo.ingest.sentry.io/123456", dsn: process.env.SENTRY_DSN || "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@oooooooooooooooo.ingest.sentry.io/123456",
// Enable tracing - Adjust sample rate as needed // Enable tracing - Adjust sample rate as needed
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0, tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
integrations: [
// send console.log, console.warn, and console.error calls as logs to Sentry
Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
],
// Enable logs to be sent to Sentry
enableLogs: true,
}); });
// DEBUG: Check Sentry object after init // DEBUG: Check Sentry object after init
@@ -37,9 +33,7 @@ const lookupRoutes = require('./routes/lookup');
const dnsLookupRoutes = require('./routes/dnsLookup'); const dnsLookupRoutes = require('./routes/dnsLookup');
const whoisLookupRoutes = require('./routes/whoisLookup'); const whoisLookupRoutes = require('./routes/whoisLookup');
const versionRoutes = require('./routes/version'); const versionRoutes = require('./routes/version');
const portScanRoutes = require('./routes/portScan'); const sslCheckRoutes = require('./routes/sslCheck'); // <-- NEUE ROUTE IMPORTIERT
const macLookupRoutes = require('./routes/macLookup');
const asnLookupRoutes = require('./routes/asnLookup');
// --- Logger Initialisierung --- // --- Logger Initialisierung ---
const logger = pino({ const logger = pino({
@@ -78,11 +72,11 @@ app.set('trust proxy', parseInt(process.env.TRUST_PROXY_COUNT || '2', 10)); // A
// --- Rate Limiter --- // --- Rate Limiter ---
// Apply a general limiter to most routes // Apply a general limiter to most routes
const generalLimiter = rateLimit({ const generalLimiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || (5 * 60 * 1000).toString(), 10), // Default 5 minutes 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 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 standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers
message: { success: false, error: 'Too many requests from this IP, please try again after a while' }, 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 keyGenerator: (req, res) => req.ip, // Use client IP address from Express
handler: (req, res, next, options) => { handler: (req, res, next, options) => {
logger.warn({ ip: req.ip, route: req.originalUrl }, 'Rate limit exceeded'); logger.warn({ ip: req.ip, route: req.originalUrl }, 'Rate limit exceeded');
@@ -94,8 +88,14 @@ const generalLimiter = rateLimit({
} }
}); });
// Apply the limiter to ALL API routes // Apply the limiter to specific API routes that perform external actions
app.use('/api', generalLimiter); // 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 --- // --- API Routes ---
@@ -107,9 +107,7 @@ app.use('/api/lookup', lookupRoutes);
app.use('/api/dns-lookup', dnsLookupRoutes); app.use('/api/dns-lookup', dnsLookupRoutes);
app.use('/api/whois-lookup', whoisLookupRoutes); app.use('/api/whois-lookup', whoisLookupRoutes);
app.use('/api/version', versionRoutes); app.use('/api/version', versionRoutes);
app.use('/api/port-scan', portScanRoutes); app.use('/api/ssl-check', sslCheckRoutes); // <-- NEUE ROUTE REGISTRIERT
app.use('/api/mac-lookup', macLookupRoutes);
app.use('/api/asn-lookup', asnLookupRoutes);
// --- Sentry Error Handler --- // --- Sentry Error Handler ---
+2 -86
View File
@@ -29,12 +29,6 @@ function isValidIp(ip) {
*/ */
function isPrivateIp(ip) { function isPrivateIp(ip) {
if (!ip) return false; if (!ip) return false;
// Normalize IPv6-mapped IPv4 addresses (e.g., ::ffff:192.168.1.1 -> 192.168.1.1)
if (ip.startsWith('::ffff:')) {
ip = ip.substring(7);
}
const ipVersion = net.isIP(ip); const ipVersion = net.isIP(ip);
if (ipVersion === 4) { if (ipVersion === 4) {
@@ -44,15 +38,12 @@ function isPrivateIp(ip) {
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || // 172.16.0.0/12 (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] === 192 && parts[1] === 168) || // 192.168.0.0/16
parts[0] === 127 || // 127.0.0.0/8 (Loopback) parts[0] === 127 || // 127.0.0.0/8 (Loopback)
(parts[0] === 169 && parts[1] === 254) || // 169.254.0.0/16 (Link-local) (parts[0] === 169 && parts[1] === 254) // 169.254.0.0/16 (Link-local)
// Block 0.0.0.0 (Commonly "Any" or "Current Network")
(parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0)
); );
} else if (ipVersion === 6) { } else if (ipVersion === 6) {
const lowerCaseIp = ip.toLowerCase(); const lowerCaseIp = ip.toLowerCase();
return ( return (
lowerCaseIp === '::1' || // ::1/128 (Loopback) lowerCaseIp === '::1' || // ::1/128 (Loopback)
lowerCaseIp === '::' || // ::/128 (Unspecified)
lowerCaseIp.startsWith('fc') || lowerCaseIp.startsWith('fd') || // fc00::/7 (Unique Local) lowerCaseIp.startsWith('fc') || lowerCaseIp.startsWith('fd') || // fc00::/7 (Unique Local)
lowerCaseIp.startsWith('fe8') || lowerCaseIp.startsWith('fe9') || // fe80::/10 (Link-local) lowerCaseIp.startsWith('fe8') || lowerCaseIp.startsWith('fe9') || // fe80::/10 (Link-local)
lowerCaseIp.startsWith('fea') || lowerCaseIp.startsWith('feb') lowerCaseIp.startsWith('fea') || lowerCaseIp.startsWith('feb')
@@ -75,20 +66,6 @@ function isValidDomain(domain) {
return domainRegex.test(domain.trim()); return domainRegex.test(domain.trim());
} }
/**
* Validiert eine MAC-Adresse.
* @param {string} mac - Die zu validierende MAC-Adresse.
* @returns {boolean} True, wenn das Format gültig ist, sonst false.
*/
function isValidMacAddress(mac) {
if (!mac || typeof mac !== 'string') {
return false;
}
// This regex matches common MAC address formats (e.g., 00:1A:2B:3C:4D:5E, 00-1A-2B-3C-4D-5E, 001A2B3C4D5E)
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$|^([0-9A-Fa-f]{12})$/;
return macRegex.test(mac.trim());
}
/** /**
* Bereinigt eine IP-Adresse (z.B. entfernt ::ffff: Präfix von IPv4-mapped IPv6). * Bereinigt eine IP-Adresse (z.B. entfernt ::ffff: Präfix von IPv4-mapped IPv6).
@@ -279,75 +256,14 @@ function parseTracerouteLine(line) {
return null; return null;
} }
/**
* Checks if a specific TCP port is open on a given host.
* @param {number} port - The port to check.
* @param {string} host - The target host IP address.
* @param {number} timeout - Connection timeout in milliseconds.
* @returns {Promise<{port: number, status: 'open'|'closed'|'timeout', service: string}>} A promise that resolves with the port status.
*/
function checkPort(port, host, timeout = 2000) {
// A small map of common ports to their services
const commonPorts = {
21: 'FTP', 22: 'SSH', 23: 'Telnet', 25: 'SMTP', 53: 'DNS', 80: 'HTTP',
110: 'POP3', 143: 'IMAP', 443: 'HTTPS', 445: 'SMB', 993: 'IMAPS',
995: 'POP3S', 1433: 'MSSQL', 1521: 'Oracle', 3306: 'MySQL', 3389: 'RDP',
5432: 'PostgreSQL', 5900: 'VNC', 8080: 'HTTP-Alt', 8443: 'HTTPS-Alt'
};
const service = commonPorts[port] || 'Unknown';
return new Promise((resolve, reject) => {
// DEFENSE IN DEPTH: Prevent scanning of private IPs at the function level
if (!isValidIp(host) || isPrivateIp(host)) {
const error = new Error(`Scanning restricted: ${host} is not a valid public IP.`);
logger.warn({ host, port }, "Blocked attempt to scan restricted IP in checkPort");
return resolve({
port,
status: 'error',
service,
error: 'Restricted IP',
details: 'Scanning private or invalid IPs is not allowed.'
});
}
const socket = new net.Socket();
socket.setTimeout(timeout);
socket.on('connect', () => {
socket.destroy();
resolve({ port, status: 'open', service });
});
socket.on('timeout', () => {
socket.destroy();
resolve({ port, status: 'timeout', service });
});
socket.on('error', (err) => {
socket.destroy();
// 'ECONNREFUSED' is the key for a closed port. Other errors might be network issues.
const status = err.code === 'ECONNREFUSED' ? 'closed' : 'error';
resolve({ port, status, service, error: err.code });
});
// Explicit inline guard (defence-in-depth; also satisfies CodeQL SSRF dataflow)
if (!isValidIp(host) || isPrivateIp(host)) {
socket.destroy();
return resolve({ port, status: 'error', service, error: 'Restricted IP' });
}
socket.connect(port, host);
});
}
module.exports = { module.exports = {
isValidIp, isValidIp,
isPrivateIp, isPrivateIp,
isValidDomain, isValidDomain,
isValidMacAddress,
getCleanIp, getCleanIp,
executeCommand, executeCommand,
parsePingOutput, parsePingOutput,
parseTracerouteLine, parseTracerouteLine,
checkPort, // Note: logger is not exported, assuming it's managed globally or passed where needed
}; };
+3
View File
@@ -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
+1 -11
View File
@@ -1,13 +1,3 @@
docker compose down docker compose down
# Setzt die Git-Commit-Variable für den Build-Prozess
export GIT_COMMIT_SHA=$(git rev-parse --short HEAD) export GIT_COMMIT_SHA=$(git rev-parse --short HEAD)
export SENTRY_DSN="https://7ea70caba68f548fb96482a573006a7b@o447623.ingest.us.sentry.io/4509062020333568" docker compose -f compose.yml up -d --build
# Schritt 1: Baue die Images mit der compose.build.yml
echo "--- Building Docker images ---"
docker compose -f compose.build.yml build
# Schritt 2: Starte die Container mit der compose.yml, die die gebauten Images verwendet
echo "--- Starting containers ---"
docker compose -f compose.yml up -d
-16
View File
@@ -1,16 +0,0 @@
# compose.build.yml
services:
backend:
# Definiert, wie das Image gebaut wird
build:
context: ./backend
args:
- GIT_COMMIT_SHA=${GIT_COMMIT_SHA:-unknown}
- SENTRY_DSN=${SENTRY_DSN:-}
image: mrunknownde/utools-backend:latest
frontend:
# Definiert, wie das Image gebaut wird
build:
context: ./frontend
image: mrunknownde/utools-frontend:latest
+54
View File
@@ -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
+31 -25
View File
@@ -1,45 +1,51 @@
services: services:
# Backend Service (Node.js App) # Backend Service (Node.js App)
backend: backend:
image: mrunknownde/utools-backend build:
container_name: utools_backend 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 restart: unless-stopped
environment: environment:
NODE_ENV: production # Setze Umgebungsvariablen für das Backend
PORT: 3000 NODE_ENV: production # Wichtig für Performance und Logging
LOG_LEVEL: info PORT: 3000 # Port innerhalb des Containers
LOG_LEVEL: info # Oder 'warn' für weniger Logs in Produktion
PING_COUNT: 4 PING_COUNT: 4
SENTRY_DSN: "https://7ea70caba68f548fb96482a573006a7b@o447623.ingest.us.sentry.io/4509062020333568" # Die DB-Pfade werden aus dem Backend-Dockerfile ENV genommen,
# ASN Cache directory (filesystem persistence across restarts) # könnten hier aber überschrieben werden, falls nötig.
ASN_CACHE_DIR: /app/asn-cache # GEOIP_CITY_DB: ./data/GeoLite2-City.mmdb
volumes: # GEOIP_ASN_DB: ./data/GeoLite2-ASN.mmdb
# Persistent ASN lookup cache — survives container restarts # Sentry DSN aus der Umgebung/ .env Datei übernehmen
- asn_cache:/app/asn-cache SENTRY_DSN: "https://7ea70caba68f548fb96482a573006a7b@o447623.ingest.us.sentry.io/4509062020333568" # Wichtig für die Laufzeit
dns: dns:
- 1.1.1.1 - 1.1.1.1 # Cloudflare DNS
- 1.0.0.1 - 1.0.0.1 # Cloudflare DNS
- 8.8.8.8 - 8.8.8.8 # Google DNS
- 8.8.4.4 - 8.8.4.4 # Google DNS
networks: networks:
- utools_network - utools_network # Verbinde mit unserem benutzerdefinierten Netzwerk
# Frontend Service (Nginx) # Frontend Service (Nginx)
frontend: frontend:
image: mrunknownde/utools-frontend build: ./frontend # Pfad zum Verzeichnis mit dem Frontend-Dockerfile
container_name: utools_frontend container_name: utools_frontend
restart: unless-stopped restart: unless-stopped
ports: 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" - "8080:80"
depends_on: depends_on:
- backend - backend # Stellt sicher, dass Backend gestartet wird (aber nicht unbedingt bereit ist)
networks: networks:
- utools_network - utools_network # Verbinde mit unserem benutzerdefinierten Netzwerk
# Definiere ein benutzerdefiniertes Netzwerk (gute Praxis)
networks: networks:
utools_network: utools_network:
driver: bridge driver: bridge # Standard-Netzwerktreiber
# Named volume — ASN cache persists across container restarts
volumes:
asn_cache:
driver: local
+26
View File
@@ -0,0 +1,26 @@
# Stage 1: Build (falls wir später einen Build-Schritt hätten, z.B. für Tailwind Purge)
# Aktuell nicht nötig, da wir CDN/statische Dateien haben.
# Stage 2: Production Environment using Nginx
FROM nginx:1.25-alpine
# Arbeitsverzeichnis im Container (optional, aber gute Praxis)
WORKDIR /usr/share/nginx/html
# Entferne die Standard Nginx Willkommensseite
RUN rm /etc/nginx/conf.d/default.conf
# Kopiere unsere eigene Nginx Konfiguration
COPY nginx.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
-471
View File
@@ -1,471 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASN / AS Lookup - uTools</title>
<meta name="description"
content="Look up any Autonomous System Number (ASN) to see peering connections, network graph, prefixes and IXP information.">
<script src="https://cdn.tailwindcss.com"></script>
<!-- D3.js v7 for network graph -->
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<style>
.loader {
border: 4px solid rgba(168, 85, 247, 0.1);
border-left-color: #d8b4fe;
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.glass-panel {
background: rgba(17, 24, 39, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(31, 41, 55, 0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
.text-gradient {
background: linear-gradient(to right, #c084fc, #e879f9);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hidden {
display: none !important;
}
nav ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
nav a {
color: #d1d5db;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
nav a:hover {
color: #fff;
background: rgba(168, 85, 247, 0.2);
border-color: rgba(168, 85, 247, 0.4);
}
nav a.active-link {
background: rgba(168, 85, 247, 0.3);
color: #fff;
border-color: #a855f7;
}
header {
background: rgba(31, 41, 55, 0.4);
backdrop-filter: blur(10px);
padding: 1.5rem;
margin-bottom: 2rem;
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
@media (min-width: 768px) {
header {
flex-direction: row;
justify-content: space-between;
}
}
/* ── Network Graph ─────────────────────────────────────── */
#graph-container {
width: 100%;
height: 600px;
background: rgba(0, 0, 0, 0.3);
border-radius: 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.06);
overflow: hidden;
position: relative;
}
#graph-svg {
width: 100%;
height: 100%;
cursor: grab;
}
#graph-svg:active {
cursor: grabbing;
}
.node-center circle {
fill: #a855f7;
stroke: #d8b4fe;
stroke-width: 2.5;
}
.node-upstream circle {
fill: #3b82f6;
stroke: #93c5fd;
stroke-width: 1.5;
}
.node-downstream circle {
fill: #10b981;
stroke: #6ee7b7;
stroke-width: 1.5;
}
.node-tier1 circle {
fill: #6b7280;
stroke: #9ca3af;
stroke-width: 1.5;
}
.node text {
fill: #e5e7eb;
font-size: 11px;
font-family: 'Courier New', monospace;
pointer-events: none;
text-anchor: middle;
}
.node:hover circle {
filter: brightness(1.4);
cursor: pointer;
}
.link {
stroke: rgba(255, 255, 255, 0.12);
stroke-linecap: round;
}
.link-upstream {
stroke: rgba(59, 130, 246, 0.35);
}
.link-tier1 {
stroke: rgba(107, 114, 128, 0.3);
stroke-dasharray: 4 3;
}
.link-downstream {
stroke: rgba(16, 185, 129, 0.35);
}
/* Tooltip */
#graph-tooltip {
position: absolute;
pointer-events: none;
background: rgba(17, 24, 39, 0.95);
backdrop-filter: blur(8px);
border: 1px solid rgba(168, 85, 247, 0.4);
border-radius: 0.5rem;
padding: 0.6rem 0.9rem;
font-size: 12px;
color: #e5e7eb;
max-width: 220px;
z-index: 50;
opacity: 0;
transition: opacity 0.15s;
}
/* Prefix list */
.prefix-tag {
display: inline-block;
font-family: monospace;
font-size: 11px;
background: rgba(168, 85, 247, 0.15);
color: #c084fc;
border: 1px solid rgba(168, 85, 247, 0.3);
border-radius: 4px;
padding: 2px 6px;
margin: 2px;
}
/* IXP table */
.ixp-row {
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.ixp-row:last-child {
border-bottom: none;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
</style>
</head>
<body
class="bg-gray-950 text-gray-100 font-sans p-4 md:p-8 min-h-screen bg-[url('https://tailwindcss.com/_next/static/media/hero-dark.939eb757.png')] bg-cover bg-center bg-fixed selection:bg-purple-500 selection:text-white">
<header class="glass-panel">
<h1>uTools <span class="text-sm font-normal text-gray-400 opacity-75 tracking-wider uppercase ml-2">Network
Suite</span></h1>
<nav>
<ul>
<li><a href="/">IP Info &amp; Tools</a></li>
<li><a href="/subnet">Subnetz Rechner</a></li>
<li><a href="/dns">DNS Lookup</a></li>
<li><a href="/whois">WHOIS Lookup</a></li>
<li><a href="/mac">MAC Lookup</a></li>
<li><a href="/asn" class="active-link">ASN Lookup</a></li>
</ul>
</nav>
</header>
<div
class="container mx-auto max-w-6xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h1 class="text-3xl font-bold mb-2 text-center text-gradient">AS / ASN Lookup</h1>
<p class="text-center text-gray-400 text-sm mb-8">Peering graph, prefixes &amp; IXP connections for any
Autonomous System</p>
<!-- Search -->
<div class="flex flex-col sm:flex-row gap-3 mb-6 max-w-2xl mx-auto">
<input type="text" id="asn-input" placeholder="Enter ASN (e.g. 15169 or AS3320)"
class="flex-grow px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all placeholder-gray-600">
<button id="lookup-button"
class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white font-bold py-3 px-6 rounded-lg shadow-lg hover:shadow-purple-500/25 transition-all duration-200 ease-in-out transform hover:-translate-y-0.5">
Lookup
</button>
</div>
<!-- Error -->
<div id="error-box"
class="hidden max-w-2xl mx-auto mb-6 p-4 bg-red-900/30 border border-red-500/40 text-red-300 rounded-lg text-sm">
</div>
<!-- Loading -->
<div id="loading-section" class="hidden flex flex-col items-center gap-4 py-16">
<div class="loader" style="width:40px;height:40px;border-width:5px;"></div>
<p class="text-gray-400 text-sm" id="loading-msg">Querying RIPE Stat &amp; PeeringDB…</p>
<!-- Shown after 3s for slow lookups -->
<div id="loading-hint" class="hidden mt-2 max-w-sm text-center">
<p class="text-xs text-amber-400/80 bg-amber-400/10 border border-amber-400/20 rounded-lg px-4 py-2">
⏳ Large ASes (like Cloudflare, Google, Tier-1 carriers) can take up to 15 seconds on the first
lookup — subsequent lookups are cached for 7 days.
</p>
</div>
</div>
<!-- Results -->
<div id="results-section" class="hidden fade-in">
<!-- AS Info Header -->
<div class="glass-card rounded-xl p-6 mb-6">
<div class="flex flex-col md:flex-row md:items-center gap-4">
<div class="flex-1">
<div class="flex items-center gap-3 mb-1">
<span id="res-asn" class="font-mono text-2xl font-bold text-purple-400"></span>
<span id="res-announced-badge"
class="hidden text-xs px-2 py-0.5 bg-green-500/20 border border-green-500/40 text-green-400 rounded-full">Announced</span>
<span id="res-type-badge"
class="text-xs px-2 py-0.5 bg-blue-500/20 border border-blue-500/40 text-blue-300 rounded-full"></span>
</div>
<h2 id="res-name" class="text-xl font-semibold text-white mb-1"></h2>
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-400 mb-2">
<span id="res-policy-container" class="hidden">Peering Policy: <span id="res-policy"
class="text-gray-200"></span></span>
<span id="res-website-container" class="hidden">Website: <a id="res-website" href="#"
target="_blank"
class="text-purple-400 hover:text-purple-300 transition-colors"></a></span>
</div>
<!-- Rich Info Grid -->
<div id="res-rich-info"
class="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-4 pt-4 border-t border-gray-700/50 hidden">
<div id="res-info-type-container" class="hidden">
<div class="text-xs text-gray-500 uppercase tracking-widest">Type</div>
<div id="res-info-type" class="text-sm text-gray-200 capitalize mt-0.5"></div>
</div>
<div id="res-info-scope-container" class="hidden">
<div class="text-xs text-gray-500 uppercase tracking-widest">Scope</div>
<div id="res-info-scope" class="text-sm text-gray-200 capitalize mt-0.5"></div>
</div>
<div id="res-info-traffic-container" class="hidden">
<div class="text-xs text-gray-500 uppercase tracking-widest">Traffic</div>
<div id="res-info-traffic" class="text-sm text-gray-200 capitalize mt-0.5"></div>
</div>
<div id="res-info-ratio-container" class="hidden">
<div class="text-xs text-gray-500 uppercase tracking-widest">Ratio</div>
<div id="res-info-ratio" class="text-sm text-gray-200 capitalize mt-0.5"></div>
</div>
</div>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 text-center">
<div class="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3">
<div id="res-upstream-count" class="text-xl font-bold text-blue-400"></div>
<div class="text-xs text-gray-400 mt-0.5">Upstreams</div>
</div>
<div class="bg-green-500/10 border border-green-500/20 rounded-lg p-3">
<div id="res-downstream-count" class="text-xl font-bold text-green-400"></div>
<div class="text-xs text-gray-400 mt-0.5">Downstreams</div>
</div>
<div
class="bg-purple-500/10 border border-purple-500/20 rounded-lg p-3 col-span-2 sm:col-span-1">
<div id="res-prefix-count" class="text-xl font-bold text-purple-400"></div>
<div class="text-xs text-gray-400 mt-0.5">Prefixes</div>
</div>
</div>
</div>
</div>
<!-- Network Map -->
<div class="glass-card rounded-xl p-6 mb-6">
<div class="flex items-center gap-3 mb-4">
<h3 class="text-lg font-bold text-purple-300">Network Map</h3>
<div class="flex gap-3 text-xs text-gray-400">
<span class="flex items-center gap-1"><span
class="inline-block w-3 h-3 rounded-full bg-gray-500"></span>Tier-1 / Transit</span>
<span class="flex items-center gap-1"><span
class="inline-block w-3 h-3 rounded-full bg-blue-500"></span>Upstream</span>
<span class="flex items-center gap-1"><span
class="inline-block w-3 h-3 rounded-full bg-purple-500"></span>This AS</span>
<span class="flex items-center gap-1"><span
class="inline-block w-3 h-3 rounded-full bg-green-500"></span>Downstream</span>
</div>
<span class="ml-auto text-xs text-gray-500">Scroll to zoom · Drag to pan · Click node to open</span>
</div>
<div id="graph-container">
<svg id="graph-svg"></svg>
<div id="graph-tooltip"></div>
</div>
</div>
<!-- Prefixes + IXPs side by side -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<!-- Prefixes -->
<div class="glass-card rounded-xl p-5">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest">Announced Prefixes</h3>
<button id="prefix-toggle"
class="text-xs text-purple-400 hover:text-purple-300 transition-colors">Show all</button>
</div>
<div id="prefix-list" class="max-h-48 overflow-y-auto"></div>
<p id="prefix-empty" class="hidden text-sm text-gray-500 italic">No prefix data available.</p>
</div>
<!-- IXPs -->
<div class="glass-card rounded-xl p-5">
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-3">IXP Presence <span
class="text-xs font-normal text-gray-500">(via PeeringDB)</span></h3>
<div id="ixp-list" class="space-y-1 text-sm max-h-48 overflow-y-auto">
<p id="ixp-empty" class="text-gray-500 italic text-sm">Not listed on PeeringDB.</p>
</div>
</div>
</div>
<!-- Direct Peers Table -->
<div class="glass-card rounded-xl p-5">
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-3">Direct Neighbours <span
class="text-xs font-normal text-gray-500">(Level 2 · via RIPE Stat)</span></h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Upstreams -->
<div>
<h4 class="text-xs font-semibold text-blue-400 mb-2 flex items-center gap-1">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M10 17a.75.75 0 01-.75-.75V5.56l-2.47 2.47a.75.75 0 01-1.06-1.06l3.75-3.75a.75.75 0 011.06 0l3.75 3.75a.75.75 0 11-1.06 1.06L10.75 5.56v10.69A.75.75 0 0110 17z"
clip-rule="evenodd" />
</svg>
Upstreams (Transit Providers)
</h4>
<div id="upstream-table" class="space-y-1 text-xs font-mono max-h-52 overflow-y-auto"></div>
</div>
<!-- Downstreams -->
<div>
<h4 class="text-xs font-semibold text-green-400 mb-2 flex items-center gap-1">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M10 3a.75.75 0 01.75.75v10.69l2.47-2.47a.75.75 0 111.06 1.06l-3.75 3.75a.75.75 0 01-1.06 0l-3.75-3.75a.75.75 0 111.06-1.06L9.25 14.44V3.75A.75.75 0 0110 3z"
clip-rule="evenodd" />
</svg>
Downstreams (Customers)
</h4>
<div id="downstream-table" class="space-y-1 text-xs font-mono max-h-52 overflow-y-auto"></div>
</div>
</div>
</div>
</div><!-- /results -->
<footer class="mt-12 pt-6 border-t border-gray-700/30 text-center text-xs text-gray-500">
<p>Data: <a href="https://stat.ripe.net" target="_blank"
class="text-purple-400 hover:text-purple-300 transition-colors">RIPE Stat</a> &amp; <a
href="https://www.peeringdb.com" target="_blank"
class="text-purple-400 hover:text-purple-300 transition-colors">PeeringDB</a> · Cache: 7 days</p>
<p class="mt-1">&copy; 2025 <a href="https://mrunk.de"
class="text-purple-400 hover:text-purple-300 transition-colors">MrUnknownDE</a></p>
</footer>
</div>
<script src="asn-lookup.js"></script>
</body>
</html>
-399
View File
@@ -1,399 +0,0 @@
// frontend/app/asn-lookup.js
'use strict';
const API_BASE = '/api';
// ─── State ────────────────────────────────────────────────────────────────────
let currentData = null;
let showAllPrefixes = false;
// ─── DOM Refs ─────────────────────────────────────────────────────────────────
const asnInput = document.getElementById('asn-input');
const lookupButton = document.getElementById('lookup-button');
const errorBox = document.getElementById('error-box');
const loadingSection = document.getElementById('loading-section');
const loadingMsg = document.getElementById('loading-msg');
const resultsSection = document.getElementById('results-section');
// ─── Helpers ──────────────────────────────────────────────────────────────────
function showError(msg) {
errorBox.textContent = msg;
errorBox.classList.remove('hidden');
loadingSection.classList.add('hidden');
resultsSection.classList.add('hidden');
if (window._loadingHintTimer) clearTimeout(window._loadingHintTimer);
}
function hideError() { errorBox.classList.add('hidden'); }
function setLoading(msg = 'Querying RIPE Stat & PeeringDB…') {
hideError();
loadingMsg.textContent = msg;
loadingSection.classList.remove('hidden');
resultsSection.classList.add('hidden');
// After 3s show a hint that large ASes can be slow
if (window._loadingHintTimer) clearTimeout(window._loadingHintTimer);
window._loadingHintTimer = setTimeout(() => {
const hint = document.getElementById('loading-hint');
if (hint) hint.classList.remove('hidden');
}, 3000);
}
function updateUrlParam(asn) {
const url = new URL(window.location);
url.searchParams.set('asn', asn);
window.history.pushState({}, '', url);
}
// ─── Main Lookup ──────────────────────────────────────────────────────────────
async function doLookup(rawAsn) {
const asn = String(rawAsn || '').trim().toUpperCase().replace(/^AS/, '');
if (!asn || isNaN(Number(asn))) {
showError('Please enter a valid ASN (e.g. 15169 or AS3320).');
return;
}
setLoading('Querying RIPE Stat & PeeringDB…');
updateUrlParam(asn);
try {
const res = await fetch(`${API_BASE}/asn-lookup?asn=${encodeURIComponent(asn)}`);
const data = await res.json();
if (!res.ok || !data.success) {
showError(data.error || `Request failed (HTTP ${res.status})`);
return;
}
currentData = data;
renderResults(data);
} catch (err) {
showError(`Network error: ${err.message}`);
}
}
// ─── Render ───────────────────────────────────────────────────────────────────
function renderResults(data) {
// Show results FIRST so the graph container has real dimensions (clientWidth > 0)
loadingSection.classList.add('hidden');
resultsSection.classList.remove('hidden');
// Reset loading hint for next lookup
if (window._loadingHintTimer) clearTimeout(window._loadingHintTimer);
const hint = document.getElementById('loading-hint');
if (hint) hint.classList.add('hidden');
// Header
document.getElementById('res-asn').textContent = `AS${data.asn}`;
document.getElementById('res-name').textContent = data.name || 'Unknown';
const announcedBadge = document.getElementById('res-announced-badge');
if (announcedBadge) {
if (data.announced) announcedBadge.classList.remove('hidden');
else announcedBadge.classList.add('hidden');
}
const typeBadge = document.getElementById('res-type-badge');
if (typeBadge) {
typeBadge.textContent = data.type || '';
typeBadge.classList.toggle('hidden', !data.type);
}
const peeringPolicy = data.peeringdb?.peeringPolicy;
const policyContainer = document.getElementById('res-policy-container');
const policyEl = document.getElementById('res-policy');
if (policyContainer && policyEl) {
if (peeringPolicy) {
policyEl.textContent = peeringPolicy;
policyContainer.classList.remove('hidden');
} else {
policyContainer.classList.add('hidden');
}
}
const website = data.peeringdb?.website;
const websiteContainer = document.getElementById('res-website-container');
const websiteEl = document.getElementById('res-website');
if (websiteContainer && websiteEl) {
if (website) {
websiteEl.href = website;
websiteEl.textContent = website.replace(/^https?:\/\//, '').replace(/\/$/, '');
websiteContainer.classList.remove('hidden');
} else {
websiteContainer.classList.add('hidden');
}
}
// Rich Info Grid
const richInfo = document.getElementById('res-rich-info');
let hasRichInfo = false;
const fields = [
{ id: 'type', value: data.peeringdb?.infoType },
{ id: 'scope', value: data.peeringdb?.infoScope },
{ id: 'traffic', value: data.peeringdb?.infoTraffic },
{ id: 'ratio', value: data.peeringdb?.infoRatio }
];
fields.forEach(f => {
const container = document.getElementById(`res-info-${f.id}-container`);
const el = document.getElementById(`res-info-${f.id}`);
if (container && el) {
if (f.value) {
el.textContent = f.value;
container.classList.remove('hidden');
hasRichInfo = true;
} else {
container.classList.add('hidden');
}
}
});
if (richInfo) {
if (hasRichInfo) richInfo.classList.remove('hidden');
else richInfo.classList.add('hidden');
}
document.getElementById('res-upstream-count').textContent = data.graph?.level2?.upstreams?.length ?? '?';
document.getElementById('res-downstream-count').textContent = data.graph?.level2?.downstreams?.length ?? '?';
document.getElementById('res-prefix-count').textContent = data.prefixes?.length ?? '?';
// Prefixes + IXPs (before graph — these are cheap)
renderPrefixes(data.prefixes);
renderIxps(data.peeringdb?.ixps);
renderNeighbourTable('upstream-table', data.graph?.level2?.upstreams ?? [], 'blue');
renderNeighbourTable('downstream-table', data.graph?.level2?.downstreams ?? [], 'green');
// Graph LAST — needs the container to be visible for clientWidth
if (data.graph) renderGraph(data.graph);
}
// ─── Prefix List ─────────────────────────────────────────────────────────────
function renderPrefixes(prefixes) {
const list = document.getElementById('prefix-list');
const empty = document.getElementById('prefix-empty');
const toggle = document.getElementById('prefix-toggle');
if (!prefixes || prefixes.length === 0) {
list?.classList.add('hidden');
empty?.classList.remove('hidden');
toggle?.classList.add('hidden');
return;
}
empty?.classList.add('hidden');
toggle?.classList.remove('hidden');
const toShow = showAllPrefixes ? prefixes : prefixes.slice(0, 20);
if (list) list.innerHTML = toShow.map(p => `<span class="prefix-tag">${p}</span>`).join('');
if (toggle) toggle.textContent = showAllPrefixes ? 'Show less' : `Show all (${prefixes.length})`;
}
document.getElementById('prefix-toggle').addEventListener('click', () => {
showAllPrefixes = !showAllPrefixes;
if (currentData) renderPrefixes(currentData.prefixes);
});
// ─── IXP List ─────────────────────────────────────────────────────────────────
function renderIxps(ixps) {
const list = document.getElementById('ixp-list');
const empty = document.getElementById('ixp-empty');
if (!ixps || ixps.length === 0) {
if (list) list.innerHTML = '';
empty?.classList.remove('hidden');
return;
}
empty?.classList.add('hidden');
if (list) {
list.innerHTML = ixps.map(ix => `
<div class="ixp-row py-1.5 flex items-center justify-between gap-2 text-sm">
<span class="text-gray-200 truncate">${ix.name}</span>
<span class="text-xs text-gray-500 shrink-0">${ix.speed >= 1000 ? `${ix.speed / 1000}G` : `${ix.speed}M`}</span>
</div>
`).join('');
}
}
// ─── Neighbour Table ──────────────────────────────────────────────────────────
function renderNeighbourTable(elId, nodes, colour) {
const el = document.getElementById(elId);
if (!nodes || nodes.length === 0) {
el.innerHTML = `<p class="text-gray-500 italic">None reported.</p>`;
return;
}
const colClass = colour === 'blue' ? 'text-blue-400' : 'text-green-400';
el.innerHTML = nodes.map(n => `
<div class="flex items-center gap-2 py-0.5 hover:bg-white/5 rounded px-1 cursor-pointer group"
onclick="window.location.href='/asn?asn=${n.asn}'">
<span class="${colClass} font-bold w-14 shrink-0">AS${n.asn}</span>
<span class="text-gray-300 truncate flex-1 group-hover:text-white">${n.name || '—'}</span>
<span class="text-gray-600 shrink-0">${n.power ? `pwr:${n.power}` : ''}</span>
</div>
`).join('');
}
// ─── D3 Network Graph ─────────────────────────────────────────────────────────
function renderGraph(graph) {
const container = document.getElementById('graph-container');
const svg = d3.select('#graph-svg');
svg.selectAll('*').remove();
const W = container.clientWidth;
const H = container.clientHeight;
// ── Build nodes & links ───────────────────────────────────────────────────
const nodeMap = new Map();
function addNode(asn, name, role) {
const key = String(asn);
if (!nodeMap.has(key)) nodeMap.set(key, { id: key, asn, name: name || `AS${asn}`, role });
}
addNode(graph.center.asn, graph.center.name, 'center');
// Limit graph nodes to top 15 to prevent Physics Engine crash & unreadable hairball
const vizUpstreams = graph.level2.upstreams.slice(0, 15);
const vizDownstreams = graph.level2.downstreams.slice(0, 15);
vizUpstreams.forEach(n => addNode(n.asn, n.name, 'upstream'));
vizDownstreams.forEach(n => addNode(n.asn, n.name, 'downstream'));
graph.level3.forEach(d => {
d.upstreams.forEach(n => addNode(n.asn, n.name, 'tier1'));
});
const nodes = Array.from(nodeMap.values());
const links = [];
const centerId = String(graph.center.asn);
vizUpstreams.forEach(n => {
links.push({ source: String(n.asn), target: centerId, type: 'upstream', power: n.power || 1 });
});
vizDownstreams.forEach(n => {
links.push({ source: centerId, target: String(n.asn), type: 'downstream', power: n.power || 1 });
});
graph.level3.forEach(d => {
d.upstreams.forEach(n => {
links.push({ source: String(n.asn), target: String(d.parentAsn), type: 'tier1', power: n.power || 1 });
});
});
// Remove duplicate links
const uniqueLinks = Array.from(
new Map(links.map(l => [`${l.source}-${l.target}`, l])).values()
);
// ── Layer X positions (fixed horizontal layout) ────────────────────────
const layerX = { tier1: W * 0.08, upstream: W * 0.3, center: W * 0.55, downstream: W * 0.8 };
nodes.forEach(n => {
n.fx = layerX[n.role] ?? W / 2;
});
// ── Power → stroke width ──────────────────────────────────────────────
const maxPower = Math.max(...uniqueLinks.map(l => l.power), 1);
const strokeScale = d3.scaleLinear().domain([0, maxPower]).range([0.5, 4]);
// ── Node size ─────────────────────────────────────────────────────────
const nodeRadius = { center: 20, upstream: 11, downstream: 11, tier1: 8 };
// ── Simulation ────────────────────────────────────────────────────────
const sim = d3.forceSimulation(nodes)
.force('link', d3.forceLink(uniqueLinks).id(d => d.id).distance(d => {
if (d.type === 'tier1') return 90;
if (d.type === 'upstream') return 130;
return 110;
}).strength(0.6))
.force('charge', d3.forceManyBody().strength(-220))
.force('y', d3.forceY(H / 2).strength(0.04))
.force('collide', d3.forceCollide().radius(d => nodeRadius[d.role] + 14))
.alphaDecay(0.025);
// ── Zoom/Pan ──────────────────────────────────────────────────────────
const g = svg.append('g');
svg.call(d3.zoom().scaleExtent([0.3, 3]).on('zoom', evt => g.attr('transform', evt.transform)));
// ── Draw links ────────────────────────────────────────────────────────
const link = g.append('g').selectAll('line')
.data(uniqueLinks).join('line')
.attr('class', d => `link link-${d.type}`)
.attr('stroke-width', d => strokeScale(d.power));
// ── Draw nodes ────────────────────────────────────────────────────────
const tooltip = document.getElementById('graph-tooltip');
const node = g.append('g').selectAll('g')
.data(nodes).join('g')
.attr('class', d => `node node-${d.role}`)
.style('cursor', 'pointer')
.on('click', (_, d) => {
if (d.role !== 'center') window.location.href = `/asn?asn=${d.asn}`;
})
.on('mouseenter', (evt, d) => {
tooltip.style.opacity = '1';
tooltip.innerHTML = `
<strong class="text-purple-300">AS${d.asn}</strong><br>
<span class="text-gray-300">${d.name}</span><br>
<span class="text-gray-500 text-xs capitalize">${d.role === 'tier1' ? 'Tier-1 / Transit' : d.role}</span>
`;
})
.on('mousemove', (evt) => {
const rect = container.getBoundingClientRect();
let x = evt.clientX - rect.left + 14;
let y = evt.clientY - rect.top - 10;
if (x + 230 > W) x = x - 244;
tooltip.style.left = x + 'px';
tooltip.style.top = y + 'px';
})
.on('mouseleave', () => { tooltip.style.opacity = '0'; })
.call(d3.drag()
.on('start', (evt, d) => { if (!evt.active) sim.alphaTarget(0.3).restart(); d.fy = d.y; })
.on('drag', (evt, d) => { d.fy = evt.y; })
.on('end', (evt, d) => { if (!evt.active) sim.alphaTarget(0); d.fy = null; })
);
node.append('circle').attr('r', d => nodeRadius[d.role]);
// Label: 2 lines (ASN + name truncated)
node.append('text')
.attr('dy', d => nodeRadius[d.role] + 13)
.attr('font-size', d => d.role === 'center' ? 12 : 9)
.attr('fill', '#e5e7eb')
.text(d => `AS${d.asn}`);
// Name label for ALL roles (tier1 gets shorter truncation)
node.append('text')
.attr('dy', d => nodeRadius[d.role] + 23)
.attr('font-size', d => d.role === 'tier1' ? 7 : 8)
.attr('fill', d => d.role === 'tier1' ? '#6b7280' : '#9ca3af')
.text(d => {
if (!d.name) return '';
const max = d.role === 'center' ? 22 : d.role === 'tier1' ? 12 : 16;
return d.name.length > max ? d.name.slice(0, max) + '…' : d.name;
});
// ── Tick ──────────────────────────────────────────────────────────────
sim.on('tick', () => {
// Clamp Y so nodes don't fly off
nodes.forEach(n => { n.y = Math.max(30, Math.min(H - 30, n.y)); });
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node.attr('transform', d => `translate(${d.x},${d.y})`);
});
}
// ─── Initialise ───────────────────────────────────────────────────────────────
lookupButton.addEventListener('click', () => doLookup(asnInput.value));
asnInput.addEventListener('keypress', e => { if (e.key === 'Enter') doLookup(asnInput.value); });
// Auto-lookup from URL
const urlParam = new URLSearchParams(window.location.search).get('asn');
if (urlParam) {
asnInput.value = urlParam;
doLookup(urlParam);
}
+39 -183
View File
@@ -1,6 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -9,200 +8,65 @@
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<!-- Eigene Styles --> <!-- Eigene Styles -->
<style> <style>
/* Einfacher Lade-Spinner (Tailwind animiert) */ /* Einfacher Lade-Spinner */
.loader { .loader {
border: 4px solid rgba(168, 85, 247, 0.1); border: 4px solid rgba(168, 85, 247, 0.3); /* Lila transparent */
/* Lila sehr transparent */ border-left-color: #a855f7; /* Lila */
border-left-color: #d8b4fe;
/* Helleres Lila */
border-radius: 50%; border-radius: 50%;
width: 24px; width: 24px;
height: 24px; height: 24px;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
@keyframes spin { @keyframes spin {
to { to { transform: rotate(360deg); }
transform: rotate(360deg);
} }
}
/* Glassmorphism Utilities */
.glass-panel {
background: rgba(17, 24, 39, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(31, 41, 55, 0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* Ergebnis-Pre-Formatierung */ /* Ergebnis-Pre-Formatierung */
.result-pre { .result-pre {
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
font-family: 'Courier New', Courier, monospace; font-family: monospace;
background-color: rgba(0, 0, 0, 0.3); background-color: #1f2937; /* Dunkelgrau */
color: #e5e7eb; color: #d1d5db; /* Hellgrau */
padding: 1rem; padding: 1rem;
border-radius: 0.375rem; border-radius: 0.375rem; /* rounded-md */
max-height: 400px; max-height: 400px;
overflow-y: auto; overflow-y: auto;
font-size: 0.875rem; font-size: 0.875rem; /* text-sm */
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.3);
} }
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* Navigations-Styling */ /* Navigations-Styling */
nav ul { nav ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 1rem; }
list-style: none; nav a { color: #c4b5fd; text-decoration: none; white-space: nowrap; }
padding: 0; nav a:hover { color: #a78bfa; text-decoration: underline; }
margin: 0; 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; }
display: flex; @media (min-width: 768px) { header { flex-direction: row; justify-content: space-between; } }
flex-wrap: wrap; header h1 { font-size: 1.5rem; font-weight: bold; color: #e5e7eb; }
gap: 0.5rem; .hidden { display: none; }
}
nav a {
color: #d1d5db;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
nav a:hover {
color: #fff;
background: rgba(168, 85, 247, 0.2);
border-color: rgba(168, 85, 247, 0.4);
box-shadow: 0 0 15px rgba(168, 85, 247, 0.1);
}
nav a.active-link {
background: rgba(168, 85, 247, 0.3);
color: #fff;
border-color: #a855f7;
}
header {
background: rgba(31, 41, 55, 0.4);
backdrop-filter: blur(10px);
padding: 1.5rem;
margin-bottom: 2rem;
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
header {
flex-direction: row;
justify-content: space-between;
}
}
header h1 {
background-clip: text;
}
.text-gradient {
background: linear-gradient(to right, #c084fc, #e879f9);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hidden {
display: none !important;
}
</style> </style>
</head> </head>
<body class="bg-gray-900 text-gray-200 font-sans p-4 md:p-8">
<body <header>
class="bg-gray-950 text-gray-100 font-sans p-4 md:p-8 min-h-screen bg-[url('https://tailwindcss.com/_next/static/media/hero-dark.939eb757.png')] bg-cover bg-center bg-fixed selection:bg-purple-500 selection:text-white"> <h1>uTools Network Suite</h1>
<header class="glass-panel">
<h1>uTools <span class="text-sm font-normal text-gray-400 opacity-75 tracking-wider uppercase ml-2">Network
Suite</span></h1>
<nav> <nav>
<ul> <ul>
<li><a href="/">IP Info &amp; Tools</a></li> <li><a href="index.html">IP Info & Tools</a></li>
<li><a href="/subnet">Subnetz Rechner</a></li> <li><a href="subnet-calculator.html">Subnetz Rechner</a></li>
<li><a href="/dns" class="active-link">DNS Lookup</a></li> <li><a href="dns-lookup.html">DNS Lookup</a></li>
<li><a href="/whois">WHOIS Lookup</a></li> <li><a href="whois-lookup.html">WHOIS Lookup</a></li>
<li><a href="/mac">MAC Lookup</a></li>
<li><a href="/asn">ASN Lookup</a></li>
</ul> </ul>
</nav> </nav>
</header> </header>
<div <div class="container mx-auto max-w-4xl bg-gray-800 rounded-lg shadow-xl p-6">
class="container mx-auto max-w-5xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h1 class="text-3xl font-bold mb-8 text-center text-gradient glitch-text">DNS Lookup</h1> <h1 class="text-3xl font-bold mb-6 text-purple-400 text-center">DNS Lookup</h1>
<!-- Bereich für DNS Lookup --> <!-- Bereich für DNS Lookup -->
<div class="mt-8 p-6 glass-card rounded-xl"> <div class="mt-8 p-4 bg-gray-700 rounded">
<div class="flex flex-col sm:flex-row gap-3 mb-6"> <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)" <input type="text" id="dns-domain-input" placeholder="Enter domain name (e.g., google.com)"
class="flex-grow px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all placeholder-gray-600"> 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" <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">
class="px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent cursor-pointer">
<option value="ANY">ANY</option> <option value="ANY">ANY</option>
<option value="A">A</option> <option value="A">A</option>
<option value="AAAA">AAAA</option> <option value="AAAA">AAAA</option>
@@ -215,32 +79,25 @@
<option value="PTR">PTR (Reverse)</option> <option value="PTR">PTR (Reverse)</option>
</select> </select>
<button id="dns-lookup-button" <button id="dns-lookup-button"
class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white font-bold py-3 px-6 rounded-lg shadow-lg hover:shadow-purple-500/25 transition-all duration-200 ease-in-out transform hover:-translate-y-0.5"> 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 Lookup DNS
</button> </button>
</div> </div>
<div id="dns-lookup-error" <div id="dns-lookup-error" class="text-red-400 mb-4 hidden"></div>
class="text-red-400 mb-4 hidden p-3 bg-red-900/20 border border-red-500/30 rounded"></div> <div id="dns-lookup-results-section" class="hidden mt-4 border-t border-gray-600 pt-4">
<div id="dns-lookup-results-section" class="hidden mt-6 border-t border-gray-700/50 pt-6 fade-in"> <h3 class="text-lg font-semibold text-purple-300 mb-2">DNS Results for: <span id="dns-lookup-query" class="font-mono text-purple-400"></span></h3>
<h3 class="text-lg font-semibold text-purple-300 mb-4 flex items-center gap-2"> <div id="dns-lookup-loader" class="loader hidden mb-2"></div>
<div class="w-1.5 h-6 bg-purple-500 rounded-full"></div> <pre id="dns-lookup-output" class="result-pre"></pre> <!-- Ergebnisbereich -->
DNS Results for: <span id="dns-lookup-query" class="font-mono text-purple-400 ml-1"></span>
</h3>
<div id="dns-lookup-loader" class="loader hidden mb-4"></div>
<pre id="dns-lookup-output" class="result-pre custom-scrollbar"></pre> <!-- Ergebnisbereich -->
</div> </div>
</div> </div>
<!-- Globaler Fehlerbereich --> <!-- Globaler Fehlerbereich -->
<div id="global-error" <div id="global-error" class="mt-6 p-4 bg-red-800 text-red-100 rounded hidden"></div>
class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg">
</div>
<!-- Footer für Version --> <!-- Footer für Version -->
<footer class="mt-12 pt-6 border-t border-gray-700/30 text-center text-xs text-gray-500"> <footer class="mt-8 pt-4 border-t border-gray-600 text-center text-xs text-gray-500">
<p>&copy; 2025 <a href="https://mrunk.de" <p>&copy; 2025 <a href="https://johanneskr.de" class="text-purple-400 hover:underline">Johannes Krüger</a></p>
class="text-purple-400 hover:text-purple-300 transition-colors">MrUnknownDE</a></p> <p>Version: <span id="commit-sha" class="font-mono">loading...</span></p>
<p class="mt-1">Version: <span id="commit-sha" class="font-mono text-gray-400">loading...</span></p>
</footer> </footer>
</div> </div>
@@ -248,5 +105,4 @@
<!-- Eigene JS-Logik für diese Seite --> <!-- Eigene JS-Logik für diese Seite -->
<script src="dns-lookup.js"></script> <script src="dns-lookup.js"></script>
</body> </body>
</html> </html>
-56
View File
@@ -44,11 +44,6 @@ document.addEventListener('DOMContentLoaded', () => {
* @param {function} displayFn - Funktion zur Formatierung und Anzeige der Daten im outputElement. * @param {function} displayFn - Funktion zur Formatierung und Anzeige der Daten im outputElement.
*/ */
async function fetchAndDisplay(endpoint, params, resultsSection, loaderElement, errorElement, queryElement, outputElement, displayFn) { async function fetchAndDisplay(endpoint, params, resultsSection, loaderElement, errorElement, queryElement, outputElement, displayFn) {
// Reset animation
resultsSection.classList.remove('fade-in');
void resultsSection.offsetWidth; // Trigger reflow
resultsSection.classList.add('fade-in');
resultsSection.classList.remove('hidden'); resultsSection.classList.remove('hidden');
loaderElement.classList.remove('hidden'); loaderElement.classList.remove('hidden');
errorElement.classList.add('hidden'); errorElement.classList.add('hidden');
@@ -96,35 +91,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
// --- URL Parameter Functions ---
/**
* Updates the URL with DNS lookup parameters
* @param {string} domain - The domain name
* @param {string} type - The DNS record type
*/
function updateUrlParams(domain, type) {
const url = new URL(window.location);
url.searchParams.set('domain', domain);
url.searchParams.set('type', type);
window.history.pushState({}, '', url);
}
/**
* Reads URL parameters and returns domain and type if present
* @returns {object|null} Object with domain and type, or null if not present
*/
function getUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
const domain = urlParams.get('domain');
const type = urlParams.get('type');
if (domain && type) {
return { domain, type };
}
return null;
}
// --- DNS Lookup Specific Functions --- // --- DNS Lookup Specific Functions ---
function displayDnsResults(data, outputEl) { function displayDnsResults(data, outputEl) {
if (!data.records || Object.keys(data.records).length === 0) { if (!data.records || Object.keys(data.records).length === 0) {
@@ -143,10 +109,6 @@ document.addEventListener('DOMContentLoaded', () => {
dnsLookupErrorEl.classList.remove('hidden'); dnsLookupErrorEl.classList.remove('hidden');
return; return;
} }
// Update URL with parameters
updateUrlParams(domain, type);
fetchAndDisplay( fetchAndDisplay(
'/dns-lookup', '/dns-lookup',
{ domain, type }, { domain, type },
@@ -159,21 +121,6 @@ document.addEventListener('DOMContentLoaded', () => {
); );
} }
/**
* Executes DNS lookup from URL parameters if they exist
*/
function executeLookupFromUrl() {
const params = getUrlParams();
if (params) {
// Populate the input fields
dnsDomainInput.value = params.domain;
dnsTypeSelect.value = params.type;
// Trigger the lookup
handleDnsLookupClick();
}
}
// --- Initial Load & Event Listeners --- // --- Initial Load & Event Listeners ---
fetchVersionInfo(); // Lade Versionsinfo für Footer fetchVersionInfo(); // Lade Versionsinfo für Footer
@@ -182,7 +129,4 @@ document.addEventListener('DOMContentLoaded', () => {
if (event.key === 'Enter') handleDnsLookupClick(); if (event.key === 'Enter') handleDnsLookupClick();
}); });
// Execute lookup from URL parameters if present
executeLookupFromUrl();
}); // End DOMContentLoaded }); // End DOMContentLoaded
+136 -454
View File
@@ -1,6 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -9,562 +8,245 @@
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<!-- Leaflet CSS --> <!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" /> integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
<!-- Eigene Styles --> <!-- Eigene Styles -->
<style> <style>
/* Container für Karten müssen eine Höhe haben */ /* Container für Karten müssen eine Höhe haben */
#map { #map { height: 300px; }
height: 300px; #lookup-map { height: 250px; } /* Höhe für Lookup-Karte */
}
#lookup-map {
height: 250px;
}
/* Höhe für Lookup-Karte */
/* Einfacher Lade-Spinner (Tailwind animiert) */ /* Einfacher Lade-Spinner (Tailwind animiert) */
.loader { .loader {
border: 4px solid rgba(168, 85, 247, 0.1); border: 4px solid rgba(168, 85, 247, 0.3); /* Lila transparent */
/* Lila sehr transparent */ border-left-color: #a855f7; /* Lila */
border-left-color: #d8b4fe;
/* Helleres Lila */
border-radius: 50%; border-radius: 50%;
width: 24px; width: 24px;
height: 24px; height: 24px;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
@keyframes spin { @keyframes spin {
to { to { transform: rotate(360deg); }
transform: rotate(360deg);
}
}
/* Glassmorphism Utilities */
.glass-panel {
background: rgba(17, 24, 39, 0.7);
/* gray-900 mit Opacity */
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(31, 41, 55, 0.6);
/* gray-800 mit Opacity */
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
} }
/* Basis für Glitch-Effekt (Beispiel: Text-Schatten) */ /* Basis für Glitch-Effekt (Beispiel: Text-Schatten) */
.glitch-text:hover { .glitch-text:hover {
text-shadow: text-shadow:
2px 2px 0px rgba(168, 85, 247, 0.4), 1px 1px 0px rgba(168, 85, 247, 0.7), /* Lila */
-2px -2px 0px rgba(236, 72, 153, 0.4); -1px -1px 0px rgba(76, 29, 149, 0.7); /* Dunkleres Lila */
} }
/* Klickbarer IP-Cursor und Link-Styling */ /* Klickbarer IP-Cursor und Link-Styling */
#ip-address-link { #ip-address-link {
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none; /* Standard-Link-Unterstreichung entfernen */
position: relative;
display: inline-block;
transition: color 0.3s ease;
} }
#ip-address-link:hover {
#ip-address-link::after { text-decoration: underline; /* Unterstreichung beim Hover */
content: '';
position: absolute;
width: 100%;
transform: scaleX(0);
height: 2px;
bottom: 0;
left: 0;
background-color: #d8b4fe;
transform-origin: bottom right;
transition: transform 0.25s ease-out;
}
#ip-address-link:hover::after {
transform: scaleX(1);
transform-origin: bottom left;
} }
/* Traceroute Output Formatierung */ /* Traceroute Output Formatierung */
#traceroute-output pre, #traceroute-output pre, .result-pre { /* Gemeinsamer Stil für <pre> */
.result-pre { white-space: pre-wrap; /* Zeilenumbruch */
white-space: pre-wrap; word-break: break-all; /* Lange Zeilen umbrechen */
word-break: break-all; font-family: monospace;
font-family: 'Courier New', Courier, monospace; background-color: #1f2937; /* Dunkelgrau */
/* Mehr Terminal-Feeling */ color: #d1d5db; /* Hellgrau */
background-color: rgba(0, 0, 0, 0.3);
/* Transparenter */
color: #e5e7eb;
padding: 1rem; padding: 1rem;
border-radius: 0.375rem; border-radius: 0.375rem; /* rounded-md */
max-height: 400px; max-height: 400px;
overflow-y: auto; overflow-y: auto;
font-size: 0.875rem; font-size: 0.875rem; /* text-sm */
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.3);
}
#traceroute-output .hop-line {
margin-bottom: 0.25rem;
padding-left: 0.5rem;
border-left: 2px solid transparent;
transition: border-left-color 0.3s;
}
#traceroute-output .hop-line:hover {
border-left-color: #a855f7;
background: rgba(255, 255, 255, 0.02);
}
#traceroute-output .hop-number {
display: inline-block;
width: 30px;
text-align: right;
margin-right: 15px;
color: #6b7280;
font-weight: bold;
}
#traceroute-output .hop-ip {
color: #60a5fa;
font-weight: 500;
}
#traceroute-output .hop-hostname {
color: #c084fc;
}
/* Helleres Lila */
#traceroute-output .hop-rtt {
color: #34d399;
margin-left: 8px;
font-size: 0.85em;
}
#traceroute-output .hop-timeout {
color: #f87171;
}
#traceroute-output .info-line {
color: #fbbf24;
font-style: italic;
}
#traceroute-output .error-line {
color: #f87171;
font-weight: bold;
border-left: 3px solid #f87171;
padding-left: 10px;
}
#traceroute-output .end-line {
color: #d8b4fe;
font-weight: bold;
margin-top: 15px;
text-transform: uppercase;
letter-spacing: 0.05em;
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding-top: 10px;
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
} }
#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 */ /* Navigations-Styling */
nav ul { nav ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 1rem; } /* flex-wrap hinzugefügt */
list-style: none; nav a { color: #c4b5fd; /* purple-300 */ text-decoration: none; white-space: nowrap; } /* nowrap hinzugefügt */
padding: 0; nav a:hover { color: #a78bfa; /* purple-400 */ text-decoration: underline; }
margin: 0; 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 */
display: flex; @media (min-width: 768px) { /* md breakpoint */
flex-wrap: wrap; header { flex-direction: row; justify-content: space-between; }
gap: 0.5rem;
} }
header h1 { font-size: 1.5rem; /* text-2xl */ font-weight: bold; color: #e5e7eb; /* gray-200 */ }
nav a { /* Hilfsklasse zum Verstecken */
color: #d1d5db; .hidden { display: none; }
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
nav a:hover {
color: #fff;
background: rgba(168, 85, 247, 0.2);
/* Purple tint */
border-color: rgba(168, 85, 247, 0.4);
box-shadow: 0 0 15px rgba(168, 85, 247, 0.1);
}
nav a.active-link {
/* Optional active state */
background: rgba(168, 85, 247, 0.3);
color: #fff;
border-color: #a855f7;
}
header {
background: rgba(31, 41, 55, 0.4);
/* Sehr transparent */
backdrop-filter: blur(10px);
padding: 1.5rem;
margin-bottom: 2rem;
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
@media (min-width: 768px) {
header {
flex-direction: row;
justify-content: space-between;
}
}
/* Standard property for compatibility */
header h1 {
background-clip: text;
}
.text-gradient {
background: linear-gradient(to right, #c084fc, #e879f9);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hidden {
display: none !important;
}
</style> </style>
</head> </head>
<body class="bg-gray-900 text-gray-200 font-sans p-4 md:p-8">
<body <header>
class="bg-gray-950 text-gray-100 font-sans p-4 md:p-8 min-h-screen bg-[url('https://tailwindcss.com/_next/static/media/hero-dark.939eb757.png')] bg-cover bg-center bg-fixed selection:bg-purple-500 selection:text-white"> <h1>uTools Network Suite</h1> <!-- Name angepasst -->
<header class="glass-panel">
<h1>uTools <span class="text-sm font-normal text-gray-400 opacity-75 tracking-wider uppercase ml-2">Network
Suite</span></h1>
<nav> <nav>
<ul> <ul>
<li><a href="/" class="active-link">IP Info &amp; Tools</a></li> <li><a href="index.html">IP Info & Tools</a></li> <!-- Angepasst -->
<li><a href="/subnet">Subnetz Rechner</a></li> <li><a href="subnet-calculator.html">Subnetz Rechner</a></li>
<li><a href="/dns">DNS Lookup</a></li> <li><a href="dns-lookup.html">DNS Lookup</a></li>
<li><a href="/whois">WHOIS Lookup</a></li> <li><a href="whois-lookup.html">WHOIS Lookup</a></li>
<li><a href="/mac">MAC Lookup</a></li> <li><a href="ssl-check.html">SSL Check</a></li> <!-- <-- NEUER LINK -->
<li><a href="/asn">ASN Lookup</a></li> <!-- REMOVED: MAC Lookup Link -->
</ul> </ul>
</nav> </nav>
</header> </header>
<div <div class="container mx-auto max-w-4xl bg-gray-800 rounded-lg shadow-xl p-6">
class="container mx-auto max-w-5xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h1 class="text-3xl font-bold mb-8 text-center text-gradient glitch-text">Your Digital Footprint</h1> <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 --> <!-- Bereich für EIGENE IP-Infos -->
<div id="info-section" class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8"> <div id="info-section" class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<!-- Linke Spalte: Eigene IP, Geo, ASN, rDNS --> <!-- Linke Spalte: Eigene IP, Geo, ASN, rDNS -->
<div class="space-y-6 fade-in" style="animation-delay: 0.1s;"> <div class="space-y-4 p-4 bg-gray-700 rounded">
<!-- IP Card --> <h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1">Your Public IP</h2>
<div class="glass-card rounded-lg p-5 relative overflow-hidden group"> <div id="ip-info" class="min-h-[50px]">
<div class="absolute top-0 right-0 p-2 opacity-10 group-hover:opacity-20 transition-opacity">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-purple-500" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
</div>
<h2 class="text-xs font-bold text-purple-400 uppercase tracking-widest mb-2">Your Public IP</h2>
<div id="ip-info" class="min-h-[40px] flex items-center">
<div id="ip-loader" class="loader"></div> <div id="ip-loader" class="loader"></div>
<a id="ip-address-link" href="#" <!-- Geändert zu <a> Tag -->
class="text-3xl font-mono font-bold text-white tracking-tight break-all hidden hover:text-purple-300 transition-colors" <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">
title="Click for WHOIS Lookup"> <span id="ip-address"></span> <!-- Span für den eigentlichen Text -->
<span id="ip-address"></span>
</a> </a>
</div> </div>
</div>
<!-- Geo/ASN Combo Card --> <h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1">Geolocation</h2>
<div class="glass-card rounded-lg p-5 space-y-4"> <div id="geo-info" class="min-h-[100px] space-y-1 text-sm">
<div>
<h2
class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2 border-b border-gray-700 pb-1">
Location Details</h2>
<div id="geo-info" class="min-h-[80px] space-y-1 text-sm text-gray-300">
<div id="geo-loader" class="loader"></div> <div id="geo-loader" class="loader"></div>
<div class="hidden grid grid-cols-2 gap-x-2 gap-y-1"> <div class="hidden"> <!-- Hide data until loaded -->
<p><span class="text-gray-500">Country:</span> <span id="country" <p><strong>Country:</strong> <span id="country">-</span></p>
class="text-gray-200 font-medium">-</span></p> <p><strong>Region:</strong> <span id="region">-</span></p>
<p><span class="text-gray-500">Region:</span> <span id="region" <p><strong>City:</strong> <span id="city">-</span></p>
class="text-gray-200 font-medium">-</span></p> <p><strong>Postal Code:</strong> <span id="postal">-</span></p>
<p><span class="text-gray-500">City:</span> <span id="city" <p><strong>Coordinates:</strong> <span id="coords">-</span></p>
class="text-gray-200 font-medium">-</span></p> <p><strong>Timezone:</strong> <span id="timezone">-</span></p>
<p><span class="text-gray-500">Zip:</span> <span id="postal" <p id="geo-error" class="text-red-400"></p>
class="text-gray-200 font-medium">-</span></p>
<p class="col-span-2"><span class="text-gray-500">Coords:</span> <span id="coords"
class="font-mono text-xs text-purple-300">-</span></p>
<p class="col-span-2"><span class="text-gray-500">Time:</span> <span id="timezone"
class="text-gray-200 font-medium">-</span></p>
<p id="geo-error" class="text-red-400 col-span-2 text-xs"></p>
</div>
</div> </div>
</div> </div>
<div> <h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1">ASN</h2>
<h2 <div id="asn-info" class="min-h-[50px] space-y-1 text-sm">
class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2 border-b border-gray-700 pb-1">
Network (ASN)</h2>
<div id="asn-info" class="min-h-[40px] space-y-1 text-sm text-gray-300">
<div id="asn-loader" class="loader"></div> <div id="asn-loader" class="loader"></div>
<div class="hidden"> <div class="hidden"> <!-- Hide data until loaded -->
<p><span class="text-gray-500">AS Number:</span> <span id="asn-number" <p><strong>Number:</strong> <span id="asn-number">-</span></p>
class="font-mono text-purple-300">-</span></p> <p><strong>Organization:</strong> <span id="asn-org">-</span></p>
<p><span class="text-gray-500">Org:</span> <span id="asn-org" <p id="asn-error" class="text-red-400"></p>
class="font-medium text-white">-</span></p>
<p id="asn-error" class="text-red-400 text-xs"></p>
</div>
</div>
</div> </div>
</div> </div>
<!-- rDNS Card --> <h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1">Reverse DNS (rDNS)</h2>
<div class="glass-card rounded-lg p-5"> <div id="rdns-info" class="min-h-[50px] space-y-1 text-sm">
<h2
class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2 border-b border-gray-700 pb-1">
Hostname (rDNS)</h2>
<div id="rdns-info" class="min-h-[30px] text-sm text-gray-300">
<div id="rdns-loader" class="loader"></div> <div id="rdns-loader" class="loader"></div>
<div class="hidden"> <div class="hidden"> <!-- Hide data until loaded -->
<ul id="rdns-list" class="list-none space-y-1 font-mono text-xs text-green-400"> <ul id="rdns-list" class="list-disc list-inside"><li>-</li></ul>
<li>-</li> <p id="rdns-error" class="text-red-400"></p>
</ul>
<p id="rdns-error" class="text-red-400 text-xs"></p>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Rechte Spalte: Eigene Karte --> <!-- Rechte Spalte: Eigene Karte -->
<div class="space-y-4 fade-in" style="animation-delay: 0.2s;"> <div class="space-y-4 p-4 bg-gray-700 rounded">
<h2 class="text-lg font-semibold text-gray-200 flex items-center gap-2"> <h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1">Your Location Map</h2>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-purple-400" viewBox="0 0 20 20" <div id="map-container" class="bg-gray-600 rounded min-h-[300px] flex items-center justify-center relative">
fill="currentColor"> <div id="map-loader" class="loader absolute"></div>
<path fill-rule="evenodd" <div id="map" class="w-full rounded hidden"></div>
d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" <p id="map-message" class="text-gray-400 hidden absolute">Could not load map.</p>
clip-rule="evenodd" />
</svg>
Location Visualization
</h2>
<div id="map-container"
class="bg-gray-800/50 rounded-lg min-h-[400px] h-full flex items-center justify-center relative border border-gray-700/50 shadow-inner overflow-hidden">
<div id="map-loader" class="loader absolute z-10"></div>
<div id="map"
class="w-full h-full rounded-lg hidden z-0 opacity-80 hover:opacity-100 transition-opacity duration-700">
</div>
<p id="map-message" class="text-gray-400 hidden absolute text-sm">Could not load map.</p>
<div class="absolute inset-0 pointer-events-none rounded-lg ring-1 ring-inset ring-white/10"></div>
</div> </div>
</div> </div>
</div> </div>
<!-- Bereich für IP Lookup --> <!-- Bereich für IP Lookup -->
<div class="mt-8 p-6 glass-card rounded-xl"> <div class="mt-8 p-4 bg-gray-700 rounded">
<h2 <h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1 mb-4">IP Address Lookup</h2>
class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-pink-500 mb-4 flex items-center gap-2"> <div class="flex flex-col sm:flex-row gap-2 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-pink-500" fill="none" viewBox="0 0 24 24" <input type="text" id="lookup-ip-input" placeholder="Enter IP address (e.g., 8.8.8.8)"
stroke="currentColor"> 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
IP Address / Domain Lookup
</h2>
<div class="flex flex-col sm:flex-row gap-3 mb-6">
<input type="text" id="lookup-ip-input" placeholder="Enter IP or Domain (e.g., 8.8.8.8 or google.com)"
class="flex-grow px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all placeholder-gray-600">
<button id="lookup-button" <button id="lookup-button"
class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white font-bold py-3 px-6 rounded-lg shadow-lg hover:shadow-purple-500/25 transition-all duration-200 ease-in-out transform hover:-translate-y-0.5"> class="bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded transition duration-150 ease-in-out">
Lookup Lookup IP
</button> </button>
</div> </div>
<div id="lookup-error" class="text-red-400 mb-4 hidden p-3 bg-red-900/20 border border-red-500/30 rounded"> <div id="lookup-error" class="text-red-400 mb-4 hidden"></div>
</div>
<!-- Ergebnisse des Lookups (initial versteckt) --> <!-- Ergebnisse des Lookups (initial versteckt) -->
<div id="lookup-results-section" <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">
class="hidden grid grid-cols-1 md:grid-cols-2 gap-8 mt-6 border-t border-gray-700/50 pt-6 fade-in">
<!-- Linke Spalte: IP, Geo, ASN, rDNS --> <!-- Linke Spalte: IP, Geo, ASN, rDNS -->
<div class="space-y-6"> <div class="space-y-4">
<h3 class="text-lg font-semibold text-gray-200">Result for: <span id="lookup-ip-address" <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>
class="font-mono text-purple-400 bg-purple-500/10 px-2 py-0.5 rounded"></span></h3> <div id="lookup-result-loader" class="loader hidden"></div> <!-- Loader für den gesamten Block -->
<div id="lookup-result-loader" class="loader hidden"></div>
<div id="lookup-geo-info" class="space-y-1 text-sm text-gray-300"> <div id="lookup-geo-info" class="space-y-1 text-sm">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">Geolocation</h4> <h4 class="font-semibold text-purple-300">Geolocation</h4>
<div class="grid grid-cols-2 gap-x-2 gap-y-1"> <p><strong>Country:</strong> <span id="lookup-country">-</span></p>
<p><span class="text-gray-500">Country:</span> <span id="lookup-country" <p><strong>Region:</strong> <span id="lookup-region">-</span></p>
class="text-white">-</span></p> <p><strong>City:</strong> <span id="lookup-city">-</span></p>
<p><span class="text-gray-500">Region:</span> <span id="lookup-region" <p><strong>Postal Code:</strong> <span id="lookup-postal">-</span></p>
class="text-white">-</span></p> <p><strong>Coordinates:</strong> <span id="lookup-coords">-</span></p>
<p><span class="text-gray-500">City:</span> <span id="lookup-city" <p><strong>Timezone:</strong> <span id="lookup-timezone">-</span></p>
class="text-white">-</span></p> <p id="lookup-geo-error" class="text-red-400"></p>
<p><span class="text-gray-500">Zip:</span> <span id="lookup-postal"
class="text-white">-</span></p>
<p class="col-span-2"><span class="text-gray-500">Coords:</span> <span id="lookup-coords"
class="font-mono text-purple-300">-</span></p>
<p class="col-span-2"><span class="text-gray-500">Time:</span> <span id="lookup-timezone"
class="text-white">-</span></p>
<p id="lookup-geo-error" class="text-red-400 col-span-2"></p>
</div>
</div> </div>
<div id="lookup-asn-info" class="space-y-1 text-sm text-gray-300"> <div id="lookup-asn-info" class="space-y-1 text-sm">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2 mt-4">ASN</h4> <h4 class="font-semibold text-purple-300">ASN</h4>
<p><span class="text-gray-500">Number:</span> <span id="lookup-asn-number" <p><strong>Number:</strong> <span id="lookup-asn-number">-</span></p>
class="text-white font-mono">-</span></p> <p><strong>Organization:</strong> <span id="lookup-asn-org">-</span></p>
<p><span class="text-gray-500">Org:</span> <span id="lookup-asn-org" class="text-white">-</span>
</p>
<p id="lookup-asn-error" class="text-red-400"></p> <p id="lookup-asn-error" class="text-red-400"></p>
</div> </div>
<div id="lookup-rdns-info" class="space-y-1 text-sm text-gray-300"> <div id="lookup-rdns-info" class="space-y-1 text-sm">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2 mt-4">Reverse DNS</h4> <h4 class="font-semibold text-purple-300">Reverse DNS (rDNS)</h4>
<ul id="lookup-rdns-list" class="list-none space-y-1 font-mono text-xs text-green-400"> <ul id="lookup-rdns-list" class="list-disc list-inside"><li>-</li></ul>
<li>-</li>
</ul>
<p id="lookup-rdns-error" class="text-red-400"></p> <p id="lookup-rdns-error" class="text-red-400"></p>
</div> </div>
</div> </div>
<!-- Rechte Spalte: Karte & Aktionen --> <!-- Rechte Spalte: Karte & Aktionen -->
<div class="space-y-6"> <div class="space-y-4">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">Location Map</h4> <h4 class="font-semibold text-purple-300">Location Map</h4>
<div id="lookup-map-container" <div id="lookup-map-container" class="bg-gray-600 rounded min-h-[250px] flex items-center justify-center relative">
class="glass-panel rounded-lg min-h-[250px] flex items-center justify-center relative overflow-hidden"> <div id="lookup-map-loader" class="loader hidden absolute"></div>
<div id="lookup-map-loader" class="loader hidden absolute z-10"></div> <div id="lookup-map" class="w-full rounded hidden"></div> <!-- Höhe via CSS -->
<div id="lookup-map" class="w-full rounded hidden opacity-90"></div>
<p id="lookup-map-message" class="text-gray-400 hidden absolute">Could not load map.</p> <p id="lookup-map-message" class="text-gray-400 hidden absolute">Could not load map.</p>
<div class="absolute inset-0 pointer-events-none ring-1 ring-inset ring-white/10 rounded-lg">
</div> </div>
<!-- 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> </div>
<!-- Action Buttons -->
<div class="flex flex-wrap gap-2 pt-2">
<button id="lookup-ping-button"
class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md"
disabled>Ping</button>
<button id="lookup-trace-button"
class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md"
disabled>Trace</button>
<button id="lookup-scan-button"
class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md"
disabled>Port Scan</button>
</div>
<!-- Bereich für Ping-Ergebnisse (Lookup) --> <!-- Bereich für Ping-Ergebnisse (Lookup) -->
<div id="lookup-ping-results" class="mt-4 text-sm hidden fade-in"> <div id="lookup-ping-results" class="mt-2 text-sm hidden">
<h4 class="font-bold text-purple-400 mb-2 flex items-center gap-2"> <h4 class="font-semibold text-purple-300">Ping Results</h4>
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> Ping Results
</h4>
<div id="lookup-ping-loader" class="loader hidden"></div> <div id="lookup-ping-loader" class="loader hidden"></div>
<pre id="lookup-ping-output" class="result-pre mt-1"></pre> <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 mt-2"></p> <p id="lookup-ping-error" class="text-red-400"></p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Bereich für Traceroute --> <!-- Bereich für Traceroute -->
<div id="traceroute-section" class="mt-8 p-6 glass-card rounded-xl hidden fade-in"> <div id="traceroute-section" class="mt-8 p-4 bg-gray-700 rounded hidden">
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4">Traceroute Results <h2 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-1 mb-4">Traceroute Results</h2>
</h2> <div id="traceroute-status" class="flex items-center mb-2">
<div id="traceroute-status" class="flex items-center mb-4 text-sm"> <div id="traceroute-loader" class="loader mr-2 hidden"></div>
<div id="traceroute-loader" class="loader mr-3 hidden"></div> <span id="traceroute-message" class="text-gray-400"></span>
<span id="traceroute-message" class="text-gray-300"></span>
</div>
<div id="traceroute-output" class="rounded-lg overflow-hidden custom-scrollbar">
<pre class="m-0"></pre>
</div>
</div>
<!-- Bereich für Port Scan -->
<div id="port-scan-section" class="mt-8 p-6 glass-card rounded-xl hidden fade-in">
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4">Port Scan Results</h2>
<div id="port-scan-status" class="flex items-center mb-4 text-sm">
<div id="port-scan-loader" class="loader mr-3 hidden"></div>
<span id="port-scan-message" class="text-gray-300"></span>
</div>
<div id="port-scan-output"
class="text-sm font-mono bg-gray-900/50 p-4 rounded-lg border border-gray-700/50 max-h-[300px] overflow-y-auto">
</div> </div>
<div id="traceroute-output"><pre></pre></div>
</div> </div>
<!-- Globaler Fehlerbereich --> <!-- Globaler Fehlerbereich -->
<div id="global-error" <div id="global-error" class="mt-6 p-4 bg-red-800 text-red-100 rounded hidden"></div>
class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg">
</div>
<!-- Footer für Version --> <!-- Footer für Version -->
<footer class="mt-12 pt-6 border-t border-gray-700/30 text-center text-xs text-gray-500"> <footer class="mt-8 pt-4 border-t border-gray-600 text-center text-xs text-gray-500">
<p>&copy; 2025 <a href="https://mrunk.de" <p>&copy; 2025 <a href="https://johanneskr.de" class="text-purple-400 hover:underline">Johannes Krüger</a></p>
class="text-purple-400 hover:text-purple-300 transition-colors">MrUnknownDE</a></p> <p>Version: <span id="commit-sha" class="font-mono">loading...</span></p>
<p class="mt-1">Version: <span id="commit-sha" class="font-mono text-gray-400">loading...</span></p>
</footer> </footer>
@@ -572,9 +254,9 @@
<!-- Leaflet JS --> <!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script> integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<!-- Eigene JS-Logik --> <!-- Eigene JS-Logik -->
<script src="script.js"></script> <script src="script.js"></script>
</body> </body>
</html> </html>
-236
View File
@@ -1,236 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MAC Vendor Lookup - uTools</title>
<!-- Tailwind CSS Play CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Eigene Styles -->
<style>
/* Einfacher Lade-Spinner (Tailwind animiert) */
.loader {
border: 4px solid rgba(168, 85, 247, 0.1);
/* Lila sehr transparent */
border-left-color: #d8b4fe;
/* Helleres Lila */
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Glassmorphism Utilities */
.glass-panel {
background: rgba(17, 24, 39, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(31, 41, 55, 0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* Ergebnis-Pre-Formatierung */
.result-pre {
white-space: pre-wrap;
word-break: break-all;
font-family: 'Courier New', Courier, monospace;
background-color: rgba(0, 0, 0, 0.3);
color: #e5e7eb;
padding: 1rem;
border-radius: 0.375rem;
max-height: 400px;
overflow-y: auto;
font-size: 1.25rem;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.3);
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* Navigations-Styling */
nav ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
nav a {
color: #d1d5db;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
nav a:hover {
color: #fff;
background: rgba(168, 85, 247, 0.2);
border-color: rgba(168, 85, 247, 0.4);
box-shadow: 0 0 15px rgba(168, 85, 247, 0.1);
}
nav a.active-link {
background: rgba(168, 85, 247, 0.3);
color: #fff;
border-color: #a855f7;
}
header {
background: rgba(31, 41, 55, 0.4);
backdrop-filter: blur(10px);
padding: 1.5rem;
margin-bottom: 2rem;
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
header {
flex-direction: row;
justify-content: space-between;
}
}
header h1 {
background-clip: text;
}
.text-gradient {
background: linear-gradient(to right, #c084fc, #e879f9);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hidden {
display: none !important;
}
</style>
</head>
<body
class="bg-gray-950 text-gray-100 font-sans p-4 md:p-8 min-h-screen bg-[url('https://tailwindcss.com/_next/static/media/hero-dark.939eb757.png')] bg-cover bg-center bg-fixed selection:bg-purple-500 selection:text-white">
<header class="glass-panel">
<h1>uTools <span class="text-sm font-normal text-gray-400 opacity-75 tracking-wider uppercase ml-2">Network
Suite</span></h1>
<nav>
<ul>
<li><a href="/">IP Info &amp; Tools</a></li>
<li><a href="/subnet">Subnetz Rechner</a></li>
<li><a href="/dns">DNS Lookup</a></li>
<li><a href="/whois">WHOIS Lookup</a></li>
<li><a href="/mac" class="active-link">MAC Lookup</a></li>
<li><a href="/asn">ASN Lookup</a></li>
</ul>
</nav>
</header>
<div
class="container mx-auto max-w-5xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h1 class="text-3xl font-bold mb-8 text-center text-gradient glitch-text">MAC Address Vendor Lookup</h1>
<div class="mt-8 p-6 glass-card rounded-xl">
<div class="flex flex-col sm:flex-row gap-3 mb-6">
<input type="text" id="mac-input" placeholder="Enter MAC address (e.g., 00:1A:2B:3C:4D:5E)"
class="flex-grow px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all placeholder-gray-600">
<button id="mac-lookup-button"
class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white font-bold py-3 px-6 rounded-lg shadow-lg hover:shadow-purple-500/25 transition-all duration-200 ease-in-out transform hover:-translate-y-0.5">
Find Vendor
</button>
</div>
<div id="mac-lookup-error"
class="text-red-400 mb-4 hidden p-3 bg-red-900/20 border border-red-500/30 rounded"></div>
<div id="mac-lookup-results-section" class="hidden mt-6 border-t border-gray-700/50 pt-6 fade-in">
<h3 class="text-lg font-semibold text-purple-300 mb-4 flex items-center justify-center gap-2">
<div class="w-1.5 h-6 bg-purple-500 rounded-full"></div>
Vendor for: <span id="mac-lookup-query" class="font-mono text-purple-400 ml-1"></span>
</h3>
<div id="mac-lookup-loader" class="loader hidden mb-4 mx-auto"></div>
<pre id="mac-lookup-output" class="result-pre custom-scrollbar"></pre>
</div>
</div>
<div id="global-error"
class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg">
</div>
<footer class="mt-12 pt-6 border-t border-gray-700/30 text-center text-xs text-gray-500">
<p>&copy; 2025 <a href="https://mrunk.de"
class="text-purple-400 hover:text-purple-300 transition-colors">MrUnknownDE</a></p>
<p class="mt-1">Version: <span id="commit-sha" class="font-mono text-gray-400">loading...</span></p>
</footer>
</div>
<script src="mac-lookup.js"></script>
</body>
</html>
-82
View File
@@ -1,82 +0,0 @@
// frontend/app/mac-lookup.js
document.addEventListener('DOMContentLoaded', () => {
const macInput = document.getElementById('mac-input');
const macLookupButton = document.getElementById('mac-lookup-button');
const macLookupErrorEl = document.getElementById('mac-lookup-error');
const macLookupResultsSection = document.getElementById('mac-lookup-results-section');
const macLookupQueryEl = document.getElementById('mac-lookup-query');
const macLookupLoader = document.getElementById('mac-lookup-loader');
const macLookupOutputEl = document.getElementById('mac-lookup-output');
const commitShaEl = document.getElementById('commit-sha');
const globalErrorEl = document.getElementById('global-error');
const API_BASE_URL = '/api';
function showGlobalError(message) {
if (!globalErrorEl) return;
globalErrorEl.textContent = `Error: ${message}`;
globalErrorEl.classList.remove('hidden');
}
async function fetchVersionInfo() {
if (!commitShaEl) return;
try {
const response = await fetch(`${API_BASE_URL}/version`);
if (!response.ok) throw new Error(`Network response: ${response.statusText}`);
const data = await response.json();
commitShaEl.textContent = data.commitSha || 'unknown';
} catch (error) {
console.error('Failed to fetch version info:', error);
commitShaEl.textContent = 'error';
}
}
function displayMacResult(data, outputEl) {
outputEl.textContent = data.vendor || 'No vendor found.';
}
async function handleMacLookup() {
const mac = macInput.value.trim();
if (!mac) {
macLookupErrorEl.textContent = 'Please enter a MAC address.';
macLookupErrorEl.classList.remove('hidden');
return;
}
// Reset animation
macLookupResultsSection.classList.remove('fade-in');
void macLookupResultsSection.offsetWidth; // Trigger reflow
macLookupResultsSection.classList.add('fade-in');
macLookupResultsSection.classList.remove('hidden');
macLookupLoader.classList.remove('hidden');
macLookupErrorEl.classList.add('hidden');
macLookupOutputEl.textContent = '';
macLookupQueryEl.textContent = mac;
try {
const response = await fetch(`${API_BASE_URL}/mac-lookup?mac=${encodeURIComponent(mac)}`);
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || `Request failed with status ${response.status}`);
}
displayMacResult(data, macLookupOutputEl);
} catch (error) {
console.error('Failed to fetch MAC vendor:', error);
macLookupErrorEl.textContent = `Error: ${error.message}`;
macLookupErrorEl.classList.remove('hidden');
macLookupOutputEl.textContent = '';
} finally {
macLookupLoader.classList.add('hidden');
}
}
fetchVersionInfo();
macLookupButton.addEventListener('click', handleMacLookup);
macInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') handleMacLookup();
});
});
+10 -180
View File
@@ -67,13 +67,6 @@ document.addEventListener('DOMContentLoaded', () => {
const tracerouteLoader = document.getElementById('traceroute-loader'); const tracerouteLoader = document.getElementById('traceroute-loader');
const tracerouteMessage = document.getElementById('traceroute-message'); const tracerouteMessage = document.getElementById('traceroute-message');
// --- DOM Elements (Port Scan) ---
const portScanSection = document.getElementById('port-scan-section');
const portScanOutputEl = document.getElementById('port-scan-output');
const portScanLoader = document.getElementById('port-scan-loader');
const portScanMessage = document.getElementById('port-scan-message');
const lookupScanButton = document.getElementById('lookup-scan-button');
// --- DOM Elements (Footer) --- // --- DOM Elements (Footer) ---
const commitShaEl = document.getElementById('commit-sha'); const commitShaEl = document.getElementById('commit-sha');
@@ -86,7 +79,6 @@ document.addEventListener('DOMContentLoaded', () => {
let currentIp = null; // Store the user's fetched IP let currentIp = null; // Store the user's fetched IP
let currentLookupIp = null; // Store the last successfully looked-up IP let currentLookupIp = null; // Store the last successfully looked-up IP
let eventSource = null; // Store the EventSource instance for traceroute let eventSource = null; // Store the EventSource instance for traceroute
let portScanEventSource = null; // Store the EventSource for port scan
// --- Helper Functions --- // --- Helper Functions ---
@@ -215,10 +207,9 @@ document.addEventListener('DOMContentLoaded', () => {
} else { } else {
try { try {
mapInstance = L.map(mapId).setView([lat, lon], 13); mapInstance = L.map(mapId).setView([lat, lon], 13);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>', maxZoom: 19,
subdomains: 'abcd', attribution: '© OpenStreetMap contributors'
maxZoom: 19
}).addTo(mapInstance); }).addTo(mapInstance);
L.marker([lat, lon]).addTo(mapInstance).bindPopup(`Approximate Location`).openPopup(); L.marker([lat, lon]).addTo(mapInstance).bindPopup(`Approximate Location`).openPopup();
window[mapId + '_instance'] = mapInstance; // Store instance window[mapId + '_instance'] = mapInstance; // Store instance
@@ -242,7 +233,7 @@ document.addEventListener('DOMContentLoaded', () => {
messageElement.classList.remove('hidden'); messageElement.classList.remove('hidden');
messageElement.textContent = 'Map could not be loaded (missing or invalid coordinates).'; messageElement.textContent = 'Map could not be loaded (missing or invalid coordinates).';
// If map existed, remove it to clean up resources // If map existed, remove it to clean up resources
if (mapInstance) { if(mapInstance) {
mapInstance.remove(); mapInstance.remove();
window[mapId + '_instance'] = null; window[mapId + '_instance'] = null;
} }
@@ -287,17 +278,7 @@ document.addEventListener('DOMContentLoaded', () => {
updateField(coordsEl, data.geo?.latitude ? `${data.geo.latitude}, ${data.geo.longitude}` : null); 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(timezoneEl, data.geo?.timezone, geoLoader); // Hide loader on last geo field
// ASN — render as clickable link if has a number (not an error object) updateField(asnNumberEl, data.asn?.number, null, asnErrorEl);
const asnNum = (data.asn && !data.asn.error) ? data.asn.number : null;
if (asnNum && asnNumberEl) {
// Reveal the hidden data container manually (updateField won't run the link path via error branch)
const asnContainer = asnNumberEl.closest('div:not(.loader)');
if (asnContainer) asnContainer.classList.remove('hidden');
asnNumberEl.innerHTML =
`<a href="/asn?asn=${asnNum}" class="hover:text-purple-200 underline decoration-dotted transition-colors" title="Open ASN Lookup">AS${asnNum}</a>`;
} else {
updateField(asnNumberEl, null, null, asnErrorEl, data.asn?.error || '-');
}
updateField(asnOrgEl, data.asn?.organization, asnLoader); updateField(asnOrgEl, data.asn?.organization, asnLoader);
updateRdns(rdnsListEl, data.rdns, rdnsLoader, rdnsErrorEl); updateRdns(rdnsListEl, data.rdns, rdnsLoader, rdnsErrorEl);
@@ -338,28 +319,6 @@ document.addEventListener('DOMContentLoaded', () => {
// --- Lookup Functions --- // --- Lookup Functions ---
// --- URL Parameter Functions ---
/**
* Updates the URL with IP lookup parameter
* @param {string} ip - The IP address or domain to lookup
*/
function updateLookupUrlParams(ip) {
const url = new URL(window.location);
url.searchParams.set('ip', ip);
window.history.pushState({}, '', url);
}
/**
* Reads URL parameters and returns IP if present
* @returns {string|null} IP or domain from URL, or null if not present
*/
function getLookupUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('ip');
}
/** Zeigt Fehler im Lookup-Bereich an */ /** Zeigt Fehler im Lookup-Bereich an */
function showLookupError(message) { function showLookupError(message) {
if (!lookupErrorEl) return; if (!lookupErrorEl) return;
@@ -402,8 +361,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (lookupPingLoader) lookupPingLoader.classList.add('hidden'); if (lookupPingLoader) lookupPingLoader.classList.add('hidden');
if (lookupPingOutputEl) lookupPingOutputEl.textContent = ''; if (lookupPingOutputEl) lookupPingOutputEl.textContent = '';
if (lookupPingErrorEl) lookupPingErrorEl.textContent = ''; if (lookupPingErrorEl) lookupPingErrorEl.textContent = '';
if (portScanSection) portScanSection.classList.add('hidden'); // Hide port scan results
if (portScanOutputEl) portScanOutputEl.innerHTML = '';
hideLookupStatus(); // Hide status on reset hideLookupStatus(); // Hide status on reset
const fieldsToClear = [ const fieldsToClear = [
@@ -416,7 +373,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (lookupPingButton) lookupPingButton.disabled = true; if (lookupPingButton) lookupPingButton.disabled = true;
if (lookupTraceButton) lookupTraceButton.disabled = true; if (lookupTraceButton) lookupTraceButton.disabled = true;
if (lookupScanButton) lookupScanButton.disabled = true;
currentLookupIp = null; currentLookupIp = null;
// Remove lookup map instance if it exists // Remove lookup map instance if it exists
@@ -424,11 +380,6 @@ document.addEventListener('DOMContentLoaded', () => {
window['lookup-map_instance'].remove(); window['lookup-map_instance'].remove();
window['lookup-map_instance'] = null; window['lookup-map_instance'] = null;
} }
if (portScanEventSource) {
portScanEventSource.close();
portScanEventSource = null;
}
} }
/** Ruft Informationen für eine spezifische IP ab */ /** Ruft Informationen für eine spezifische IP ab */
@@ -451,7 +402,7 @@ document.addEventListener('DOMContentLoaded', () => {
throw new Error(data.error || `Network response: ${response.statusText} (${response.status})`); throw new Error(data.error || `Network response: ${response.statusText} (${response.status})`);
} }
console.log('Received Lookup Info for', ipToLookup, ':', data); console.log(`Received Lookup Info for ${ipToLookup}:`, data);
currentLookupIp = data.ip; // Store the IP that was actually looked up currentLookupIp = data.ip; // Store the IP that was actually looked up
updateField(lookupIpAddressEl, data.ip); // Display the looked-up IP updateField(lookupIpAddressEl, data.ip); // Display the looked-up IP
@@ -462,13 +413,7 @@ document.addEventListener('DOMContentLoaded', () => {
updateField(lookupCoordsEl, data.geo?.latitude ? `${data.geo.latitude}, ${data.geo.longitude}` : null); updateField(lookupCoordsEl, data.geo?.latitude ? `${data.geo.latitude}, ${data.geo.longitude}` : null);
updateField(lookupTimezoneEl, data.geo?.timezone); updateField(lookupTimezoneEl, data.geo?.timezone);
// ASN — render as clickable link if available
if (data.asn?.number && lookupAsnNumberEl) {
lookupAsnNumberEl.innerHTML =
`<a href="/asn?asn=${data.asn.number}" class="text-purple-400 hover:text-purple-300 underline decoration-dotted transition-colors font-mono" title="Open ASN Lookup">AS${data.asn.number}</a>`;
} else {
updateField(lookupAsnNumberEl, data.asn?.number, null, lookupAsnErrorEl); updateField(lookupAsnNumberEl, data.asn?.number, null, lookupAsnErrorEl);
}
updateField(lookupAsnOrgEl, data.asn?.organization); updateField(lookupAsnOrgEl, data.asn?.organization);
updateRdns(lookupRdnsListEl, data.rdns, null, lookupRdnsErrorEl); updateRdns(lookupRdnsListEl, data.rdns, null, lookupRdnsErrorEl);
@@ -477,10 +422,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (lookupPingButton) lookupPingButton.disabled = false; if (lookupPingButton) lookupPingButton.disabled = false;
if (lookupTraceButton) lookupTraceButton.disabled = false; if (lookupTraceButton) lookupTraceButton.disabled = false;
if (lookupScanButton) lookupScanButton.disabled = false;
} catch (error) { } catch (error) {
console.error('Failed to fetch lookup info for', ipToLookup, ':', error); console.error(`Failed to fetch lookup info for ${ipToLookup}:`, error);
showLookupError(`Lookup failed: ${error.message}`); showLookupError(`Lookup failed: ${error.message}`);
if (lookupMapMessageEl) { if (lookupMapMessageEl) {
lookupMapMessageEl.textContent = 'Map could not be loaded due to an error.'; lookupMapMessageEl.textContent = 'Map could not be loaded due to an error.';
@@ -528,7 +472,7 @@ document.addEventListener('DOMContentLoaded', () => {
throw new Error(data.error || 'No A or AAAA records found.'); throw new Error(data.error || 'No A or AAAA records found.');
} catch (error) { } catch (error) {
console.error('DNS resolution failed for', domain, ':', error); console.error(`DNS resolution failed for ${domain}:`, error);
throw new Error(`Could not resolve domain: ${error.message}`); throw new Error(`Could not resolve domain: ${error.message}`);
} }
} }
@@ -657,7 +601,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (!tracerouteOutputEl) return; if (!tracerouteOutputEl) return;
const lineDiv = document.createElement('div'); const lineDiv = document.createElement('div');
if (className) lineDiv.classList.add(className); if (className) lineDiv.classList.add(className);
lineDiv.classList.add('fade-in'); // Animation hinzufügen
lineDiv.textContent = text; lineDiv.textContent = text;
tracerouteOutputEl.appendChild(lineDiv); tracerouteOutputEl.appendChild(lineDiv);
tracerouteOutputEl.scrollTop = tracerouteOutputEl.scrollHeight; tracerouteOutputEl.scrollTop = tracerouteOutputEl.scrollHeight;
@@ -666,7 +609,7 @@ document.addEventListener('DOMContentLoaded', () => {
function displayTracerouteHop(hopData) { function displayTracerouteHop(hopData) {
if (!tracerouteOutputEl) return; if (!tracerouteOutputEl) return;
const lineDiv = document.createElement('div'); const lineDiv = document.createElement('div');
lineDiv.classList.add('hop-line', 'fade-in'); // Animation hinzufügen lineDiv.classList.add('hop-line');
const hopNumSpan = document.createElement('span'); const hopNumSpan = document.createElement('span');
hopNumSpan.classList.add('hop-number'); hopNumSpan.classList.add('hop-number');
@@ -710,98 +653,13 @@ document.addEventListener('DOMContentLoaded', () => {
tracerouteOutputEl.scrollTop = tracerouteOutputEl.scrollHeight; tracerouteOutputEl.scrollTop = tracerouteOutputEl.scrollHeight;
} }
// --- Port Scan Functions ---
function startPortScan(ip) {
if (!ip) {
showGlobalError('Cannot start port scan: IP address is missing.');
return;
}
if (!portScanSection || !portScanOutputEl || !portScanLoader || !portScanMessage) return;
if (portScanEventSource) {
portScanEventSource.close();
}
portScanSection.classList.remove('hidden');
portScanOutputEl.innerHTML = '';
portScanLoader.classList.remove('hidden');
portScanMessage.textContent = `Starting port scan for ${ip}...`;
hideGlobalError();
hideLookupError();
const url = `${API_BASE_URL}/port-scan?targetIp=${encodeURIComponent(ip)}`;
portScanEventSource = new EventSource(url);
portScanEventSource.onopen = () => {
console.log('SSE connection opened for port scan.');
};
portScanEventSource.onerror = (event) => {
console.error('Port Scan EventSource failed:', event);
portScanMessage.textContent = 'Connection error during port scan.';
portScanLoader.classList.add('hidden');
portScanEventSource.close();
};
portScanEventSource.addEventListener('info', (event) => {
const infoData = JSON.parse(event.data);
portScanMessage.textContent = infoData.message;
});
portScanEventSource.addEventListener('port_status', (event) => {
const portData = JSON.parse(event.data);
displayPortScanResult(portData);
});
portScanEventSource.addEventListener('error', (event) => {
const errorData = JSON.parse(event.data);
displayPortScanResult({ error: errorData.error });
});
portScanEventSource.addEventListener('end', (event) => {
const endData = JSON.parse(event.data);
portScanMessage.textContent = endData.message;
portScanLoader.classList.add('hidden');
portScanEventSource.close();
});
}
function displayPortScanResult(data) {
if (!portScanOutputEl) return;
const lineDiv = document.createElement('div');
lineDiv.classList.add('mb-1', 'fade-in'); // Animation hinzufügen
let statusColor = 'text-gray-400';
let statusText = data.status.toUpperCase();
if (data.status === 'open') {
statusColor = 'text-green-400';
statusText = 'OPEN';
} else if (data.status === 'closed') {
statusColor = 'text-red-400';
statusText = 'CLOSED';
} else if (data.status === 'timeout') {
statusColor = 'text-yellow-400';
statusText = 'TIMEOUT (Filtered?)';
}
if (data.error) {
lineDiv.innerHTML = `<span class="text-red-400">Error: ${data.error}</span>`;
} else {
lineDiv.innerHTML = `Port <span class="font-bold w-12 inline-block">${data.port}</span> <span class="w-24 inline-block">(${data.service})</span>: <span class="font-bold ${statusColor}">${statusText}</span>`;
}
portScanOutputEl.appendChild(lineDiv);
portScanOutputEl.scrollTop = portScanOutputEl.scrollHeight;
}
// --- Event Handlers --- // --- Event Handlers ---
function handleIpClick(event) { function handleIpClick(event) {
event.preventDefault(); // Verhindert das Standardverhalten des Links (#) event.preventDefault(); // Verhindert das Standardverhalten des Links (#)
if (currentIp) { if (currentIp) {
console.log(`User IP link clicked: ${currentIp}. Redirecting to WHOIS lookup...`); console.log(`User IP link clicked: ${currentIp}. Redirecting to WHOIS lookup...`);
// Leite zur Whois-Seite weiter und übergebe die IP als 'query'-Parameter // Leite zur Whois-Seite weiter und übergebe die IP als 'query'-Parameter
window.location.href = `/whois?query=${encodeURIComponent(currentIp)}`; window.location.href = `whois-lookup.html?query=${encodeURIComponent(currentIp)}`;
} else { } else {
console.warn('Cannot redirect to WHOIS: current IP is not available.'); console.warn('Cannot redirect to WHOIS: current IP is not available.');
} }
@@ -818,9 +676,6 @@ document.addEventListener('DOMContentLoaded', () => {
resetLookupResults(); // Reset results before starting resetLookupResults(); // Reset results before starting
hideLookupError(); hideLookupError();
// Update URL with the query parameter
updateLookupUrlParams(query);
if (isValidIpAddress(query)) { if (isValidIpAddress(query)) {
// Input is an IP address // Input is an IP address
console.log(`Lookup button clicked for IP: ${query}`); console.log(`Lookup button clicked for IP: ${query}`);
@@ -863,27 +718,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
function handleLookupScanClick() {
if (currentLookupIp) {
console.log(`Starting port scan for looked-up IP: ${currentLookupIp}`);
startPortScan(currentLookupIp);
}
}
/**
* Executes IP lookup from URL parameters if they exist
*/
function executeLookupFromUrl() {
const ipParam = getLookupUrlParams();
if (ipParam && lookupIpInput) {
// Populate the input field
lookupIpInput.value = ipParam;
// Trigger the lookup
handleLookupClick();
}
}
// --- Initial Load & Event Listeners --- // --- Initial Load & Event Listeners ---
fetchIpInfo(); // Lade Infos zur eigenen IP fetchIpInfo(); // Lade Infos zur eigenen IP
fetchVersionInfo(); // Lade Versionsinfo für Footer fetchVersionInfo(); // Lade Versionsinfo für Footer
@@ -895,12 +729,8 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
if (lookupPingButton) lookupPingButton.addEventListener('click', handleLookupPingClick); if (lookupPingButton) lookupPingButton.addEventListener('click', handleLookupPingClick);
if (lookupTraceButton) lookupTraceButton.addEventListener('click', handleLookupTraceClick); if (lookupTraceButton) lookupTraceButton.addEventListener('click', handleLookupTraceClick);
if (lookupScanButton) lookupScanButton.addEventListener('click', handleLookupScanClick);
// Der Event Listener für den IP-Link wird jetzt in fetchIpInfo() hinzugefügt, // Der Event Listener für den IP-Link wird jetzt in fetchIpInfo() hinzugefügt,
// nachdem die IP erfolgreich abgerufen wurde. // nachdem die IP erfolgreich abgerufen wurde.
// Execute lookup from URL parameters if present
executeLookupFromUrl();
}); // End DOMContentLoaded }); // End DOMContentLoaded
+141
View File
@@ -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>&copy; 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>
+117
View File
@@ -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
}
}
});
+149 -242
View File
@@ -1,6 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -9,299 +8,208 @@
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<!-- Eigene Styles (für Navigation etc., wie in index.html) --> <!-- Eigene Styles (für Navigation etc., wie in index.html) -->
<style> <style>
/* Einfacher Lade-Spinner (Tailwind animiert) */
.loader {
border: 4px solid rgba(168, 85, 247, 0.1);
/* Lila sehr transparent */
border-left-color: #d8b4fe;
/* Helleres Lila */
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Glassmorphism Utilities */
.glass-panel {
background: rgba(17, 24, 39, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(31, 41, 55, 0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* Navigations-Styling */ /* Navigations-Styling */
nav ul { nav ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 1rem; } /* flex-wrap hinzugefügt */
list-style: none; nav a { color: #c4b5fd; /* purple-300 */ text-decoration: none; white-space: nowrap; } /* nowrap hinzugefügt */
padding: 0; nav a:hover { color: #a78bfa; /* purple-400 */ text-decoration: underline; }
margin: 0; 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 */
display: flex; @media (min-width: 768px) { /* md breakpoint */
flex-wrap: wrap; header { flex-direction: row; justify-content: space-between; }
gap: 0.5rem;
} }
header h1 { font-size: 1.5rem; /* text-2xl */ font-weight: bold; color: #e5e7eb; /* gray-200 */ }
nav a { /* Styling für Formular und Ergebnisse */
color: #d1d5db; label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: #d1d5db; /* gray-300 */ }
text-decoration: none; input[type="text"] {
padding: 0.5rem 1rem; width: 100%;
border-radius: 0.5rem; padding: 0.75rem;
transition: all 0.2s ease; margin-bottom: 1rem;
position: relative; background-color: #4b5563; /* gray-600 */
overflow: hidden; border: 1px solid #6b7280; /* gray-500 */
background: rgba(255, 255, 255, 0.03); border-radius: 0.375rem; /* rounded-md */
border: 1px solid rgba(255, 255, 255, 0.05); color: #e5e7eb; /* gray-200 */
font-family: monospace;
} }
input[type="text"]:focus {
nav a:hover { outline: none;
color: #fff; border-color: #a78bfa; /* purple-400 */
background: rgba(168, 85, 247, 0.2); box-shadow: 0 0 0 2px rgba(167, 139, 250, 0.5);
border-color: rgba(168, 85, 247, 0.4);
box-shadow: 0 0 15px rgba(168, 85, 247, 0.1);
} }
button[type="submit"] {
nav a.active-link { background-color: #8b5cf6; /* purple-500 */
background: rgba(168, 85, 247, 0.3); color: white;
color: #fff; padding: 0.75rem 1.5rem;
border-color: #a855f7; border: none;
border-radius: 0.375rem; /* rounded-md */
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
} }
button[type="submit"]:hover {
header { background-color: #7c3aed; /* purple-600 */
background: rgba(31, 41, 55, 0.4); }
backdrop-filter: blur(10px); #results, #examples {
margin-top: 2rem;
padding: 1.5rem; padding: 1.5rem;
margin-bottom: 2rem; background-color: #374151; /* gray-700 */
border-radius: 1rem; border-radius: 0.5rem; /* rounded-lg */
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
} }
#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; }
@media (min-width: 768px) {
header {
flex-direction: row;
justify-content: space-between;
}
}
header h1 {
background-clip: text;
}
.text-gradient {
background: linear-gradient(to right, #c084fc, #e879f9);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hidden {
display: none !important;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
</style> </style>
</head> </head>
<body class="bg-gray-900 text-gray-200 font-sans p-4 md:p-8">
<body <header>
class="bg-gray-950 text-gray-100 font-sans p-4 md:p-8 min-h-screen bg-[url('https://tailwindcss.com/_next/static/media/hero-dark.939eb757.png')] bg-cover bg-center bg-fixed selection:bg-purple-500 selection:text-white"> <h1>uTools Network Suite</h1> <!-- Titel angepasst -->
<header class="glass-panel">
<h1>uTools <span class="text-sm font-normal text-gray-400 opacity-75 tracking-wider uppercase ml-2">Network
Suite</span></h1>
<nav> <nav>
<ul> <ul>
<li><a href="/">IP Info &amp; Tools</a></li> <li><a href="index.html">IP Info & Tools</a></li> <!-- Angepasst -->
<li><a href="/subnet" class="active-link">Subnetz Rechner</a></li> <li><a href="subnet-calculator.html">Subnetz Rechner</a></li>
<li><a href="/dns">DNS Lookup</a></li> <li><a href="dns-lookup.html">DNS Lookup</a></li> <!-- Neu -->
<li><a href="/whois">WHOIS Lookup</a></li> <li><a href="whois-lookup.html">WHOIS Lookup</a></li> <!-- Neu -->
<li><a href="/mac">MAC Lookup</a></li>
<li><a href="/asn">ASN Lookup</a></li>
</ul> </ul>
</nav> </nav>
</header> </header>
<div <div class="container mx-auto max-w-4xl bg-gray-800 rounded-lg shadow-xl p-6">
class="container mx-auto max-w-5xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h2 class="text-3xl font-bold mb-8 text-center text-gradient glitch-text">IP Subnetz Rechner</h2> <h2 class="text-2xl font-bold mb-6 text-purple-400 text-center">IP Subnetz Rechner</h2>
<form id="subnet-form" class="mb-8 glass-card p-6 rounded-xl"> <form id="subnet-form" class="mb-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div> <div>
<label for="ip-address" <label for="ip-address">IP Adresse:</label>
class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">IP Adresse:</label>
<input type="text" id="ip-address" name="ip-address" placeholder="z.B. 192.168.1.1" required <input type="text" id="ip-address" name="ip-address" placeholder="z.B. 192.168.1.1" required
class="w-full px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all"> 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>
<div> <div>
<label for="cidr" class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">CIDR / <label for="cidr">CIDR / Subnetzmaske:</label>
Maske:</label>
<input type="text" id="cidr" name="cidr" placeholder="z.B. 24 oder 255.255.255.0" required <input type="text" id="cidr" name="cidr" placeholder="z.B. 24 oder 255.255.255.0" required
class="w-full px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all"> 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>
</div> </div>
<button type="submit" <button type="submit"
class="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white font-bold py-3 px-6 rounded-lg shadow-lg hover:shadow-purple-500/25 transition-all duration-200 ease-in-out transform hover:-translate-y-0.5"> class="bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded transition duration-150 ease-in-out">
Berechnen Berechnen
</button> </button>
</form> </form>
<div id="results" class="glass-card rounded-xl p-6 hidden fade-in"> <!-- Ergebnisse initial verstecken --> <div id="results" class="bg-gray-700 rounded p-6 hidden"> <!-- Ergebnisse initial verstecken -->
<h3 <h3 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-2 mb-4">Ergebnisse:</h3>
class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4 flex items-center gap-2"> <div class="space-y-2 text-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" <p><strong>Netzwerkadresse:</strong> <span id="network-address" class="font-mono text-purple-400">-</span></p>
stroke="currentColor"> <p><strong>Broadcast-Adresse:</strong> <span id="broadcast-address" class="font-mono text-purple-400">-</span></p>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <p><strong>Subnetzmaske:</strong> <span id="subnet-mask" class="font-mono text-purple-400">-</span></p>
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> <p><strong>Anzahl der Hosts:</strong> <span id="host-count" class="font-mono text-purple-400">-</span></p>
</svg> <p><strong>Erste Host-Adresse:</strong> <span id="first-host" class="font-mono text-purple-400">-</span></p>
Ergebnisse: <p><strong>Letzte Host-Adresse:</strong> <span id="last-host" class="font-mono text-purple-400">-</span></p>
</h3>
<div class="space-y-3 text-sm">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Netzwerkadresse:</span>
<span id="network-address" class="font-mono text-white font-semibold">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Broadcast-Adresse:</span>
<span id="broadcast-address" class="font-mono text-purple-400 font-semibold">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Subnetzmaske:</span>
<span id="subnet-mask" class="font-mono text-gray-300">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Anzahl der Hosts:</span>
<span id="host-count" class="font-mono text-green-400 font-bold">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Erste Host-Adresse:</span>
<span id="first-host" class="font-mono text-blue-300">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<span class="text-gray-400">Letzte Host-Adresse:</span>
<span id="last-host" class="font-mono text-blue-300">-</span>
</div>
</div> </div>
</div> </div>
<!-- Beispiel-Subnetze --> <!-- Beispiel-Subnetze -->
<div id="examples" class="glass-card rounded-xl p-6 mt-8"> <div id="examples" class="bg-gray-700 rounded p-6 mt-8">
<h3 class="text-lg font-bold text-gray-400 uppercase tracking-wider border-b border-gray-700/50 pb-2 mb-4"> <h3 class="text-xl font-semibold text-purple-300 border-b border-purple-500 pb-2 mb-4">Beispiel-Subnetze (Private Adressbereiche)</h3>
Beispiel-Subnetze (Private Adressbereiche)</h3>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full text-sm text-left text-gray-400"> <table class="min-w-full text-sm">
<thead class="text-xs uppercase bg-gray-800/50 text-gray-200"> <thead>
<tr> <tr>
<th scope="col" class="px-6 py-3">Bereich</th> <th>Bereich</th>
<th scope="col" class="px-6 py-3">CIDR</th> <th>CIDR</th>
<th scope="col" class="px-6 py-3">Subnetzmaske</th> <th>Subnetzmaske</th>
<th scope="col" class="px-6 py-3">Beschreibung</th> <th>Beschreibung</th>
<th scope="col" class="px-6 py-3">Aktion</th> <th>Aktion</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-700/50"> <tbody class="divide-y divide-gray-600">
<tr class="hover:bg-gray-700/30 transition-colors"> <tr>
<td class="px-6 py-4 font-mono text-white">192.168.0.0 - 192.168.255.255</td> <td><code>192.168.0.0 - 192.168.255.255</code></td>
<td class="px-6 py-4 font-mono">/16 (Gesamt)</td> <td><code>/16</code> (Gesamt)</td>
<td class="px-6 py-4 font-mono">255.255.0.0</td> <td><code>255.255.0.0</code></td>
<td class="px-6 py-4">Klasse C (oft als /24 genutzt)</td> <td>Klasse C (oft als /24 genutzt)</td>
<td class="px-6 py-4"><span <td><span class="example-link" data-ip="192.168.1.1" data-cidr="24">Beispiel /24</span></td>
class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline"
data-ip="192.168.1.1" data-cidr="24">Beispiel /24</span></td>
</tr> </tr>
<tr class="hover:bg-gray-700/30 transition-colors"> <tr>
<td class="px-6 py-4 font-mono text-white">172.16.0.0 - 172.31.255.255</td> <td><code>172.16.0.0 - 172.31.255.255</code></td>
<td class="px-6 py-4 font-mono">/12 (Gesamt)</td> <td><code>/12</code> (Gesamt)</td>
<td class="px-6 py-4 font-mono">255.240.0.0</td> <td><code>255.240.0.0</code></td>
<td class="px-6 py-4">Klasse B</td> <td>Klasse B</td>
<td class="px-6 py-4"><span <td><span class="example-link" data-ip="172.16.10.5" data-cidr="16">Beispiel /16</span></td>
class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline"
data-ip="172.16.10.5" data-cidr="16">Beispiel /16</span></td>
</tr> </tr>
<tr class="hover:bg-gray-700/30 transition-colors"> <tr>
<td class="px-6 py-4 font-mono text-white">10.0.0.0 - 10.255.255.255</td> <td><code>10.0.0.0 - 10.255.255.255</code></td>
<td class="px-6 py-4 font-mono">/8 (Gesamt)</td> <td><code>/8</code> (Gesamt)</td>
<td class="px-6 py-4 font-mono">255.0.0.0</td> <td><code>255.0.0.0</code></td>
<td class="px-6 py-4">Klasse A</td> <td>Klasse A</td>
<td class="px-6 py-4"><span <td><span class="example-link" data-ip="10.0.50.100" data-cidr="8">Beispiel /8</span></td>
class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline"
data-ip="10.0.50.100" data-cidr="8">Beispiel /8</span></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<p class="mt-4 text-xs text-gray-500 italic">Klicken Sie auf "Beispiel", um die Felder oben auszufüllen und <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>
die Berechnung zu starten.</p>
</div> </div>
<!-- Globaler Fehlerbereich --> <!-- Globaler Fehlerbereich -->
<div id="global-error" <div id="global-error" class="mt-6 p-4 bg-red-800 text-red-100 rounded hidden"></div>
class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg">
</div>
</div> </div>
<footer class="mt-12 pt-6 border-t border-gray-700/30 text-center text-xs text-gray-500"> <footer class="mt-8 pt-4 border-t border-gray-600 text-center text-xs text-gray-500">
<p>&copy; 2025 <a href="https://mrunk.de" <p>&copy; 2025 <a href="https://johanneskr.de" class="text-purple-400 hover:underline">Johannes Krüger</a></p>
class="text-purple-400 hover:text-purple-300 transition-colors">MrUnknownDE</a></p> <p>Version: <span id="commit-sha" class="font-mono">loading...</span></p> <!-- Footer mit Version hinzugefügt -->
<p class="mt-1">Version: <span id="commit-sha" class="font-mono text-gray-400">loading...</span></p>
<!-- Footer mit Version hinzugefügt -->
</footer> </footer>
<!-- Nur das Skript für den Rechner laden --> <!-- Nur das Skript für den Rechner laden -->
@@ -348,5 +256,4 @@
}); });
</script> </script>
</body> </body>
</html> </html>
+39 -182
View File
@@ -1,6 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -9,225 +8,84 @@
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<!-- Eigene Styles --> <!-- Eigene Styles -->
<style> <style>
/* Einfacher Lade-Spinner (Tailwind animiert) */ /* Einfacher Lade-Spinner */
.loader { .loader {
border: 4px solid rgba(168, 85, 247, 0.1); border: 4px solid rgba(168, 85, 247, 0.3); /* Lila transparent */
/* Lila sehr transparent */ border-left-color: #a855f7; /* Lila */
border-left-color: #d8b4fe;
/* Helleres Lila */
border-radius: 50%; border-radius: 50%;
width: 24px; width: 24px;
height: 24px; height: 24px;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
@keyframes spin { @keyframes spin {
to { to { transform: rotate(360deg); }
transform: rotate(360deg);
} }
}
/* Glassmorphism Utilities */
.glass-panel {
background: rgba(17, 24, 39, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-card {
background: rgba(31, 41, 55, 0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* Ergebnis-Pre-Formatierung */ /* Ergebnis-Pre-Formatierung */
.result-pre { .result-pre {
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
font-family: 'Courier New', Courier, monospace; font-family: monospace;
background-color: rgba(0, 0, 0, 0.3); background-color: #1f2937; /* Dunkelgrau */
color: #e5e7eb; color: #d1d5db; /* Hellgrau */
padding: 1rem; padding: 1rem;
border-radius: 0.375rem; border-radius: 0.375rem; /* rounded-md */
max-height: 600px; max-height: 600px; /* Mehr Höhe für WHOIS */
overflow-y: auto; overflow-y: auto;
font-size: 0.875rem; font-size: 0.875rem; /* text-sm */
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.3);
} }
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* Navigations-Styling */ /* Navigations-Styling */
nav ul { nav ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 1rem; }
list-style: none; nav a { color: #c4b5fd; text-decoration: none; white-space: nowrap; }
padding: 0; nav a:hover { color: #a78bfa; text-decoration: underline; }
margin: 0; 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; }
display: flex; @media (min-width: 768px) { header { flex-direction: row; justify-content: space-between; } }
flex-wrap: wrap; header h1 { font-size: 1.5rem; font-weight: bold; color: #e5e7eb; }
gap: 0.5rem; .hidden { display: none; }
}
nav a {
color: #d1d5db;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
nav a:hover {
color: #fff;
background: rgba(168, 85, 247, 0.2);
border-color: rgba(168, 85, 247, 0.4);
box-shadow: 0 0 15px rgba(168, 85, 247, 0.1);
}
nav a.active-link {
background: rgba(168, 85, 247, 0.3);
color: #fff;
border-color: #a855f7;
}
header {
background: rgba(31, 41, 55, 0.4);
backdrop-filter: blur(10px);
padding: 1.5rem;
margin-bottom: 2rem;
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
header {
flex-direction: row;
justify-content: space-between;
}
}
header h1 {
background-clip: text;
}
.text-gradient {
background: linear-gradient(to right, #c084fc, #e879f9);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hidden {
display: none !important;
}
</style> </style>
</head> </head>
<body class="bg-gray-900 text-gray-200 font-sans p-4 md:p-8">
<body <header>
class="bg-gray-950 text-gray-100 font-sans p-4 md:p-8 min-h-screen bg-[url('https://tailwindcss.com/_next/static/media/hero-dark.939eb757.png')] bg-cover bg-center bg-fixed selection:bg-purple-500 selection:text-white"> <h1>uTools Network Suite</h1>
<header class="glass-panel">
<h1>uTools <span class="text-sm font-normal text-gray-400 opacity-75 tracking-wider uppercase ml-2">Network
Suite</span></h1>
<nav> <nav>
<ul> <ul>
<li><a href="/">IP Info &amp; Tools</a></li> <li><a href="index.html">IP Info & Tools</a></li>
<li><a href="/subnet">Subnetz Rechner</a></li> <li><a href="subnet-calculator.html">Subnetz Rechner</a></li>
<li><a href="/dns">DNS Lookup</a></li> <li><a href="dns-lookup.html">DNS Lookup</a></li>
<li><a href="/whois" class="active-link">WHOIS Lookup</a></li> <li><a href="whois-lookup.html">WHOIS Lookup</a></li>
<li><a href="/mac">MAC Lookup</a></li>
<li><a href="/asn">ASN Lookup</a></li>
</ul> </ul>
</nav> </nav>
</header> </header>
<div <div class="container mx-auto max-w-4xl bg-gray-800 rounded-lg shadow-xl p-6">
class="container mx-auto max-w-5xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h1 class="text-3xl font-bold mb-8 text-center text-gradient glitch-text">WHOIS Lookup</h1> <h1 class="text-3xl font-bold mb-6 text-purple-400 text-center">WHOIS Lookup</h1>
<!-- Bereich für WHOIS Lookup --> <!-- Bereich für WHOIS Lookup -->
<div class="mt-8 p-6 glass-card rounded-xl"> <div class="mt-8 p-4 bg-gray-700 rounded">
<div class="flex flex-col sm:flex-row gap-3 mb-6"> <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)" <input type="text" id="whois-query-input" placeholder="Enter domain or IP (e.g., google.com or 8.8.8.8)"
class="flex-grow px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all placeholder-gray-600"> 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" <button id="whois-lookup-button"
class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white font-bold py-3 px-6 rounded-lg shadow-lg hover:shadow-purple-500/25 transition-all duration-200 ease-in-out transform hover:-translate-y-0.5"> 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 Lookup WHOIS
</button> </button>
</div> </div>
<div id="whois-lookup-error" <div id="whois-lookup-error" class="text-red-400 mb-4 hidden"></div>
class="text-red-400 mb-4 hidden p-3 bg-red-900/20 border border-red-500/30 rounded"></div> <div id="whois-lookup-results-section" class="hidden mt-4 border-t border-gray-600 pt-4">
<div id="whois-lookup-results-section" class="hidden mt-6 border-t border-gray-700/50 pt-6 fade-in"> <h3 class="text-lg font-semibold text-purple-300 mb-2">WHOIS Results for: <span id="whois-lookup-query" class="font-mono text-purple-400"></span></h3>
<h3 class="text-lg font-semibold text-purple-300 mb-4 flex items-center gap-2"> <div id="whois-lookup-loader" class="loader hidden mb-2"></div>
<div class="w-1.5 h-6 bg-purple-500 rounded-full"></div> <pre id="whois-lookup-output" class="result-pre"></pre> <!-- Ergebnisbereich -->
WHOIS Results for: <span id="whois-lookup-query" class="font-mono text-purple-400 ml-1"></span>
</h3>
<div id="whois-lookup-loader" class="loader hidden mb-4"></div>
<pre id="whois-lookup-output" class="result-pre custom-scrollbar"></pre> <!-- Ergebnisbereich -->
</div> </div>
</div> </div>
<!-- Globaler Fehlerbereich --> <!-- Globaler Fehlerbereich -->
<div id="global-error" <div id="global-error" class="mt-6 p-4 bg-red-800 text-red-100 rounded hidden"></div>
class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg">
</div>
<!-- Footer für Version --> <!-- Footer für Version -->
<footer class="mt-12 pt-6 border-t border-gray-700/30 text-center text-xs text-gray-500"> <footer class="mt-8 pt-4 border-t border-gray-600 text-center text-xs text-gray-500">
<p>&copy; 2025 <a href="https://mrunk.de" <p>&copy; 2025 <a href="https://johanneskr.de" class="text-purple-400 hover:underline">Johannes Krüger</a></p>
class="text-purple-400 hover:text-purple-300 transition-colors">MrUnknownDE</a></p> <p>Version: <span id="commit-sha" class="font-mono">loading...</span></p>
<p class="mt-1">Version: <span id="commit-sha" class="font-mono text-gray-400">loading...</span></p>
</footer> </footer>
</div> </div>
@@ -235,5 +93,4 @@
<!-- Eigene JS-Logik für diese Seite --> <!-- Eigene JS-Logik für diese Seite -->
<script src="whois-lookup.js"></script> <script src="whois-lookup.js"></script>
</body> </body>
</html> </html>
-5
View File
@@ -43,11 +43,6 @@ document.addEventListener('DOMContentLoaded', () => {
* @param {function} displayFn - Funktion zur Formatierung und Anzeige der Daten im outputElement. * @param {function} displayFn - Funktion zur Formatierung und Anzeige der Daten im outputElement.
*/ */
async function fetchAndDisplay(endpoint, params, resultsSection, loaderElement, errorElement, queryElement, outputElement, displayFn) { async function fetchAndDisplay(endpoint, params, resultsSection, loaderElement, errorElement, queryElement, outputElement, displayFn) {
// Reset animation
resultsSection.classList.remove('fade-in');
void resultsSection.offsetWidth; // Trigger reflow
resultsSection.classList.add('fade-in');
resultsSection.classList.remove('hidden'); resultsSection.classList.remove('hidden');
loaderElement.classList.remove('hidden'); loaderElement.classList.remove('hidden');
errorElement.classList.add('hidden'); errorElement.classList.add('hidden');
+1 -14
View File
@@ -10,22 +10,9 @@ server {
access_log /var/log/nginx/access.log; access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log; error_log /var/log/nginx/error.log;
# Clean URL rewrites - Map short URLs to actual HTML files
rewrite ^/dns$ /dns-lookup.html last;
rewrite ^/dns-lookup$ /dns-lookup.html last;
rewrite ^/whois$ /whois-lookup.html last;
rewrite ^/whois-lookup$ /whois-lookup.html last;
rewrite ^/mac$ /mac-lookup.html last;
rewrite ^/mac-lookup$ /mac-lookup.html last;
rewrite ^/subnet$ /subnet-calculator.html last;
rewrite ^/subnet-calculator$ /subnet-calculator.html last;
rewrite ^/asn$ /asn-lookup.html last;
rewrite ^/asn-lookup$ /asn-lookup.html last;
# Statische Dateien direkt ausliefern # Statische Dateien direkt ausliefern
location / { location / {
# First try the exact URI, then with .html, then fall back to index.html try_files $uri $uri/ /index.html; # Wichtig für Single-Page-Apps (auch wenn wir keine sind)
try_files $uri $uri.html $uri/ /index.html;
} }
# API-Anfragen an den Backend-Service weiterleiten # API-Anfragen an den Backend-Service weiterleiten
+42
View File
@@ -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;