make docker grate again ^^

This commit is contained in:
2025-03-29 12:01:58 +01:00
parent 654df54fa7
commit 4535631e9f
6 changed files with 679 additions and 128 deletions

7
backend/.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
.git
.env
node_modules
npm-debug.log
Dockerfile
.dockerignore
*.md

62
backend/Dockerfile Normal file
View File

@@ -0,0 +1,62 @@
# 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)
COPY package*.json ./
# Install app dependencies using npm ci for faster, reliable builds
# --only=production installs only production dependencies
RUN npm ci --only=production
# 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
# Run the app when the container launches
CMD ["node", "server.js"]

View File

@@ -1,4 +1,7 @@
# .env # .env
GEOIP_CITY_DB=./data/GeoLite2-City.mmdb GEOIP_CITY_DB=./data/GeoLite2-City.mmdb
GEOIP_ASN_DB=./data/GeoLite2-ASN.mmdb GEOIP_ASN_DB=./data/GeoLite2-ASN.mmdb
PORT=3000 PORT=3000
LOG_LEVEL=debug # z.B. für mehr Details im Development
PING_COUNT=4
# NODE_ENV=development # Setze dies ggf. für pino-pretty

View File

@@ -12,7 +12,10 @@
"@maxmind/geoip2-node": "^6.0.0", "@maxmind/geoip2-node": "^6.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",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0"
} }
}, },
"node_modules/@maxmind/geoip2-node": { "node_modules/@maxmind/geoip2-node": {
@@ -40,6 +43,14 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
}, },
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.3", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -98,6 +109,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -142,6 +158,14 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/dateformat": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
"engines": {
"node": "*"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -204,6 +228,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -289,6 +321,38 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-rate-limit": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
"integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": "^4.11 || 5 || ^5.0.0-beta.1"
}
},
"node_modules/fast-copy": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
"integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="
},
"node_modules/fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
},
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
@@ -398,6 +462,11 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -437,6 +506,14 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
"engines": {
"node": ">=10"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -512,6 +589,14 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mmdb-lib": { "node_modules/mmdb-lib": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-2.1.1.tgz", "resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-2.1.1.tgz",
@@ -553,6 +638,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/on-finished": { "node_modules/on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -564,6 +657,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/parseurl": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -577,6 +678,78 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
}, },
"node_modules/pino": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz",
"integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==",
"dependencies": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^4.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^3.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pino-pretty": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz",
"integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==",
"dependencies": {
"colorette": "^2.0.7",
"dateformat": "^4.6.3",
"fast-copy": "^3.0.2",
"fast-safe-stringify": "^2.1.1",
"help-me": "^5.0.0",
"joycon": "^3.1.1",
"minimist": "^1.2.6",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pump": "^3.0.0",
"secure-json-parse": "^2.4.0",
"sonic-boom": "^4.0.1",
"strip-json-comments": "^3.1.1"
},
"bin": {
"pino-pretty": "bin.js"
}
},
"node_modules/pino-std-serializers": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="
},
"node_modules/process-warning": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz",
"integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
]
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -589,6 +762,15 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.13.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@@ -603,6 +785,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -625,6 +812,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -644,11 +839,24 @@
} }
] ]
}, },
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"node_modules/secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="
},
"node_modules/send": { "node_modules/send": {
"version": "0.19.0", "version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
@@ -772,6 +980,22 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/sonic-boom": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/statuses": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -780,6 +1004,25 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"dependencies": {
"real-require": "^0.2.0"
}
},
"node_modules/tiny-lru": { "node_modules/tiny-lru": {
"version": "11.2.11", "version": "11.2.11",
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.2.11.tgz", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.2.11.tgz",
@@ -831,6 +1074,11 @@
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
} }
} }
} }

View File

@@ -13,6 +13,9 @@
"@maxmind/geoip2-node": "^6.0.0", "@maxmind/geoip2-node": "^6.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",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0"
} }
} }

View File

