diff --git a/.env.example b/.env.example index ad508865..1c22053f 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,18 @@ +# Server BACKEND_PORT="11966" FRONTEND_PORT="18966" -BING_MAP_API_KEY="" ALLOWED_DOMAINS="" +# APIs +BING_MAP_API_KEY="" IPINFO_API_TOKEN="" KEYCDN_USER_AGENT="" -IPCHECKING_API_KEY="" CLOUDFLARE_API="" +IPAPIIS_API_KEY="" +IPCHECKING_API_KEY="" +MAC_LOOKUP_API_KEY="" +# Security related SECURITY_BLACKLIST_LOG_FILE_PATH="" SECURITY_RATE_LIMIT="" SECURITY_DELAY_AFTER="" -IPAPIIS_API_KEY="" -MAC_LOOKUP_API_KEY="" \ No newline at end of file +# Google Analytics +VITE_GOOGLE_ANALYTICS_ID="" \ No newline at end of file diff --git a/README.md b/README.md index 98f38090..978aa8fc 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ You can use the program without adding any environment variables, but if you wan | `KEYCDN_USER_AGENT` | No | `""` | The domain name when using KeyCDN, must contain https prefix. Used to obtain IP address information through KeyCDN | | `CLOUDFLARE_API` | No | `""` | API Key for Cloudflare, used to obtain AS system information through Cloudflare | | `MAC_LOOKUP_API_KEY` | No | `""` | API Key for MAC Lookup, used to obtain MAC address information | +| `VITE_GOOGLE_ANALYTICS_ID` | **Yes** | `""` | Google Analytics ID, used to track user behavior | ### Using Environment Variables in a Node Environment diff --git a/README_FR.md b/README_FR.md index 410d7fd5..ce8d09ab 100644 --- a/README_FR.md +++ b/README_FR.md @@ -103,6 +103,7 @@ Vous pouvez utiliser le programme sans ajouter de variables d'environnement, mai | `KEYCDN_USER_AGENT` | Non | `""` | Le nom de domaine lorsque vous utilisez KeyCDN, doit contenir le préfixe https. Utilisé pour obtenir des informations sur l'adresse IP via KeyCDN | | `CLOUDFLARE_API` | Non | `""` | Clé API pour Cloudflare, utilisée pour obtenir des informations sur le système AS via Cloudflare | | `MAC_LOOKUP_API_KEY` | Non | `""` | Clé API pour MAC Lookup, utilisée pour obtenir des informations sur l'adresse MAC via MAC Lookup | +| `VITE_GOOGLE_ANALYTICS_ID` | **Oui** | `""` | Identifiant Google Analytics, utilisé pour l'analyse des utilisateurs | ### Utilisation des variables d'environnement dans un environnement Node diff --git a/README_ZH.md b/README_ZH.md index 85d1d01c..0809b781 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -105,6 +105,7 @@ docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest | `KEYCDN_USER_AGENT` | 否 | `""` | 使用 KeyCDN 时的域名,需包含 https 前缀。用于通过 KeyCDN 获取 IP 归属地信息 | | `CLOUDFLARE_API` | 否 | `""` | Cloudflare 的 API Key,用于通过 Cloudflare 获取 AS 系统的信息 | | `MAC_LOOKUP_API_KEY` | 否 | `""` | MAC 查询的 API Key,用于通过 MAC Lookup 获取 MAC 地址的归属信息 | +| `VITE_GOOGLE_ANALYTICS_ID` | **是** | `""` | Google Analytics 的 ID,用于统计访问量 | ### 在 Node 环境里使用环境变量 diff --git a/api/configs.js b/api/configs.js index 89bbaea6..dfa88cd6 100644 --- a/api/configs.js +++ b/api/configs.js @@ -14,7 +14,7 @@ export default (req, res) => { } const hostname = referer ? new URL(referer).hostname : ''; - const originalSite = hostname === 'ipcheck.ing' || hostname === 'www.ipcheck.ing'; + const originalSite = hostname === 'ipcheck.ing' || hostname === 'www.ipcheck.ing' || hostname === 'localtest.ipcheck.ing'; const envConfigs = { bingMap: process.env.BING_MAP_API_KEY, diff --git a/api/macchecker.js b/api/macchecker.js index 978fc284..0c0e23cb 100644 --- a/api/macchecker.js +++ b/api/macchecker.js @@ -64,15 +64,15 @@ const modifyData = (data) => { data.isLocal = isLocal ? true : false; data.isGlobal = !isLocal ? true : false; data.isUnicast = !isMulticast ? true : false; - data.macPrefix = data.macPrefix? data.macPrefix : 'N/A'; - data.company = data.company? data.company : 'N/A'; - data.country = data.country? data.country : 'N/A'; - data.address = data.address? data.address : 'N/A'; - data.updated = data.updated? data.updated : 'N/A'; - data.blockStart = data.blockStart? data.blockStart : 'N/A'; - data.blockEnd = data.blockEnd? data.blockEnd : 'N/A'; - data.blockSize = data.blockSize? data.blockSize : 'N/A'; - data.blockType = data.blockType? data.blockType : 'N/A'; + data.macPrefix = data.macPrefix ? data.macPrefix.match(/.{1,2}/g).join(':') : 'N/A'; + data.company = data.company ? data.company : 'N/A'; + data.country = data.country ? data.country : 'N/A'; + data.address = data.address ? data.address : 'N/A'; + data.updated = data.updated ? data.updated : 'N/A'; + data.blockStart = data.blockStart ? data.blockStart.match(/.{1,2}/g).join(':') : 'N/A'; + data.blockEnd = data.blockEnd ? data.blockEnd.match(/.{1,2}/g).join(':') : 'N/A'; + data.blockSize = data.blockSize ? data.blockSize : 'N/A'; + data.blockType = data.blockType ? data.blockType : 'N/A'; return data; } \ No newline at end of file diff --git a/api/maxmind.js b/api/maxmind.js new file mode 100644 index 00000000..62078645 --- /dev/null +++ b/api/maxmind.js @@ -0,0 +1,61 @@ +import maxmind from 'maxmind'; +import { isValidIP } from '../common/valid-ip.js'; +import { refererCheck } from '../common/referer-check.js'; + +let cityLookup, asnLookup; + +// 异步初始化数据库 +async function initDatabases() { + cityLookup = await maxmind.open('./common/maxmind-db/GeoLite2-City.mmdb'); + asnLookup = await maxmind.open('./common/maxmind-db/GeoLite2-ASN.mmdb'); +} + +initDatabases(); + +export default (req, res) => { + + // 限制只能从指定域名访问 + const referer = req.headers.referer; + if (!refererCheck(referer)) { + return res.status(403).json({ error: referer ? 'Access denied' : 'What are you doing?' }); + } + + const ip = req.query.ip; + if (!ip) { + return res.status(400).json({ error: 'No IP address provided' }); + } + + // 检查 IP 地址是否合法 + if (!isValidIP(ip)) { + return res.status(400).json({ error: 'Invalid IP address' }); + } + + // 获取请求语言 + const lang = req.query.lang === 'zh-CN' || req.query.lang === 'en' || req.query.lang === 'fr' ? req.query.lang : 'en'; + + try { + const city = cityLookup.get(ip); + const asn = asnLookup.get(ip); + let result = modifyJson(ip, lang, city, asn); + res.json(result); + } catch (e) { + res.status(500).json({ error: e.message }); + } +} + +function modifyJson(ip, lang, city, asn) { + city = city || {}; + asn = asn || {}; + return { + ip, + city: city.city ? city.city.names[lang] || city.city.names.en : "N/A", + region: city.subdivisions ? city.subdivisions[0].names[lang] || city.subdivisions[0].names.en : "N/A", + country: city.country ? city.country.iso_code : "N/A", + country_name: city.country ? city.country.names[lang] : "N/A", + country_code: city.country ? city.country.iso_code : "N/A", + latitude: city.location ? city.location.latitude : "N/A", + longitude: city.location ? city.location.longitude : "N/A", + asn: asn.autonomous_system_number ? "AS" + asn.autonomous_system_number : "N/A", + org: asn.autonomous_system_organization ? asn.autonomous_system_organization : "N/A" + }; +}; \ No newline at end of file diff --git a/backend-server.js b/backend-server.js index b358d5b1..19ff78d0 100644 --- a/backend-server.js +++ b/backend-server.js @@ -18,6 +18,7 @@ import whois from './api/whois.js'; import ipapiisHandler from './api/ipapiis.js'; import invisibilitytestHandler from './api/invisibilitytest.js'; import macChecker from './api/macchecker.js'; +import maxmindHandler from './api/maxmind.js'; dotenv.config(); @@ -137,6 +138,7 @@ app.get('/api/whois', whois); app.get('/api/ipapiis', ipapiisHandler); app.get('/api/invisibility', invisibilitytestHandler); app.get('/api/macchecker', macChecker); +app.get('/api/maxmind', maxmindHandler); // 使用查询参数处理所有配置请求 app.get('/api/configs', validateConfigs); diff --git a/common/maxmind-db/GeoLite2-ASN.mmdb b/common/maxmind-db/GeoLite2-ASN.mmdb new file mode 100644 index 00000000..e4e3fa06 Binary files /dev/null and b/common/maxmind-db/GeoLite2-ASN.mmdb differ diff --git a/common/maxmind-db/GeoLite2-City.mmdb b/common/maxmind-db/GeoLite2-City.mmdb new file mode 100644 index 00000000..b46ce8e9 Binary files /dev/null and b/common/maxmind-db/GeoLite2-City.mmdb differ diff --git a/frontend/components/SpeedTest.vue b/frontend/components/SpeedTest.vue index 05313814..a228cd8b 100644 --- a/frontend/components/SpeedTest.vue +++ b/frontend/components/SpeedTest.vue @@ -86,15 +86,15 @@ v-if="speedTestStatus === 'finished' && hasScores">

