This commit is contained in:
2026-08-22 20:25:18 +02:00
commit f341efe056
6 changed files with 498 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
services:
fpm-monip:
image: php:8.2-fpm-alpine
container_name: fpm-monip
restart: unless-stopped
volumes:
- ./src:/var/www/html
environment:
TZ: "Europe/Paris"
IPINFO_TOKEN: ${IPINFO_TOKEN}
networks:
- traefik-net
monip:
image: nginx:alpine
container_name: monip
restart: unless-stopped
# ports:
# - "8030:80"
volumes:
- ./src:/var/www/html
- ./nginx/conf.d:/etc/nginx/conf.d
environment:
TZ: "Europe/Paris"
networks:
- traefik-net
networks:
traefik-net:
external: true
+27
View File
@@ -0,0 +1,27 @@
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Gestion des fichiers PHP via PHP-FPM
location ~ \.php$ {
fastcgi_pass fpm-monip:9000; # Nom du service PHP-FPM dans docker-compose
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param HTTP_X_REAL_IP $remote_addr;
fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
}
# Cache des assets statiques
location ~* \.(css|js|png|jpg|jpeg|gif|ico)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
}
+128
View File
@@ -0,0 +1,128 @@
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Share+Tech+Mono&family=VT323&display=swap');
:root {
--matrix-bg: #020403;
--matrix-surface: #071009;
--matrix-surface-2: #0a140c;
--accent-primary: #00ff41;
--accent-soft: color-mix(in srgb, var(--accent-primary) 35%, white);
--accent-line: color-mix(in srgb, var(--accent-primary) 32%, transparent);
--accent-glow: color-mix(in srgb, var(--accent-primary) 32%, transparent);
--matrix-alert: #ff5a36;
--matrix-warn: #d8e7c4;
--matrix-muted: #6c8b73;
}
* { box-sizing: border-box; }
html { background: var(--matrix-bg); }
body.matrix-mode {
position: relative;
min-height: 100vh;
margin: 0;
overflow-x: hidden;
background: var(--matrix-bg);
color: var(--accent-soft);
font-family: 'Share Tech Mono', monospace;
font-size: .78rem;
letter-spacing: .025em;
}
body.matrix-mode::before {
position: fixed;
inset: 0;
z-index: -2;
pointer-events: none;
content: "";
background: radial-gradient(ellipse at 50% -10%, color-mix(in srgb, var(--accent-primary) 8.5%, transparent), transparent 58%), var(--matrix-bg);
}
body.matrix-mode::after {
position: fixed;
inset: 0;
z-index: 10;
pointer-events: none;
content: "";
opacity: .12;
background: repeating-linear-gradient(to bottom, transparent 0, transparent 3px, color-mix(in srgb, var(--accent-primary) 16%, transparent) 4px, transparent 5px);
}
.container { position: relative; z-index: 1; max-width: 1080px; }
.card {
color: var(--accent-soft);
background: rgba(7, 16, 9, .92);
border: 1px solid var(--accent-line);
border-radius: 0;
box-shadow: 0 0 18px color-mix(in srgb, var(--accent-primary) 10%, transparent);
}
.card:hover {
border-color: color-mix(in srgb, var(--accent-primary) 68%, transparent);
box-shadow: 0 0 22px color-mix(in srgb, var(--accent-primary) 15%, transparent);
}
.card-header, .card-footer { border-color: var(--accent-line); border-radius: 0 !important; }
.card-header { padding: .6rem .78rem .48rem; font-family: 'VT323', monospace; font-size: .85rem; letter-spacing: .09em; text-transform: uppercase; }
.card-body { padding: .78rem; font-size: .78rem; }
.card-footer { padding: .55rem .78rem; background: rgba(0, 0, 0, .28) !important; color: var(--matrix-muted) !important; font-size: .65rem; }
.header-main { color: var(--matrix-bg); background: var(--accent-primary); border-color: var(--accent-primary); }
.header-main h2 { color: var(--matrix-bg); text-shadow: none; font-size: 1.05rem; }
.header-alt { color: var(--accent-primary); background: color-mix(in srgb, var(--accent-primary) 10%, transparent); border-bottom: 1px solid var(--accent-primary); }
.header-alert { color: #ff9a72; background: rgba(120, 24, 12, .55); border-bottom: 1px solid var(--matrix-alert); }
.header-warn { color: var(--matrix-warn); background: rgba(140, 165, 98, .12); border-bottom: 1px solid rgba(216, 231, 196, .7); }
.header-debug { color: #b7c0b9; background: rgba(86, 99, 90, .27); border-bottom: 1px solid rgba(183, 192, 185, .55); }
h1, h2, h3, h4, h5, h6 { color: var(--accent-primary); text-shadow: 0 0 8px color-mix(in srgb, var(--accent-primary) 32%, transparent); }
.header-main h2 { text-shadow: none; }
strong { color: color-mix(in srgb, var(--accent-primary) 20%, white); font-weight: normal; }
p { line-height: 1.4; }
.text-muted { color: var(--matrix-muted) !important; }
.text-danger { color: var(--matrix-alert) !important; text-shadow: 0 0 6px rgba(255, 90, 54, .3); }
.text-success { color: var(--accent-primary) !important; }
pre {
max-height: 26rem;
margin: 0;
overflow: auto;
padding: .78rem;
color: var(--accent-primary);
background: #010201;
border: 1px solid var(--accent-line);
border-radius: 0;
font-family: 'JetBrains Mono', 'Menlo', 'Consolas', monospace;
font-size: .68rem;
line-height: 1.3;
white-space: pre-wrap;
word-break: break-word;
}
.py-4 { padding-top: 2rem !important; padding-bottom: 2rem !important; }
.shadow-sm { box-shadow: 0 0 26px color-mix(in srgb, var(--accent-primary) 11%, transparent) !important; }
.hue-toggle {
position: fixed;
right: .8rem;
bottom: .65rem;
z-index: 20;
padding: .18rem .35rem;
color: var(--accent-primary);
background: rgba(2, 4, 3, .82);
border: 1px solid var(--accent-line);
border-radius: 0;
font: .68rem 'Share Tech Mono', monospace;
letter-spacing: .04em;
cursor: pointer;
opacity: .72;
}
.hue-toggle:hover, .hue-toggle:focus-visible { opacity: 1; color: var(--accent-primary); box-shadow: 0 0 10px color-mix(in srgb, var(--accent-primary) 22%, transparent); outline: none; }
@media (max-width: 575.98px) {
.py-4 { padding-top: 1.25rem !important; padding-bottom: 1.25rem !important; }
.card-header { font-size: .85rem; }
.card-body { padding: .65rem; }
}
+173
View File
@@ -0,0 +1,173 @@
<?php
function fetchGeo(string $ip): array
{
$token = getenv('IPINFO_TOKEN') ?: '';
$url = 'https://ipinfo.io/' . rawurlencode($ip) . '/json';
if ($token !== '') {
$url .= '?token=' . rawurlencode($token);
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = $response ? json_decode($response, true) : [];
return is_array($data) ? $data : [];
}
function getRealClientIp(): string
{
$trustedProxies = [
'172.16.0.0/12',
'10.0.0.0/8',
'192.168.0.0/16',
'127.0.0.1',
];
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
if (!ipInRanges($remoteAddr, $trustedProxies)) {
return $remoteAddr;
}
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
foreach ($ips as $ip) {
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
if (!empty($_SERVER['HTTP_X_REAL_IP']) && filter_var($_SERVER['HTTP_X_REAL_IP'], FILTER_VALIDATE_IP)) {
return $_SERVER['HTTP_X_REAL_IP'];
}
return $remoteAddr;
}
function ipInRanges(string $ip, array $ranges): bool
{
foreach ($ranges as $range) {
if (ipInRange($ip, $range)) {
return true;
}
}
return false;
}
function ipInRange(string $ip, string $cidr): bool
{
if (strpos($cidr, '/') === false) {
return $ip === $cidr;
}
[$subnet, $bits] = explode('/', $cidr);
$ipLong = ip2long($ip);
$subnetLong = ip2long($subnet);
if ($ipLong === false || $subnetLong === false) {
return false;
}
$mask = -1 << (32 - (int)$bits);
$subnetLong &= $mask;
return ($ipLong & $mask) === $subnetLong;
}
function getWhoisServer(string $ip): string
{
// Convertir l'IP en nombre pour comparaison
$ipLong = ip2long($ip);
if ($ipLong === false) {
return 'whois.iana.org'; // Fallback
}
// Plages IP simplifiées (à adapter si besoin)
if ($ipLong >= ip2long('192.0.0.0') && $ipLong <= ip2long('223.255.255.255')) {
return 'whois.apnic.net'; // Asie-Pacifique
} elseif ($ipLong >= ip2long('128.0.0.0') && $ipLong <= ip2long('191.255.255.255')) {
return 'whois.arin.net'; // Amérique
} elseif ($ipLong >= ip2long('185.0.0.0') && $ipLong <= ip2long('191.255.255.255')) {
return 'whois.ripe.net'; // Europe (étendue)
} elseif ($ipLong >= ip2long('200.0.0.0') && $ipLong <= ip2long('200.255.255.255')) {
return 'whois.lacnic.net'; // Amérique Latine
} else {
return 'whois.ripe.net'; // Europe par défaut (pour ton cas)
}
}
function fetchLocalWhois(string $ip, int $timeout = 5): array
{
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
return ['raw' => '', 'error' => 'IP invalide'];
}
$escapedIp = escapeshellarg($ip);
// 1. Convertir l'IP en format in-addr.arpa (ex: 82.64.199.86 → 86.199.64.82.in-addr.arpa)
$parts = explode('.', $ip);
$reverseIp = implode('.', array_reverse($parts)) . '.in-addr.arpa';
$escapedReverseIp = escapeshellarg($reverseIp);
// 2. Essayer avec whois.ripe.net + in-addr.arpa (méthode la plus fiable)
$command = "timeout {$timeout} whois -h whois.ripe.net {$escapedReverseIp} 2>&1";
$output = shell_exec($command);
// 3. Fallback : Essayer sans -h (utilise la config par défaut)
if (empty(trim($output ?? '')) || strpos($output, 'no entries found') !== false) {
$command = "timeout {$timeout} whois {$escapedReverseIp} 2>&1";
$output = shell_exec($command);
}
// 4. Fallback ultime : Essayer avec l'IP directe (si le client whois est bien configuré)
if (empty(trim($output ?? '')) || strpos($output, 'no entries found') !== false) {
$command = "timeout {$timeout} whois {$escapedIp} 2>&1";
$output = shell_exec($command);
}
return [
'raw' => $output !== null ? trim($output) : '',
'source' => 'local'
];
}
function fetchWhois2(string $ip): array
{
if (!function_exists('curl_init')) {
return [];
}
$ch = curl_init("https://ipwhois.app/json/{$ip}");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $httpCode !== 200) {
return [];
}
$data = json_decode($response, true);
return is_array($data) ? $data : [];
}
function countryName(string $code): string
{
$countries = [
'FR' => 'France',
'US' => 'États-Unis',
'GB' => 'Royaume-Uni',
'DE' => 'Allemagne',
'ES' => 'Espagne',
'IT' => 'Italie',
'BE' => 'Belgique',
'CH' => 'Suisse',
'CA' => 'Canada',
'PT' => 'Portugal',
'NL' => 'Pays-Bas',
'LU' => 'Luxembourg',
];
$normalizedCode = strtoupper(trim($code));
return $countries[$normalizedCode] ?? $code;
}
+140
View File
@@ -0,0 +1,140 @@
<?php
include "functions.php";
$apiKey = '';
$showDebug = false;
$ip = getRealClientIp();
$reverseDns = gethostbyaddr($ip);
$hostname = gethostbyaddr($ip);
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'N/A';
$httpReferer = $_SERVER['HTTP_REFERER'] ?? 'N/A';
$httpHost = $_SERVER['HTTP_HOST'] ?? 'N/A';
// Appel API de géolocalisation.
$geoData = filter_var($ip, FILTER_VALIDATE_IP) ? fetchGeo($ip) : [];
// Normalisation des champs ipinfo.io pour le rendu HTML.
if (!empty($geoData)) {
if (isset($geoData['loc']) && strpos((string)$geoData['loc'], ',') !== false) {
[$lat, $lon] = explode(',', (string)$geoData['loc'], 2);
$geoData['lat'] = trim($lat);
$geoData['lon'] = trim($lon);
}
// Conserver d'abord le code ISO brut avant de traduire le nom du pays.
$geoData['countryCode'] = $geoData['country'] ?? null;
$geoData['country'] = countryName((string)($geoData['countryCode'] ?? ''));
$geoData['regionName'] = $geoData['region'] ?? null;
$geoData['zip'] = $geoData['postal'] ?? null;
$geoData['isp'] = $geoData['org'] ?? null;
$geoData['status'] = 'success';
}
// Suppression des valeurs null après la normalisation.
$geoData = array_filter($geoData, static function ($value): bool {
return $value !== null;
});
// Récupération des informations WHOIS / ASN.
// $whoisData = filter_var($ip, FILTER_VALIDATE_IP) ? fetchWhois2($ip) : [];
// $whoisData = filter_var($ip, FILTER_VALIDATE_IP) ? fetchLocalWhois($ip) : [];
$whoisData = filter_var($ip, FILTER_VALIDATE_IP) ? fetchLocalWhois($ip) : [];
if (empty($whoisData['raw']) || strpos($whoisData['raw'], 'no entries found') !== false) {
$whoisData = fetchWhois2($ip); // Fallback vers l'API
$whoisData['source'] = 'api';
}
$whoisData = is_array($whoisData) ? $whoisData : ['raw' => $whoisData];
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>MONIP - <?= htmlspecialchars((string)$ip, ENT_QUOTES, 'UTF-8'); ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/style.css">
</head>
<body class="matrix-mode">
<div class="container py-4"><div class="row justify-content-center"><div class="col-lg-8">
<div class="card shadow-sm mb-4">
<div class="card-header header-main"><h2 class="h4 mb-0">&gt; VBLOG.IO IP TRACER</h2></div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6"><div class="card h-100"><div class="card-header header-main"><h5 class="h6 mb-0">&gt; ADRESSE IP SOURCE</h5></div>
<div class="card-body">
<p class="mb-1"><strong>IPv4:</strong> <?= htmlspecialchars((string)$ip, ENT_QUOTES, 'UTF-8'); ?></p>
<p class="mb-1"><strong>REVERSE DNS:</strong> <?= htmlspecialchars((string)($reverseDns ?: 'N/A'), ENT_QUOTES, 'UTF-8'); ?></p>
<p class="mb-1"><strong>HOSTNAME:</strong> <?= htmlspecialchars((string)($hostname ?: 'N/A'), ENT_QUOTES, 'UTF-8'); ?></p>
</div></div></div>
<div class="col-md-6"><div class="card h-100"><div class="card-header header-alt"><h5 class="h6 mb-0">&gt; GÉOLOCALISATION</h5></div>
<div class="card-body">
<?php if (!empty($geoData['country'])): ?>
<p class="mb-1"><strong>PAYS:</strong> <?= htmlspecialchars((string)($geoData['country'] ?? 'N/A') . ' (' . (string)($geoData['countryCode'] ?? 'N/A') . ')', ENT_QUOTES, 'UTF-8'); ?></p>
<p class="mb-1"><strong>RÉGION:</strong> <?= htmlspecialchars((string)($geoData['regionName'] ?? 'N/A') . ' (' . (string)($geoData['region'] ?? 'N/A') . ')', ENT_QUOTES, 'UTF-8'); ?></p>
<p class="mb-1"><strong>VILLE:</strong> <?= htmlspecialchars((string)($geoData['city'] ?? 'N/A'), ENT_QUOTES, 'UTF-8'); ?></p>
<p class="mb-1"><strong>FUSEAU HORAIRE:</strong> <?= htmlspecialchars((string)($geoData['timezone'] ?? 'N/A'), ENT_QUOTES, 'UTF-8'); ?></p>
<p class="mb-1"><strong>COORDONNÉES:</strong> <?= htmlspecialchars((string)($geoData['lat'] ?? 'N/A') . ', ' . (string)($geoData['lon'] ?? 'N/A'), ENT_QUOTES, 'UTF-8'); ?></p>
<?php else: ?><p class="text-muted">DONNÉES GÉO INDISPONIBLES.</p><?php endif; ?>
</div></div></div>
</div>
<div class="row mb-3">
<div class="col-md-6"><div class="card h-100"><div class="card-header header-warn"><h5 class="h6 mb-0">&gt; FOURNISSEUR (ISP)</h5></div>
<div class="card-body">
<?php if (!empty($geoData['isp'])): ?>
<p class="mb-1"><strong>ISP:</strong> <?= htmlspecialchars((string)($geoData['isp'] ?? 'N/A'), ENT_QUOTES, 'UTF-8'); ?></p>
<p class="mb-1"><strong>ORGANISATION:</strong> <?= htmlspecialchars((string)($geoData['org'] ?? 'N/A'), ENT_QUOTES, 'UTF-8'); ?></p>
<p class="mb-1"><strong>AS:</strong> <?= htmlspecialchars((string)($geoData['as'] ?? 'N/A'), ENT_QUOTES, 'UTF-8'); ?></p>
<?php else: ?><p class="text-muted">DONNÉES ISP INDISPONIBLES.</p><?php endif; ?>
</div></div></div>
<div class="col-md-6"><div class="card h-100"><div class="card-header header-alert"><h5 class="h6 mb-0">&gt; SÉCURITÉ</h5></div>
<div class="card-body">
<?php
$isProxy = $geoData['proxy'] ?? false;
$isHosting = $geoData['hosting'] ?? false;
$isMobile = $geoData['mobile'] ?? false;
?>
<p class="mb-1"><strong>PROXY:</strong> <?= $isProxy ? '<span class="text-danger">OUI</span>' : 'Non'; ?></p>
<p class="mb-1"><strong>HÉBERGÉ:</strong> <?= $isHosting ? '<span class="text-danger">OUI</span>' : 'Non'; ?></p>
<p class="mb-1"><strong>MOBILE:</strong> <?= $isMobile ? '<span class="text-success">OUI</span>' : 'Non'; ?></p>
</div></div></div>
</div>
<div class="card mb-3"><div class="card-header header-debug"><h5 class="h6 mb-0">&gt; WHOIS / ASN</h5></div>
<div class="card-body"><pre><?= !empty($whoisData['raw']) ? htmlspecialchars((string)$whoisData['raw'], ENT_QUOTES, 'UTF-8') : (!empty($whoisData['asn']) ? 'AS' . htmlspecialchars((string)($whoisData['asn'] ?? 'N/A'), ENT_QUOTES, 'UTF-8') . ' - ' . htmlspecialchars((string)($whoisData['isp'] ?? 'N/A'), ENT_QUOTES, 'UTF-8') : 'DONNÉES WHOIS INDISPONIBLES.'); ?></pre></div></div>
<?php if ($showDebug): ?>
<div class="card mb-3"><div class="card-header header-debug"><h5 class="h6 mb-0">&gt; EN-TÊTES HTTP</h5></div>
<div class="card-body"><pre><?= htmlspecialchars(print_r($_SERVER, true), ENT_QUOTES, 'UTF-8'); ?></pre></div></div>
<?php endif; ?>
<div class="card"><div class="card-header header-alt"><h5 class="h6 mb-0">&gt; NAVIGATEUR &amp; SYSTÈME</h5></div>
<div class="card-body">
<p class="mb-1"><strong>USER AGENT:</strong> <?= htmlspecialchars((string)$userAgent, ENT_QUOTES, 'UTF-8'); ?></p>
<p class="mb-1"><strong>RÉFÉRENT:</strong> <?= htmlspecialchars((string)$httpReferer, ENT_QUOTES, 'UTF-8'); ?></p>
<p class="mb-1"><strong>HOST:</strong> <?= htmlspecialchars((string)$httpHost, ENT_QUOTES, 'UTF-8'); ?></p>
<p class="mb-0"><strong>DATE LOCALE:</strong> <?= date('Y-m-d H:i:s'); ?></p>
</div></div>
</div>
<div class="card-footer text-muted small"><strong>MONIP</strong> | v1.0 | <?= date('Y-m-d H:i:s'); ?></div>
</div>
</div></div></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<button type="button" class="hue-toggle" id="hue-toggle" aria-label="Basculer la teinte d'accent">[ HUE::GRN ]</button>
<script>
(() => {
const root = document.documentElement;
const toggle = document.getElementById('hue-toggle');
const hues = { GRN: '#00ff41', BLU: '#00cfff' };
const savedHue = localStorage.getItem('monip-hue');
// let hue = savedHue === 'GRN' ? 'GRN' : 'BLU';
let hue = savedHue || 'BLU'; // Utilise la valeur sauvegardée, ou bleu si rien n'est enregistré
const applyHue = (nextHue) => {
hue = nextHue === 'BLU' ? 'BLU' : 'GRN';
root.style.setProperty('--accent-primary', hues[hue]);
toggle.textContent = `[ HUE::${hue} ]`;
localStorage.setItem('monip-hue', hue);
};
applyHue(hue);
toggle.addEventListener('click', () => applyHue(hue === 'GRN' ? 'BLU' : 'GRN'));
})();
</script>
</body></html>