Skip to content

Commit

Permalink
Add Whois Search
Browse files Browse the repository at this point in the history
  • Loading branch information
jason5ng32 committed May 7, 2024
1 parent 2f033d1 commit 07db811
Show file tree
Hide file tree
Showing 13 changed files with 342 additions and 13 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Notes: You can use my demo for free, and you can also deploy it yourself.
* 📡 **MTR Test**: Perform MTR tests on servers located in different regions around the world.
* 🔦 **DNS Resolver**: Performs DNS resolution of a domain name from multiple sources and obtains real-time resolution results that can be used for contamination determination.
* 🚧 **Censorship Check**: Check if a website is blocked in some countries.
* 📓 **Whois Search**: Perform whois information search for domain names or IP addresses
* 🌗 **Dark Mode**: Automatically toggles between dark and daylight modes based on system settings, with an option for manual switching.
* 📱 **Minimalist Mode**: A mobile-optimized mode that shortens page length for quick access to essential information..
* 🔍 **Search IP Information**: Provides a tool for querying information about any IP address.
Expand Down
1 change: 1 addition & 0 deletions README_FR.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Notes: Vous pouvez utiliser ma démo gratuitement et vous pouvez également la d
* 📡 **Test MTR** : Effectue des tests MTR sur des serveurs situés dans différentes régions du monde.
* 🔦 **Résolveur DNS** : effectue la résolution DNS d'un nom de domaine à partir de plusieurs sources, obtient les résultats de la résolution en temps réel et peut être utilisé pour la détermination de la contamination.
* 🚧 **Test de Censorship**: Vérifier si un site est bloqué dans certains pays.
* 📓 **Recherche Whois** : Effectuer une recherche d'informations Whois pour les noms de domaine ou les adresses IP
* 🌗 **Mode sombre** : Bascule automatiquement entre les modes sombre et clair en fonction des paramètres du système, avec une option de basculement manuel.
* 📱 **Mode minimaliste** : Un mode optimisé pour les mobiles qui réduit la longueur de la page pour un accès rapide aux informations essentielles.
* 🔍 **Recherche d'informations sur l'adresse IP** : Fournit un outil pour interroger des informations sur n'importe quelle adresse IP.
Expand Down
1 change: 1 addition & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
* 📡 **MTR 测试**:从分布在全球的多个服务器进行 MTR 测试,了解你与全球的连接路径
* 🔦 **DNS 解析器**:从多个渠道对域名进行 DNS 解析,获取实时的解析结果,可用于污染判断
* 🚧 **封锁测试**:检查特定的网站在部分国家是否被封锁
* 📓 **Whois 查询**:对域名或 IP 进行 whois 信息查询
* 🌗 **暗黑模式**:根据系统设置自动切换暗黑/白天模式,也可以手动切换
* 📱 **简约模式**:为移动版提供的专门模式,缩短页面长度,快速查看最重要的信息
* 🔍 **查任意 IP 信息**:可以通过小工具查询任意 IP 的信息
Expand Down
56 changes: 56 additions & 0 deletions api/whois.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import whoiser from 'whoiser';

