commit 08c76bc6ddb11897cbdc8783ec122eea82bfe0e3 Author: seo Date: Sun Jun 7 00:33:58 2026 +0900 Initial seoul project import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9134392 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +car/ +.agents/ +.codex/ +.env +*.log +*.db +*.sqlite +*.sql +cache/ +tmp/ +secrets/ +secret/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b4ee95 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Seoul + +PHP dashboard that builds a service shortcut list from nginx configuration and local PHP entry points. + +## Main Features + +- Reads nginx configuration paths and extracts HTTPS server blocks. +- Finds shallow PHP entries under document roots. +- Filters self links, internal names, IP literals, variables, and regex-like host values. +- Provides PWA metadata and service worker assets. + +## Main Entry Points + +- `index.php` +- `manifest.webmanifest` +- `sw.js` + +## Structure + +- `index.php`: dashboard renderer. +- `manifest.webmanifest`: PWA manifest. +- `sw.js`: service worker. +- `assets/`: icons and static assets. + +## Notes + +The `car/` directory is maintained as a separate repository and is intentionally ignored here. + +## Security + +- nginx configuration is read for display only. +- Generated host and link values are escaped before rendering. + diff --git a/assets/apple-touch-icon.png b/assets/apple-touch-icon.png new file mode 100644 index 0000000..a8384f5 Binary files /dev/null and b/assets/apple-touch-icon.png differ diff --git a/assets/favicon.svg b/assets/favicon.svg new file mode 100644 index 0000000..8d8b731 --- /dev/null +++ b/assets/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icon-192.png b/assets/icon-192.png new file mode 100644 index 0000000..dc947d1 Binary files /dev/null and b/assets/icon-192.png differ diff --git a/assets/icon-32.png b/assets/icon-32.png new file mode 100644 index 0000000..ab7a4bc Binary files /dev/null and b/assets/icon-32.png differ diff --git a/assets/icon-512.png b/assets/icon-512.png new file mode 100644 index 0000000..be4ac92 Binary files /dev/null and b/assets/icon-512.png differ diff --git a/index.php b/index.php new file mode 100644 index 0000000..43644aa --- /dev/null +++ b/index.php @@ -0,0 +1,472 @@ + true, + 'localhost' => true, +]; + +$denyRootHosts = [ + 'seo.chaegeon.com' => true, +]; + +function collectFiles(array $paths): array +{ + $files = []; + + foreach ($paths as $path) { + if (is_file($path)) { + $files[] = $path; + continue; + } + + if (!is_dir($path)) { + continue; + } + + $it = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS) + ); + + foreach ($it as $file) { + if (!$file->isFile()) { + continue; + } + + $name = $file->getFilename(); + + if ( + str_ends_with($name, '.conf') || + str_contains($name, 'ReverseProxy') || + str_contains($name, 'nginx') || + str_contains($file->getPathname(), 'sites-enabled') + ) { + $files[] = $file->getPathname(); + } + } + } + + return array_values(array_unique($files)); +} + +function extractServerBlocks(string $text): array +{ + $blocks = []; + $len = strlen($text); + $pos = 0; + + while (($serverPos = strpos($text, 'server', $pos)) !== false) { + $before = $serverPos > 0 ? $text[$serverPos - 1] : ' '; + $after = $text[$serverPos + 6] ?? ' '; + + if (preg_match('/[a-zA-Z0-9_\-]/', $before) || preg_match('/[a-zA-Z0-9_\-]/', $after)) { + $pos = $serverPos + 6; + continue; + } + + $brace = strpos($text, '{', $serverPos); + if ($brace === false) { + break; + } + + $depth = 0; + for ($i = $brace; $i < $len; $i++) { + if ($text[$i] === '{') { + $depth++; + } elseif ($text[$i] === '}') { + $depth--; + if ($depth === 0) { + $blocks[] = substr($text, $serverPos, $i - $serverPos + 1); + $pos = $i + 1; + break; + } + } + } + + if ($pos <= $serverPos) { + break; + } + } + + return $blocks; +} + +function cleanNginxText(string $text): string +{ + $text = preg_replace('/#.*$/m', '', $text); + return $text ?? ''; +} + +function extractServerNames(string $block): array +{ + preg_match_all('/server_name\s+([^;]+);/i', $block, $matches); + + $names = []; + + foreach ($matches[1] ?? [] as $line) { + foreach (preg_split('/\s+/', trim($line)) as $host) { + $host = trim($host); + + if ($host === '' || $host === '_' || str_contains($host, '$')) { + continue; + } + + if (str_starts_with($host, '~')) { + continue; + } + + $host = trim($host, '.'); + + if (filter_var($host, FILTER_VALIDATE_IP)) { + continue; + } + + if (preg_match('/^[a-zA-Z0-9.-]+$/', $host)) { + $names[] = strtolower($host); + } + } + } + + return array_values(array_unique($names)); +} + +function detectScheme(string $block): string +{ + if (preg_match('/listen\s+[^;]*443[^;]*ssl/i', $block)) { + return 'https'; + } + + if (preg_match('/ssl_certificate\s+/i', $block)) { + return 'https'; + } + + return 'http'; +} + +function isBlockedServer(string $block): bool +{ + if (preg_match('/return\s+444\s*;/i', $block)) { + return true; + } + + if (preg_match('/deny\s+all\s*;/i', $block) && !preg_match('/proxy_pass|fastcgi_pass|root\s+/i', $block)) { + return true; + } + + return false; +} + +function extractLocations(string $block): array +{ + return ['/']; +} + +function extractDocumentRoot(string $block): ?string +{ + if (!preg_match('/^\s*root\s+([^;]+);/mi', $block, $matches)) { + return null; + } + + $root = trim($matches[1], " \t\n\r\0\x0B\"'"); + + if ($root === '' || str_contains($root, '$') || !is_dir($root)) { + return null; + } + + $root = rtrim($root, '/'); + + if ($root === '') { + return null; + } + + return $root; +} + +function discoverPhpEntryPaths(?string $root): array +{ + if ($root === null) { + return []; + } + + $paths = []; + $allowedFiles = [ + 'index.php' => true, + 'monitor.php' => true, + ]; + + $it = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS) + ); + + foreach ($it as $file) { + if (!$file->isFile()) { + continue; + } + + $filename = $file->getFilename(); + if (!isset($allowedFiles[$filename])) { + continue; + } + + $relative = substr($file->getPathname(), strlen($root)); + if ($relative === false || $relative === '') { + continue; + } + + $relative = str_replace(DIRECTORY_SEPARATOR, '/', $relative); + if ($relative === '/index.php') { + continue; + } + + $depth = substr_count(trim($relative, '/'), '/'); + + if ($depth > 1) { + continue; + } + + $paths[] = $relative; + } + + sort($paths, SORT_NATURAL); + + return array_values(array_unique($paths)); +} + +$items = []; +$files = collectFiles($nginxFiles); + +foreach ($files as $file) { + $raw = @file_get_contents($file); + if ($raw === false) { + continue; + } + + $text = cleanNginxText($raw); + $blocks = extractServerBlocks($text); + + foreach ($blocks as $block) { + if (isBlockedServer($block)) { + continue; + } + + $hosts = extractServerNames($block); + if (!$hosts) { + continue; + } + + $scheme = detectScheme($block); + + if ($scheme !== 'https') { + continue; + } + + $paths = array_values(array_unique(array_merge( + extractLocations($block), + discoverPhpEntryPaths(extractDocumentRoot($block)) + ))); + + foreach ($hosts as $host) { + if (str_contains($host, 'webdav')) { + continue; + } + + if (isset($denyHosts[$host])) { + continue; + } + + foreach ($paths as $path) { + if ($path === '/' && isset($denyRootHosts[$host])) { + continue; + } + + $url = $scheme . '://' . $host . $path; + + $items[$url] = [ + 'url' => $url, + 'host' => $host, + 'scheme' => $scheme, + ]; + } + } + } +} + +ksort($items, SORT_NATURAL); +?> + + + + + Nginx 바로가기 + + + + + + + + + + + + + + + + + + +
+
+ +
표시할 URL이 없습니다.
+ + +
+
+ + + +
+ + +
+
+ + + diff --git a/manifest.webmanifest b/manifest.webmanifest new file mode 100644 index 0000000..49b9d03 --- /dev/null +++ b/manifest.webmanifest @@ -0,0 +1,25 @@ +{ + "name": "Seoul", + "short_name": "Seoul", + "description": "Seoul server shortcuts", + "start_url": ".", + "scope": ".", + "display": "standalone", + "orientation": "any", + "background_color": "#ffffff", + "theme_color": "#ffffff", + "icons": [ + { + "src": "assets/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "assets/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..e75ae8d --- /dev/null +++ b/sw.js @@ -0,0 +1,50 @@ +const CACHE_NAME = 'seoul-shortcuts-v1'; +const CORE_ASSETS = [ + './', + './assets/favicon.svg', + './assets/icon-32.png', + './assets/icon-192.png', + './assets/icon-512.png', + './assets/apple-touch-icon.png', + './manifest.webmanifest' +]; + +self.addEventListener('install', event => { + event.waitUntil( + caches.open(CACHE_NAME) + .then(cache => cache.addAll(CORE_ASSETS)) + .catch(() => undefined) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', event => { + event.waitUntil( + caches.keys().then(keys => Promise.all( + keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key)) + )) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', event => { + if (event.request.method !== 'GET') { + return; + } + + const url = new URL(event.request.url); + + if (url.origin !== location.origin) { + return; + } + + event.respondWith( + fetch(event.request) + .then(response => { + const copy = response.clone(); + caches.open(CACHE_NAME).then(cache => cache.put(event.request, copy)); + return response; + }) + .catch(() => caches.match(event.request)) + ); +});