{{ t('speedtest.score') }} {{ t('speedtest.videoStreaming') }} - + {{ speedTest.streamingScore }} {{ t('speedtest.gaming') }} - + {{ speedTest.gamingScore }} {{ t('speedtest.rtc') }} - + {{ speedTest.rtcScore }} {{ t('speedtest.resultNote') }} @@ -365,4 +365,8 @@ defineExpose({ background-color: var(--bs-btn-hover-bg); border-color: var(--bs-btn-hover-border-color); } +.jn-text-warning { + --bs-text-opacity: 1; + color: #c67c14; +} diff --git a/frontend/components/advanced-tools/MacChecker.vue b/frontend/components/advanced-tools/MacChecker.vue index 2bd879ee..3b09be09 100644 --- a/frontend/components/advanced-tools/MacChecker.vue +++ b/frontend/components/advanced-tools/MacChecker.vue @@ -37,7 +37,7 @@

-
+

{{ t('macchecker.manufacturer') }}

@@ -84,7 +84,7 @@
-
+

{{ t('macchecker.property') }}

@@ -162,8 +162,9 @@ const tableItems = computed(() => { // 检查 MAC 是否有效 const validateInput = (input) => { if (!input) return null; - // 清理所有的分隔符 - const normalizedInput = input.replace(/[:-]/g, ''); + // 清理所有的分隔符和空格 + const normalizedInput = input.replace(/[:-]/g, '') + .replace(/\s+/g, ''); // 检查长度和格式 if (normalizedInput.length < 6 || normalizedInput.length > 12 || !/^[0-9A-Fa-f]+$/.test(normalizedInput)) { errorMsg.value = t('macchecker.invalidMAC'); diff --git a/frontend/store.js b/frontend/store.js index a821af74..80efe67c 100644 --- a/frontend/store.js +++ b/frontend/store.js @@ -39,6 +39,7 @@ export const useMainStore = defineStore('main', { { id: 4, text: 'KeyCDN', url: '/api/keycdn?ip={{ip}}', enabled: true }, { id: 5, text: 'IP.SB', url: '/api/ipsb?ip={{ip}}', enabled: true }, { id: 6, text: 'IPAPI.is', url: '/api/ipapiis?ip={{ip}}', enabled: true }, + { id: 7, text: 'MaxMind', url: '/api/maxmind?ip={{ip}}&lang={{lang}}', enabled: true }, ], }), diff --git a/frontend/utils/use-analytics.js b/frontend/utils/use-analytics.js index 2f6f711f..d915bb3b 100644 --- a/frontend/utils/use-analytics.js +++ b/frontend/utils/use-analytics.js @@ -1,12 +1,14 @@ import Analytics from 'analytics'; import googleAnalytics from '@analytics/google-analytics'; +const analyticsID = import.meta.env.VITE_GOOGLE_ANALYTICS_ID || ''; + // Google Analytics 配置 const analytics = Analytics({ app: 'MyIP', plugins: [ googleAnalytics({ - measurementIds: ['G-TEYKKD81TL'], + measurementIds: [analyticsID], }) ] }); diff --git a/package.json b/package.json index d474f678..e8b78194 100644 --- a/package.json +++ b/package.json @@ -27,17 +27,18 @@ "express-slow-down": "^2.0.3", "flag-icons": "^7.2.3", "http-proxy-middleware": "^3.0.0", + "maxmind": "^4.3.20", "nodemon": "^3.1.4", "pinia": "^2.1.7", "svgmap": "^2.10.1", - "vue": "^3.4.29", + "vue": "^3.4.31", "vue-i18n": "^9.13.1", "vue-router": "^4.4.0", "whoiser": "^1.17.3" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.5", - "vite": "^5.3.1", + "vite": "^5.3.3", "vite-plugin-pwa": "^0.20.0" } }