function isValidIP(ip) {
const ipv4Pattern =
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const ipv6Pattern =
/^(([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4})|(([0-9a-fA-F]{1,4}:){0,6}([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:){0,6}([0-9a-fA-F]{1,4})?))$/;
return ipv4Pattern.test(ip) || ipv6Pattern.test(ip);
};

function isValidDomain(domain) {
const domainPattern = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i;
return domainPattern.test(domain);
}

export default async (req, res) => {

// 限制只能从指定域名访问
const allowedDomains = ['localhost', ...(process.env.ALLOWED_DOMAINS || '').split(',')];
const referer = req.headers.referer;

if (referer) {
const domain = new URL(referer).hostname;
if (!allowedDomains.includes(domain)) {
return res.status(403).json({ error: 'Access denied' });
}
} else {
return res.status(403).json({ error: 'What are you doing?' });
}

const query = req.query.q;
if (!query) {
return res.status(400).json({ error: 'No address provided' });
}

// 检查 IP 地址是否合法
if (!isValidIP(query) && !isValidDomain(query)) {
return res.status(400).json({ error: 'Invalid IP or address' });
}

if (isValidIP(query)) {
try {
const ipinfo = await whoiser.ip(query, { timeout: 5000,raw: true});
res.json(ipinfo);
} catch (e) {
res.status(500).json({ error: e.message });
}
} else {
try {
const domaininfo = await whoiser.domain(query, { ignorePrivacy: false, timeout: 5000, follow: 2,raw: true});
res.json(domaininfo);
} catch (e) {
res.status(500).json({ error: e.message });
}
}
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"vue": "^3.4.26",
"vue-i18n": "^9.13.1",
"vue-router": "^4.3.2",
"vuex": "^4.1.0"
"vuex": "^4.1.0",
"whoiser": "^1.17.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
Expand Down
2 changes: 2 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import validateConfigs from './api/configs.js';
import dnsResolver from './api/dnsresolver.js';
import rateLimit from 'express-rate-limit';
import { slowDown } from 'express-slow-down'
import whois from './api/whois.js';

dotenv.config();

Expand Down Expand Up @@ -131,6 +132,7 @@ app.get('/api/ipsb', ipsbHandler);
app.get('/api/cfradar', cfHander);
app.get('/api/recaptcha', recaptchaHandler);
app.get('/api/dnsresolver', dnsResolver);
app.get('/api/whois', whois);

// 使用查询参数处理所有配置请求
app.get('/api/configs', validateConfigs);
Expand Down
9 changes: 9 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,15 @@ export default {
},
description: this.$t('shortcutKeys.CensorshipCheck'),
},
{
keys: "w",
action: () => {
this.scrollToElement("AdvancedTools", 80);
this.$refs.advancedToolsRef.navigateAndToggleOffcanvas('/whois');
this.$trackEvent('Nav', 'NavClick', 'Whois');
},
description: this.$t('shortcutKeys.Whois'),
},
{
keys: "m",
action: () => {
Expand Down
4 changes: 4 additions & 0 deletions src/components/advancedtools.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export default {
{ path: '/ruletest', icon: '🚏', titleKey: 'ruletest.Title', noteKey: 'advancedtools.RuleTestNote' },
{ path: '/dnsresolver', icon: '🔦', titleKey: 'dnsresolver.Title', noteKey: 'advancedtools.DNSResolverNote' },
{ path: '/censorshipcheck', icon: '🚧', titleKey: 'censorshipcheck.Title', noteKey: 'advancedtools.CensorshipCheck' },
{ path: '/whois', icon: '📓', titleKey: 'whois.Title', noteKey: 'advancedtools.Whois' },
],
isFullScreen: false,
}
Expand All @@ -96,6 +97,9 @@ export default {
case '/censorshipcheck':
this.$trackEvent('Nav', 'NavClick', 'CensorshipCheck');
break;
case '/whois':
this.$trackEvent('Nav', 'NavClick', 'Whois');
break;
}
var offcanvas = new Offcanvas(document.getElementById('offcanvasTools'));
offcanvas.show();
Expand Down
215 changes: 215 additions & 0 deletions src/components/whois.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
<template>
<!-- Whois Resolver -->
<div class="whois-section mb-4">
<div class="jn-title2">
<h2 id="Whois" :class="{ 'mobile-h2': isMobile }">📓 {{ $t('whois.Title') }}</h2>
</div>
<div class="text-secondary">
<p>{{ $t('whois.Note') }}</p>
</div>
<div class="row">
<div class="col-12 mb-3">
<div class="card jn-card" :class="{ 'dark-mode dark-mode-border': isDarkMode }">
<div class="card-body mb-3">
<div class="col-12 col-md-auto">
<label for="queryURLorIP" class="col-form-label">{{ $t('whois.Note2') }}</label>
</div>

<div class="input-group mb-2 mt-2 ">
<input type="text" class="form-control" :class="{ 'dark-mode': isDarkMode }"
:disabled="whoisCheckStatus === 'running'" :placeholder="$t('whois.Placeholder')"
v-model="queryURLorIP" @keyup.enter="onSubmit" name="queryURLorIP" id="queryURLorIP"
data-1p-ignore>

<button class="btn btn-primary" @click="onSubmit"
:disabled="whoisCheckStatus === 'running' || !queryURLorIP">
<span v-if="whoisCheckStatus === 'idle'">{{
$t('whois.Run') }}</span>
<span v-if="whoisCheckStatus === 'running'" class="spinner-grow spinner-grow-sm"
aria-hidden="true"></span>
</button>

</div>

<div class="jn-placeholder">
<p v-if="errorMsg" class="text-danger">{{ errorMsg }}</p>
</div>

<!-- Results Table -->
<div v-if="whoisResults && Object.keys(whoisResults).length">

<h3 class="fs-6 alert alert-success ">{{ $t('whois.Note3') }}</h3>
<div v-if="type === 'domain'" class="accordion" id="whoisResultAccordion"
:data-bs-theme="isDarkMode ? 'dark' : ''">
<div class="accordion-item" v-for="(provider, index) in providers" :key="provider">
<h2 class="accordion-header" :id="'heading' + index">
<button class="accordion-button" type="button" data-bs-toggle="collapse"
:data-bs-target="'#collapse' + index"
:aria-expanded="index === 0 ? 'true' : 'false'"
:aria-controls="'collapse' + index" :class="{ collapsed: index !== 0 }">
<span><i class="bi" :class="'bi-' + (index + 1) + '-circle-fill'"></i>&nbsp; <strong>{{ $t('whois.Provider') }}: {{ provider.toUpperCase()
}}</strong></span>
</button>
</h2>
<div :id="'collapse' + index" class="accordion-collapse collapse"
:class="{ show: index === 0 }" :aria-labelledby="'heading' + index">
<div class="accordion-body">
<div class="card card-body border-0 mt-3"
:class="[isDarkMode ? 'bg-black text-light' : 'bg-light']">
<pre>{{ whoisResults[providers[index]].__raw }}</pre>
</div>
</div>
</div>
</div>
</div>

<div v-else class="card card-body mt-3"
:class="[isDarkMode ? 'bg-black text-light' : 'bg-light']">
<pre>{{ whoisResults.__raw }}</pre>
</div>

</div>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
import { ref, computed } from 'vue';
import { useStore } from 'vuex';
export default {
name: 'Whois',
setup() {
const store = useStore();
const isDarkMode = computed(() => store.state.isDarkMode);
const isMobile = computed(() => store.state.isMobile);
return {
isDarkMode,
isMobile,
};
},
data() {
return {
queryURLorIP: '',
whoisCheckStatus: 'idle',
errorMsg: '',
providers: [],
type: '',
whoisResults: {},
}
},
methods: {
// 检查 URL 输入是否有效
formatURL(domain) {
// 检查是否包含协议头,若没有则尝试为其添加 http:// 以便进行 URL 格式验证
if (!domain.match(/^https?:\/\//)) {
domain = 'http://' + domain;
}
try {
const url = new URL(domain);
const hostname = url.hostname;
const parts = hostname.split('.');
const mainDomain = parts.slice(-2).join('.');
if (mainDomain.match(/^[a-z0-9-]+(\.[a-z0-9-]+)*\.[a-z]{2,}$/i)) {
return mainDomain;
}
} catch {
}
return false;
},
// 检查 IP 输入是否有效
isValidIP(ip) {
const ipv4Pattern =
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const ipv6Pattern =
/^(([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4})|(([0-9a-fA-F]{1,4}:){0,6}([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:){0,6}([0-9a-fA-F]{1,4})?))$/;
return ipv4Pattern.test(ip) || ipv6Pattern.test(ip);
},
// 检查输入是否有效
validInput(input) {
if (this.formatURL(input)) {
this.type = 'domain';
return this.formatURL(input);
} else if (this.isValidIP(input)) {
this.type = 'ip';
return input;
} else {
this.errorMsg = this.$t('whois.invalidURL');
return false;
};
},
// 提交查询
onSubmit() {
this.$trackEvent('Section', 'StartClick', 'Whois');
this.errorMsg = '';
this.providers = [];
this.whoisResults = {};
const query = this.validInput(this.queryURLorIP);
if (query) {
this.getWhoisResults(query);
}
},
// 获取 Whois 结果
async getWhoisResults(query) {
this.whoisCheckStatus = 'running';
try {
const response = await fetch(`/api/whois?q=${query}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
this.getProviders(data);
if (this.type === 'domain' && this.providers.length >= 1) {
this.whoisResults = data;
this.errorMsg = '';
} else if (this.type === 'ip' && data.__raw) {
this.whoisResults = data;
this.errorMsg = '';
} else {
this.errorMsg = this.$t('whois.fetchError');
}
this.whoisCheckStatus = 'idle';
} catch (error) {
console.error('Error fetching Whois results:', error);
this.whoisCheckStatus = 'idle';
this.errorMsg = this.$t('whois.fetchError');
}
},
// 获取 Whois 服务商
getProviders(data) {
if (this.type === 'domain') {
for (const [key, value] of Object.entries(data)) {
if (key.match(/^[a-z0-9-]+(\.[a-z0-9-]+)*\.[a-z]{2,}$/i)) {
if (data[key].__raw) {
this.providers.push(key);
}
}
}
}
},
},
}
</script>

<style scoped>
.jn-placeholder {
height: 16pt;
}
</style>
Loading

0 comments on commit 07db811

Please sign in to comment.