@@ -1,11 +1,22 @@
// server.js // server.js
require('dotenv').config(); require('dotenv').config(); // Lädt Variablen aus .env in process.env
const express = require('express'); const express = require('express');
const cors = require('cors'); const cors = require('cors');
const geoip = require('@maxmind/geoip2-node'); const geoip = require('@maxmind/geoip2-node');
const net = require('net'); // Node.js built-in module for IP validation const net = require('net'); // Node.js built-in module for IP validation
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const dns = require('dns').promises; const dns = require('dns').promises;
const pino = require('pino'); // Logging library
const rateLimit = require('express-rate-limit'); // Rate limiting middleware
// --- Logger Initialisierung ---
const logger = pino({
level: process.env.LOG_LEVEL || 'info', // Konfigurierbares Log-Level (z.B. 'debug', 'info', 'warn', 'error')
// Pretty print nur im Development, sonst JSON für bessere Maschinenlesbarkeit
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:standard', ignore: 'pid,hostname' } }
: undefined,
});
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
@@ -22,21 +33,45 @@ let asnReader;
* @returns {boolean} True, wenn gültig (als v4 oder v6), sonst false. * @returns {boolean} True, wenn gültig (als v4 oder v6), sonst false.
*/ */
function isValidIp(ip) { function isValidIp(ip) {
// Frühe Prüfung auf offensichtlich ungültige Werte
if (!ip || typeof ip !== 'string' || ip.trim() === '') { if (!ip || typeof ip !== 'string' || ip.trim() === '') {
// console.log(`isValidIp (net): Input invalid`); // Optional Debugging
return false; return false;
} }
const trimmedIp = ip.trim(); const trimmedIp = ip.trim();
const ipVersion = net.isIP(trimmedIp); // Gibt 0, 4 oder 6 zurück
// net.isIP(trimmedIp) gibt 0 zurück, wenn ungültig, 4 für IPv4, 6 für IPv6. // logger.debug({ ip: trimmedIp, version: ipVersion }, 'isValidIp check'); // Optional: Debug log
const ipVersion = net.isIP(trimmedIp);
// console.log(`isValidIp (net): net.isIP check for "${trimmedIp}": Version ${ipVersion}`); // Optional Debugging
return ipVersion === 4 || ipVersion === 6; return ipVersion === 4 || ipVersion === 6;
} }
/**
* Prüft, ob eine IP-Adresse im privaten, Loopback- oder Link-Local-Bereich liegt.
* @param {string} ip - Die zu prüfende IP-Adresse (bereits validiert).
* @returns {boolean} True, wenn die IP privat/lokal ist, sonst false.
*/
function isPrivateIp(ip) {
if (!ip) return false;
const ipVersion = net.isIP(ip);
if (ipVersion === 4) {
const parts = ip.split('.').map(Number);
return (
parts[0] === 10 || // 10.0.0.0/8
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || // 172.16.0.0/12
(parts[0] === 192 && parts[1] === 168) || // 192.168.0.0/16
parts[0] === 127 || // 127.0.0.0/8 (Loopback)
(parts[0] === 169 && parts[1] === 254) // 169.254.0.0/16 (Link-local)
);
} else if (ipVersion === 6) {
const lowerCaseIp = ip.toLowerCase();
return (
lowerCaseIp === '::1' || // ::1/128 (Loopback)
lowerCaseIp.startsWith('fc') || lowerCaseIp.startsWith('fd') || // fc00::/7 (Unique Local)
lowerCaseIp.startsWith('fe8') || lowerCaseIp.startsWith('fe9') || // fe80::/10 (Link-local)
lowerCaseIp.startsWith('fea') || lowerCaseIp.startsWith('feb')
);
}
return false;
}
/** /**
* 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).
@@ -45,36 +80,31 @@ function isValidIp(ip) {
* @returns {string} Die bereinigte IP-Adresse. * @returns {string} Die bereinigte IP-Adresse.
*/ */
function getCleanIp(ip) { function getCleanIp(ip) {
if (!ip) return ip; // Handle null/undefined case if (!ip) return ip;
const trimmedIp = ip.trim();
const trimmedIp = ip.trim(); // Trimmen für Konsistenz
if (trimmedIp.startsWith('::ffff:')) { if (trimmedIp.startsWith('::ffff:')) {
const potentialIp4 = trimmedIp.substring(7); const potentialIp4 = trimmedIp.substring(7);
// Prüfen, ob der extrahierte Teil eine gültige IPv4 ist
if (net.isIP(potentialIp4) === 4) { if (net.isIP(potentialIp4) === 4) {
return potentialIp4; return potentialIp4;
} }
} }
// Handle localhost cases for testing
if (trimmedIp === '::1' || trimmedIp === '127.0.0.1') { if (trimmedIp === '::1' || trimmedIp === '127.0.0.1') {
return trimmedIp; return trimmedIp;
} }
return trimmedIp; // Gib die getrimmte IP zurück return trimmedIp;
} }
/** /**
* Führt einen Shell-Befehl sicher aus und gibt stdout zurück. * Führt einen Shell-Befehl sicher aus und gibt stdout zurück. (Nur für Ping verwendet)
* @param {string} command - Der Befehl (z.B. 'ping'). * @param {string} command - Der Befehl (z.B. 'ping').
* @param {string[]} args - Die Argumente als Array. * @param {string[]} args - Die Argumente als Array.
* @returns {Promise<string>} Eine Promise, die mit stdout aufgelöst wird. * @returns {Promise<string>} Eine Promise, die mit stdout aufgelöst wird.
*/ */
function executeCommand(command, args) { function executeCommand(command, args) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Argumenten-Validierung (einfach)
args.forEach(arg => { args.forEach(arg => {
if (typeof arg === 'string' && /[;&|`$()<>]/.test(arg)) { if (typeof arg === 'string' && /[;&|`$()<>]/.test(arg)) {
console.error(`Potential command injection attempt detected in argument: ${arg}`); logger.error({ command, arg }, "Potential command injection attempt detected in argument");
return reject(new Error(`Invalid character detected in command argument.`)); return reject(new Error(`Invalid character detected in command argument.`));
} }
}); });
@@ -86,12 +116,12 @@ function executeCommand(command, args) {
proc.stdout.on('data', (data) => { stdout += data.toString(); }); proc.stdout.on('data', (data) => { stdout += data.toString(); });
proc.stderr.on('data', (data) => { stderr += data.toString(); }); proc.stderr.on('data', (data) => { stderr += data.toString(); });
proc.on('error', (err) => { proc.on('error', (err) => {
console.error(`Failed to start command ${command}: ${err.message}`); logger.error({ command, args, error: err.message }, `Failed to start command`);
reject(new Error(`Failed to start command ${command}: ${err.message}`)); reject(new Error(`Failed to start command ${command}: ${err.message}`));
}); });
proc.on('close', (code) => { proc.on('close', (code) => {
if (code !== 0) { if (code !== 0) {
console.error(`Command ${command} ${args.join(' ')} failed with code ${code}: ${stderr || stdout}`); logger.error({ command, args, exitCode: code, stderr: stderr.trim(), stdout: stdout.trim() }, `Command failed`);
reject(new Error(`Command ${command} failed with code ${code}: ${stderr || 'No stderr output'}`)); reject(new Error(`Command ${command} failed with code ${code}: ${stderr || 'No stderr output'}`));
} else { } else {
resolve(stdout); resolve(stdout);
@@ -100,84 +130,167 @@ function executeCommand(command, args) {
}); });
} }
/**
* Parst die Ausgabe des Linux/macOS ping Befehls.
* @param {string} pingOutput - Die rohe stdout Ausgabe von ping.
* @returns {object} Ein Objekt mit geparsten Daten oder Fehlern.
*/
function parsePingOutput(pingOutput) {
const result = {
rawOutput: pingOutput,
stats: null,
error: null,
};
try {
let packetsTransmitted = 0;
let packetsReceived = 0;
let packetLossPercent = 100;
let rtt = { min: null, avg: null, max: null, mdev: null };
const lines = pingOutput.trim().split('\n');
const statsLine = lines.find(line => line.includes('packets transmitted'));
if (statsLine) {
const transmittedMatch = statsLine.match(/(\d+)\s+packets transmitted/);
const receivedMatch = statsLine.match(/(\d+)\s+(?:received|packets received)/); // Anpassung für Varianten
const lossMatch = statsLine.match(/([\d.]+)%\s+packet loss/);
if (transmittedMatch) packetsTransmitted = parseInt(transmittedMatch[1], 10);
if (receivedMatch) packetsReceived = parseInt(receivedMatch[1], 10);
if (lossMatch) packetLossPercent = parseFloat(lossMatch[1]);
}
const rttLine = lines.find(line => line.startsWith('rtt min/avg/max/mdev') || line.startsWith('round-trip min/avg/max/stddev'));
if (rttLine) {
const rttMatch = rttLine.match(/([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/);
if (rttMatch) {
rtt = {
min: parseFloat(rttMatch[1]),
avg: parseFloat(rttMatch[2]),
max: parseFloat(rttMatch[3]),
mdev: parseFloat(rttMatch[4]),
};
}
}
result.stats = {
packets: { transmitted: packetsTransmitted, received: packetsReceived, lossPercent: packetLossPercent },
rtt: rtt.avg !== null ? rtt : null,
};
if (packetsTransmitted > 0 && rtt.avg === null && packetsReceived === 0) {
result.error = "Request timed out or host unreachable."; // Spezifischer Fehler bei Totalausfall
}
} catch (parseError) {
logger.error({ error: parseError.message, output: pingOutput }, "Failed to parse ping output");
result.error = "Failed to parse ping output.";
}
return result;
}
/** /**
* Prüft, ob eine IP-Adresse im privaten, Loopback- oder Link-Local-Bereich liegt. * Parst eine einzelne Zeile der Linux/macOS traceroute Ausgabe.
* @param {string} ip - Die zu prüfende IP-Adresse (bereits validiert). * @param {string} line - Eine Zeile aus stdout.
* @returns {boolean} True, wenn die IP privat/lokal ist, sonst false. * @returns {object | null} Ein Objekt mit Hop-Daten oder null bei uninteressanten Zeilen.
*/ */
function isPrivateIp(ip) { function parseTracerouteLine(line) {
if (!ip) return false; // Sollte durch isValidIp vorher abgefangen werden line = line.trim();
if (!line || line.startsWith('traceroute to')) return null; // Ignoriere Header
const ipVersion = net.isIP(ip); // Gibt 4 oder 6 zurück // Regex angepasst für mehr Robustheit (optionaler Hostname, IP immer da, RTTs oder *)
const hopMatch = line.match(/^(\s*\d+)\s+(?:([a-zA-Z0-9\.\-]+)\s+\(([\d\.:a-fA-F]+)\)|([\d\.:a-fA-F]+))\s+(.*)$/);
const timeoutMatch = line.match(/^(\s*\d+)\s+(\*\s+\*\s+\*)/);
if (ipVersion === 4) { if (timeoutMatch) {
const parts = ip.split('.').map(Number); return {
return ( hop: parseInt(timeoutMatch[1].trim(), 10),
// 10.0.0.0/8 hostname: null,
parts[0] === 10 || ip: null,
// 172.16.0.0/12 rtt: ['*', '*', '*'],
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || rawLine: line,
// 192.168.0.0/16 };
(parts[0] === 192 && parts[1] === 168) || } else if (hopMatch) {
// 127.0.0.0/8 (Loopback) const hop = parseInt(hopMatch[1].trim(), 10);
parts[0] === 127 || const hostname = hopMatch[2]; // Kann undefined sein
// 169.254.0.0/16 (Link-local) const ipInParen = hopMatch[3]; // Kann undefined sein
(parts[0] === 169 && parts[1] === 254) const ipDirect = hopMatch[4]; // Kann undefined sein
); const restOfLine = hopMatch[5].trim();
} else if (ipVersion === 6) {
const lowerCaseIp = ip.toLowerCase(); const ip = ipInParen || ipDirect;
return (
// ::1/128 (Loopback) // Extrahiere RTTs (können * sein oder Zahl mit " ms")
lowerCaseIp === '::1' || const rttParts = restOfLine.split(/\s+/);
// fc00::/7 (Unique Local Addresses) const rtts = rttParts.map(p => p === '*' ? '*' : p.replace(/\s*ms$/, '')).filter(p => p === '*' || !isNaN(parseFloat(p))).slice(0, 3);
lowerCaseIp.startsWith('fc') || lowerCaseIp.startsWith('fd') || // Fülle fehlende RTTs mit '*' auf, falls weniger als 3 gefunden wurden
// fe80::/10 (Link-local) while (rtts.length < 3) rtts.push('*');
lowerCaseIp.startsWith('fe8') || lowerCaseIp.startsWith('fe9') ||
lowerCaseIp.startsWith('fea') || lowerCaseIp.startsWith('feb') return {
); hop: hop,
hostname: hostname || null, // Setze null, wenn kein Hostname gefunden
ip: ip,
rtt: rtts,
rawLine: line,
};
} }
// logger.debug({ line }, "Unparsed traceroute line"); // Optional: Log unparsed lines
// Wenn net.isIP 0 zurückgibt (sollte nicht passieren nach isValidIp) return null; // Nicht als Hop-Zeile erkannt
return false;
} }
// --- Initialisierung (MaxMind DBs laden) --- // --- Initialisierung (MaxMind DBs laden) ---
async function initialize() { async function initialize() {
try { try {
console.log('Loading MaxMind databases...'); logger.info('Loading MaxMind databases...');
const cityDbPath = process.env.GEOIP_CITY_DB || './data/GeoLite2-City.mmdb'; const cityDbPath = process.env.GEOIP_CITY_DB || './data/GeoLite2-City.mmdb';
const asnDbPath = process.env.GEOIP_ASN_DB || './data/GeoLite2-ASN.mmdb'; const asnDbPath = process.env.GEOIP_ASN_DB || './data/GeoLite2-ASN.mmdb';
console.log(`City DB Path: ${cityDbPath}`); logger.info({ cityDbPath, asnDbPath }, 'Database paths');
console.log(`ASN DB Path: ${asnDbPath}`);
cityReader = await geoip.Reader.open(cityDbPath); cityReader = await geoip.Reader.open(cityDbPath);
asnReader = await geoip.Reader.open(asnDbPath); asnReader = await geoip.Reader.open(asnDbPath);
console.log('MaxMind databases loaded successfully.'); logger.info('MaxMind databases loaded successfully.');
} catch (error) { } catch (error) {
console.error('FATAL: Could not load MaxMind databases.'); logger.fatal({ error: error.message, stack: error.stack }, 'Could not load MaxMind databases. Exiting.');
console.error('Ensure GEOIP_CITY_DB and GEOIP_ASN_DB point to valid .mmdb files in the ./data directory or via .env');
console.error(error);
process.exit(1); process.exit(1);
} }
} }
// --- Middleware --- // --- Middleware ---
app.use(cors()); app.use(cors()); // Erlaubt Anfragen von anderen Origins
app.use(express.json()); app.use(express.json()); // Parst JSON-Request-Bodies
// app.set('trust proxy', true); // Nur aktivieren, wenn nötig und korrekt konfiguriert
// Vertraue Proxy-Headern (vorsichtig verwenden!)
// app.set('trust proxy', 1); // Vertraue dem ersten Proxy (z.B. Nginx, Load Balancer)
// Rate Limiter
const diagnosticLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 Minuten
max: process.env.NODE_ENV === 'production' ? 10 : 100, // Mehr Anfragen im Dev erlauben
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many diagnostic requests (ping/traceroute) from this IP, please try again after 5 minutes' },
keyGenerator: (req, res) => req.ip || req.socket.remoteAddress, // IP des Clients als Schlüssel
handler: (req, res, next, options) => {
logger.warn({ ip: req.ip || req.socket.remoteAddress, route: req.originalUrl }, 'Rate limit exceeded');
res.status(options.statusCode).send(options.message);
}
});
// Wende Limiter nur auf Ping und Traceroute an
app.use('/api/ping', diagnosticLimiter);
app.use('/api/traceroute', diagnosticLimiter);
// --- Routen --- // --- Routen ---
// Haupt-Endpunkt: Liefert alle Infos zur IP des Clients // Haupt-Endpunkt: Liefert alle Infos zur IP des Clients
app.get('/api/ipinfo', async (req, res) => { app.get('/api/ipinfo', async (req, res) => {
console.log(`ipinfo request: req.ip = ${req.ip}, req.socket.remoteAddress = ${req.socket.remoteAddress}`); const requestIp = req.ip || req.socket.remoteAddress; // req.ip berücksichtigt 'trust proxy'
const clientIpRaw = req.ip || req.socket.remoteAddress; logger.info({ ip: requestIp, method: req.method, url: req.originalUrl }, 'ipinfo request received');
const clientIp = getCleanIp(clientIpRaw); // Verwendet jetzt die neue getCleanIp
console.log(`ipinfo: Raw IP = ${clientIpRaw}, Cleaned IP = ${clientIp}`); const clientIp = getCleanIp(requestIp);
logger.debug({ rawIp: requestIp, cleanedIp: clientIp }, 'IP cleaning result');
if (!clientIp || !isValidIp(clientIp)) { // Verwendet jetzt die neue isValidIp if (!clientIp || !isValidIp(clientIp)) {
if (clientIp === '127.0.0.1' || clientIp === '::1') { if (clientIp === '127.0.0.1' || clientIp === '::1') {
logger.info({ ip: clientIp }, 'Responding with localhost info');
return res.json({ return res.json({
ip: clientIp, ip: clientIp,
geo: { note: 'Localhost IP, no Geo data available.' }, geo: { note: 'Localhost IP, no Geo data available.' },
@@ -185,36 +298,53 @@ app.get('/api/ipinfo', async (req, res) => {
rdns: ['localhost'], rdns: ['localhost'],
}); });
} }
console.error(`ipinfo: Could not determine a valid client IP. Raw: ${clientIpRaw}, Cleaned: ${clientIp}`); logger.error({ rawIp: requestIp, cleanedIp: clientIp }, 'Could not determine a valid client IP');
return res.status(400).json({ error: 'Could not determine a valid client IP address.', rawIp: clientIpRaw, cleanedIp: clientIp }); return res.status(400).json({ error: 'Could not determine a valid client IP address.', rawIp: requestIp, cleanedIp: clientIp });
} }
try { try {
let geo = null; let geo = null;
try { try {
const geoData = cityReader.city(clientIp); const geoData = cityReader.city(clientIp);
geo = { /* ... Geo-Daten wie zuvor ... */ }; geo = {
city: geoData.city?.names?.en,
region: geoData.subdivisions?.[0]?.isoCode,
country: geoData.country?.isoCode,
countryName: geoData.country?.names?.en,
postalCode: geoData.postal?.code,
latitude: geoData.location?.latitude,
longitude: geoData.location?.longitude,
timezone: geoData.location?.timeZone,
};
logger.debug({ ip: clientIp, geo }, 'GeoIP lookup successful');
} catch (e) { } catch (e) {
console.warn(`ipinfo: MaxMind City lookup failed for ${clientIp}: ${e.message}`); logger.warn({ ip: clientIp, error: e.message }, `MaxMind City lookup failed`);
geo = { error: 'GeoIP lookup failed.' }; geo = { error: 'GeoIP lookup failed (IP not found in database or private range).' };
} }
let asn = null; let asn = null;
try { try {
const asnData = asnReader.asn(clientIp); const asnData = asnReader.asn(clientIp);
asn = { /* ... ASN-Daten wie zuvor ... */ }; asn = {
number: asnData.autonomousSystemNumber,
organization: asnData.autonomousSystemOrganization,
};
logger.debug({ ip: clientIp, asn }, 'ASN lookup successful');
} catch (e) { } catch (e) {
console.warn(`ipinfo: MaxMind ASN lookup failed for ${clientIp}: ${e.message}`); logger.warn({ ip: clientIp, error: e.message }, `MaxMind ASN lookup failed`);
asn = { error: 'ASN lookup failed.' }; asn = { error: 'ASN lookup failed (IP not found in database or private range).' };
} }
let rdns = null; let rdns = null;
try { try {
const hostnames = await dns.reverse(clientIp); const hostnames = await dns.reverse(clientIp);
rdns = hostnames; rdns = hostnames;
logger.debug({ ip: clientIp, rdns }, 'rDNS lookup successful');
} catch (e) { } catch (e) {
if (e.code !== 'ENOTFOUND' && e.code !== 'ENODATA') { if (e.code !== 'ENOTFOUND' && e.code !== 'ENODATA') {
console.warn(`ipinfo: rDNS lookup error for ${clientIp}:`, e.message); logger.warn({ ip: clientIp, error: e.message, code: e.code }, `rDNS lookup error`);
} else {
logger.debug({ ip: clientIp, code: e.code }, 'rDNS lookup failed (No record)');
} }
rdns = { error: `rDNS lookup failed (${e.code || 'Unknown error'})` }; rdns = { error: `rDNS lookup failed (${e.code || 'Unknown error'})` };
} }
@@ -222,8 +352,8 @@ app.get('/api/ipinfo', async (req, res) => {
res.json({ ip: clientIp, geo, asn, rdns }); res.json({ ip: clientIp, geo, asn, rdns });
} catch (error) { } catch (error) {
console.error(`ipinfo: Error processing ipinfo for ${clientIp}:`, error); logger.error({ ip: clientIp, error: error.message, stack: error.stack }, 'Error processing ipinfo');
res.status(500).json({ error: 'Internal server error.' }); res.status(500).json({ error: 'Internal server error while processing IP information.' });
} }
}); });
@@ -231,92 +361,190 @@ app.get('/api/ipinfo', async (req, res) => {
app.get('/api/ping', async (req, res) => { app.get('/api/ping', async (req, res) => {
const targetIpRaw = req.query.targetIp; const targetIpRaw = req.query.targetIp;
const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw; const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw;
const requestIp = req.ip || req.socket.remoteAddress;
console.log(`--- PING Request ---`); logger.info({ requestIp, targetIp }, 'Ping request received');
console.log(`Value of targetIp: "${targetIp}"`);
const isValidResult = isValidIp(targetIp); if (!isValidIp(targetIp)) {
console.log(`isValidIp (net) result for "${targetIp}": ${isValidResult}`); logger.warn({ requestIp, targetIp }, 'Invalid target IP for ping');
if (!isValidResult) {
console.log(`isValidIp (net) returned false for "${targetIp}", sending 400.`);
return res.status(400).json({ error: 'Invalid target IP address provided.' }); return res.status(400).json({ error: 'Invalid target IP address provided.' });
} }
// --- NEUE PRÜFUNG AUF PRIVATE IP ---
if (isPrivateIp(targetIp)) { if (isPrivateIp(targetIp)) {
console.log(`Target IP "${targetIp}" is private/local. Aborting ping.`); logger.warn({ requestIp, targetIp }, 'Attempt to ping private IP blocked');
return res.status(403).json({ error: 'Operations on private or local IP addresses are not allowed.' }); return res.status(403).json({ error: 'Operations on private or local IP addresses are not allowed.' });
} }
// --- ENDE NEUE PRÜFUNG ---
try { try {
console.log(`Proceeding to execute ping for "${targetIp}"...`); const pingCount = process.env.PING_COUNT || '4';
const args = ['-c', '4', targetIp]; const countArg = parseInt(pingCount, 10) || 4;
const args = ['-c', `${countArg}`, targetIp]; // Linux/macOS
const command = 'ping'; const command = 'ping';
console.log(`Executing: ${command} ${args.join(' ')}`); logger.info({ requestIp, targetIp, command: `${command} ${args.join(' ')}` }, 'Executing ping');
const output = await executeCommand(command, args); const output = await executeCommand(command, args);
const parsedResult = parsePingOutput(output);
console.log(`Ping for ${targetIp} successful.`); logger.info({ requestIp, targetIp, stats: parsedResult.stats }, 'Ping successful');
// TODO: Ping-Ausgabe parsen res.json({ success: true, ...parsedResult });
res.json({ success: true, rawOutput: output });
} catch (error) { } catch (error) {
res.status(500).json({ success: false, error: `Ping command failed: ${error.message}` }); // executeCommand loggt bereits Details
logger.error({ requestIp, targetIp, error: error.message }, 'Ping command failed');
// Sende strukturierte Fehlermeldung, wenn möglich
const parsedError = parsePingOutput(error.message); // Versuche, Fehler aus Ping-Output zu parsen
res.status(500).json({
success: false,
error: `Ping command failed: ${parsedError.error || error.message}`,
rawOutput: parsedError.rawOutput || error.message
});
} }
}); });
// Traceroute Endpunkt // Traceroute Endpunkt (Server-Sent Events)
app.get('/api/traceroute', async (req, res) => { app.get('/api/traceroute', (req, res) => { // Beachte: nicht async, da wir streamen
const targetIpRaw = req.query.targetIp; const targetIpRaw = req.query.targetIp;
const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw; const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw;
const requestIp = req.ip || req.socket.remoteAddress;
console.log(`--- TRACEROUTE Request ---`); logger.info({ requestIp, targetIp }, 'Traceroute stream request received');
console.log(`Value of targetIp: "${targetIp}"`);
const isValidResult = isValidIp(targetIp); if (!isValidIp(targetIp)) {
console.log(`isValidIp (net) result for "${targetIp}": ${isValidResult}`); logger.warn({ requestIp, targetIp }, 'Invalid target IP for traceroute');
if (!isValidResult) {
console.log(`isValidIp (net) returned false for "${targetIp}", sending 400.`);
return res.status(400).json({ error: 'Invalid target IP address provided.' }); return res.status(400).json({ error: 'Invalid target IP address provided.' });
} }
// --- NEUE PRÜFUNG AUF PRIVATE IP ---
if (isPrivateIp(targetIp)) { if (isPrivateIp(targetIp)) {
console.log(`Target IP "${targetIp}" is private/local. Aborting traceroute.`); logger.warn({ requestIp, targetIp }, 'Attempt to traceroute private IP blocked');
return res.status(403).json({ error: 'Operations on private or local IP addresses are not allowed.' }); return res.status(403).json({ error: 'Operations on private or local IP addresses are not allowed.' });
} }
// --- ENDE NEUE PRÜFUNG ---
try { try {
console.log(`Proceeding to execute traceroute for "${targetIp}"...`); logger.info({ requestIp, targetIp }, `Starting traceroute stream...`);
const args = ['-n', targetIp]; // Linux/macOS
// Set SSE Headers
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // Wichtig für Nginx-Proxies
res.flushHeaders(); // Send headers immediately
const args = ['-n', targetIp]; // Linux/macOS, -n für keine Namensauflösung (schneller)
const command = 'traceroute'; const command = 'traceroute';
const proc = spawn(command, args);
logger.info({ requestIp, targetIp, command: `${command} ${args.join(' ')}` }, 'Spawned traceroute process');
console.log(`Executing: ${command} ${args.join(' ')}`); let buffer = ''; // Buffer für unvollständige Zeilen
const output = await executeCommand(command, args);
console.log(`Traceroute for ${targetIp} successful.`); const sendEvent = (event, data) => {
// TODO: Traceroute-Ausgabe parsen try {
res.json({ success: true, rawOutput: output }); res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
} catch (e) {
logger.error({ requestIp, targetIp, event, error: e.message }, "Error writing to SSE stream (client likely disconnected)");
proc.kill(); // Beende Prozess, wenn Schreiben fehlschlägt
res.end();
}
};
proc.stdout.on('data', (data) => {
buffer += data.toString();
let lines = buffer.split('\n');
buffer = lines.pop() || ''; // Letzte (evtl. unvollständige) Zeile zurück in den Buffer
lines.forEach(line => {
const parsed = parseTracerouteLine(line);
if (parsed) {
logger.debug({ requestIp, targetIp, hop: parsed.hop, ip: parsed.ip }, 'Sending hop data');
sendEvent('hop', parsed);
} else if (line.trim()) {
logger.debug({ requestIp, targetIp, message: line.trim() }, 'Sending info data');
sendEvent('info', { message: line.trim() });
}
});
});
proc.stderr.on('data', (data) => {
const errorMsg = data.toString().trim();
logger.warn({ requestIp, targetIp, stderr: errorMsg }, 'Traceroute stderr output');
sendEvent('error', { error: errorMsg });
});
proc.on('error', (err) => {
logger.error({ requestIp, targetIp, error: err.message }, `Failed to start traceroute command`);
sendEvent('error', { error: `Failed to start traceroute: ${err.message}` });
if (!res.writableEnded) res.end();
});
proc.on('close', (code) => {
if (buffer) { // Verarbeite letzte Zeile im Buffer
const parsed = parseTracerouteLine(buffer);
if (parsed) {
sendEvent('hop', parsed);
} else if (buffer.trim()) {
sendEvent('info', { message: buffer.trim() });
}
}
if (code !== 0) {
logger.error({ requestIp, targetIp, exitCode: code }, `Traceroute command finished with error code ${code}`);
sendEvent('error', { error: `Traceroute command failed with exit code ${code}` });
} else {
logger.info({ requestIp, targetIp }, `Traceroute stream completed successfully.`);
}
sendEvent('end', { exitCode: code });
if (!res.writableEnded) res.end();
});
// Handle client disconnect
req.on('close', () => {
logger.info({ requestIp, targetIp }, 'Client disconnected from traceroute stream, killing process.');
if (!proc.killed) {
proc.kill();
}
if (!res.writableEnded) res.end();
});
} catch (error) { } catch (error) {
res.status(500).json({ success: false, error: `Traceroute command failed: ${error.message}` }); // Dieser Catch ist eher für synchrone Fehler vor dem Spawn
logger.error({ requestIp, targetIp, error: error.message, stack: error.stack }, 'Error setting up traceroute stream');
if (!res.headersSent) {
res.status(500).json({ success: false, error: `Failed to initiate traceroute: ${error.message}` });
} else {
// Wenn Header gesendet wurden, können wir nur noch versuchen, einen Fehler zu schreiben und zu beenden
try {
if (!res.writableEnded) {
res.write(`event: error\ndata: ${JSON.stringify({ error: `Internal server error: ${error.message}` })}\n\n`);
res.end();
}
} catch (e) { logger.error({ requestIp, targetIp, error: e.message }, "Error writing final error to SSE stream"); }
}
} }
}); }); // Ende von app.get('/api/traceroute'...)
// --- Server starten --- // --- Server starten ---
initialize().then(() => { initialize().then(() => {
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`); logger.info({ port: PORT, node_env: process.env.NODE_ENV || 'development' }, `Server listening`);
console.log(`API endpoints available at:`); logger.info(`API endpoints available at:`);
console.log(` http://localhost:${PORT}/api/ipinfo`); logger.info(` http://localhost:${PORT}/api/ipinfo`);
console.log(` http://localhost:${PORT}/api/ping?targetIp=<ip>`); logger.info(` http://localhost:${PORT}/api/ping?targetIp=<ip>`);
console.log(` http://localhost:${PORT}/api/traceroute?targetIp=<ip>`); logger.info(` http://localhost:${PORT}/api/traceroute?targetIp=<ip>`);
}); });
}).catch(error => { }).catch(error => {
console.error("Server could not start due to initialization errors."); // Fehler bei der Initialisierung wurde bereits geloggt.
logger.fatal("Server could not start due to initialization errors.");
process.exit(1); // Beenden bei schwerwiegendem Startfehler
});
// Graceful Shutdown Handling (optional aber gut für Produktion)
const signals = { 'SIGINT': 2, 'SIGTERM': 15 };
Object.keys(signals).forEach((signal) => {
process.on(signal, () => {
logger.info(`Received ${signal}, shutting down gracefully...`);
// Hier könnten noch offene Verbindungen geschlossen werden etc.
// z.B. server.close(() => { logger.info('HTTP server closed.'); process.exit(128 + signals[signal]); });
// Für dieses Beispiel beenden wir direkt:
process.exit(128 + signals[signal]);
});
}); });