function isValidIPv4(address) { const parts = address.split("."); if (parts.length !== 4) return false; for (const part of parts) { if (!/^\d{1,3}$/.test(part)) return false; const n = Number(part); if (n < 0 || n > 255) return false; if (part.length > 1 && part.startsWith("0")) return false; } return true; } function isValidIPv6(address) { if (typeof address !== "string") return false; let addr = address.trim(); if (addr.startsWith("[") && addr.endsWith("]")) addr = addr.slice(1, -1); const percentIndex = addr.indexOf("%"); if (percentIndex !== -1) addr = addr.slice(0, percentIndex); if (addr.length === 0) return false; if (!/^[0-9a-fA-F:.]+$/.test(addr)) return false; const hasEmbeddedIPv4 = addr.includes("."); if (hasEmbeddedIPv4) { const lastColon = addr.lastIndexOf(":"); if (lastColon === -1) return false; const tail = addr.slice(lastColon + 1); if (!isValidIPv4(tail)) return false; addr = `${addr.slice(0, lastColon)}:ipv4`; } const isGroup = (g) => g === "ipv4" || /^[0-9a-fA-F]{1,4}$/.test(g); const countGroups = (groups) => groups.reduce((sum, g) => sum + (g === "ipv4" ? 2 : 1), 0); if (addr.includes("::")) { if (addr.indexOf("::") !== addr.lastIndexOf("::")) return false; const [left, right] = addr.split("::"); const leftGroups = left ? left.split(":").filter(Boolean) : []; const rightGroups = right ? right.split(":").filter(Boolean) : []; if (!leftGroups.every(isGroup) || !rightGroups.every(isGroup)) return false; const total = countGroups(leftGroups) + countGroups(rightGroups); return total < 8; } const groups = addr.split(":"); if (groups.some((g) => g.length === 0)) return false; if (!groups.every(isGroup)) return false; const total = countGroups(groups); return total === 8; } function classifyIp(address) { if (!address) return "unknown"; if (isValidIPv4(address)) return "ipv4"; if (isValidIPv6(address)) return "ipv6"; return "unknown"; } function getClientIp(req) { return ( req.headers.get("CF-Connecting-IP") || (req.headers.get("X-Forwarded-For") || "").split(",")[0].trim() || "" ); } const CONTINENT_ZH = { AF: "非洲", AN: "南极洲", AS: "亚洲", EU: "欧洲", NA: "北美洲", OC: "大洋洲", SA: "南美洲", }; const COUNTRY_ZH = { AD: "安道尔", AE: "阿联酋", AF: "阿富汗", AG: "安提瓜和巴布达", AI: "安圭拉", AL: "阿尔巴尼亚", AM: "亚美尼亚", AO: "安哥拉", AQ: "南极洲", AR: "阿根廷", AS: "美属萨摩亚", AT: "奥地利", AU: "澳大利亚", AW: "阿鲁巴", AX: "奥兰群岛", AZ: "阿塞拜疆", BA: "波黑", BB: "巴巴多斯", BD: "孟加拉国", BE: "比利时", BF: "布基纳法索", BG: "保加利亚", BH: "巴林", BI: "布隆迪", BJ: "贝宁", BL: "圣巴泰勒米", BM: "百慕大", BN: "文莱", BO: "玻利维亚", BQ: "荷属加勒比", BR: "巴西", BS: "巴哈马", BT: "不丹", BV: "布韦岛", BW: "博茨瓦纳", BY: "白俄罗斯", BZ: "伯利兹", CA: "加拿大", CC: "科科斯群岛", CD: "刚果(金)", CF: "中非", CG: "刚果(布)", CH: "瑞士", CI: "科特迪瓦", CK: "库克群岛", CL: "智利", CM: "喀麦隆", CN: "中国", CO: "哥伦比亚", CR: "哥斯达黎加", CU: "古巴", CV: "佛得角", CW: "库拉索", CX: "圣诞岛", CY: "塞浦路斯", CZ: "捷克", DE: "德国", DJ: "吉布提", DK: "丹麦", DM: "多米尼克", DO: "多米尼加", DZ: "阿尔及利亚", EC: "厄瓜多尔", EE: "爱沙尼亚", EG: "埃及", EH: "西撒哈拉", ER: "厄立特里亚", ES: "西班牙", ET: "埃塞俄比亚", FI: "芬兰", FJ: "斐济", FK: "福克兰群岛", FM: "密克罗尼西亚", FO: "法罗群岛", FR: "法国", GA: "加蓬", GB: "英国", GD: "格林纳达", GE: "格鲁吉亚", GF: "法属圭亚那", GG: "根西岛", GH: "加纳", GI: "直布罗陀", GL: "格陵兰", GM: "冈比亚", GN: "几内亚", GP: "瓜德罗普", GQ: "赤道几内亚", GR: "希腊", GS: "南乔治亚和南桑威奇群岛", GT: "危地马拉", GU: "关岛", GW: "几内亚比绍", GY: "圭亚那", HK: "中国香港", HM: "赫德岛和麦克唐纳群岛", HN: "洪都拉斯", HR: "克罗地亚", HT: "海地", HU: "匈牙利", ID: "印度尼西亚", IE: "爱尔兰", IL: "以色列", IM: "马恩岛", IN: "印度", IO: "英属印度洋领地", IQ: "伊拉克", IR: "伊朗", IS: "冰岛", IT: "意大利", JE: "泽西岛", JM: "牙买加", JO: "约旦", JP: "日本", KE: "肯尼亚", KG: "吉尔吉斯斯坦", KH: "柬埔寨", KI: "基里巴斯", KM: "科摩罗", KN: "圣基茨和尼维斯", KP: "朝鲜", KR: "韩国", KW: "科威特", KY: "开曼群岛", KZ: "哈萨克斯坦", LA: "老挝", LB: "黎巴嫩", LC: "圣卢西亚", LI: "列支敦士登", LK: "斯里兰卡", LR: "利比里亚", LS: "莱索托", LT: "立陶宛", LU: "卢森堡", LV: "拉脱维亚", LY: "利比亚", MA: "摩洛哥", MC: "摩纳哥", MD: "摩尔多瓦", ME: "黑山", MF: "法属圣马丁", MG: "马达加斯加", MH: "马绍尔群岛", MK: "北马其顿", ML: "马里", MM: "缅甸", MN: "蒙古", MO: "中国澳门", MP: "北马里亚纳群岛", MQ: "马提尼克", MR: "毛里塔尼亚", MS: "蒙特塞拉特", MT: "马耳他", MU: "毛里求斯", MV: "马尔代夫", MW: "马拉维", MX: "墨西哥", MY: "马来西亚", MZ: "莫桑比克", NA: "纳米比亚", NC: "新喀里多尼亚", NE: "尼日尔", NF: "诺福克岛", NG: "尼日利亚", NI: "尼加拉瓜", NL: "荷兰", NO: "挪威", NP: "尼泊尔", NR: "瑙鲁", NU: "纽埃", NZ: "新西兰", OM: "阿曼", PA: "巴拿马", PE: "秘鲁", PF: "法属波利尼西亚", PG: "巴布亚新几内亚", PH: "菲律宾", PK: "巴基斯坦", PL: "波兰", PM: "圣皮埃尔和密克隆", PN: "皮特凯恩群岛", PR: "波多黎各", PS: "巴勒斯坦", PT: "葡萄牙", PW: "帕劳", PY: "巴拉圭", QA: "卡塔尔", RE: "留尼汪", RO: "罗马尼亚", RS: "塞尔维亚", RU: "俄罗斯", RW: "卢旺达", SA: "沙特阿拉伯", SB: "所罗门群岛", SC: "塞舌尔", SD: "苏丹", SE: "瑞典", SG: "新加坡", SH: "圣赫勒拿", SI: "斯洛文尼亚", SJ: "斯瓦尔巴和扬马延", SK: "斯洛伐克", SL: "塞拉利昂", SM: "圣马力诺", SN: "塞内加尔", SO: "索马里", SR: "苏里南", SS: "南苏丹", ST: "圣多美和普林西比", SV: "萨尔瓦多", SX: "荷属圣马丁", SY: "叙利亚", SZ: "斯威士兰", TC: "特克斯和凯科斯群岛", TD: "乍得", TF: "法属南部领地", TG: "多哥", TH: "泰国", TJ: "塔吉克斯坦", TK: "托克劳", TL: "东帝汶", TM: "土库曼斯坦", TN: "突尼斯", TO: "汤加", TR: "土耳其", TT: "特立尼达和多巴哥", TV: "图瓦卢", TW: "中国台湾", TZ: "坦桑尼亚", UA: "乌克兰", UG: "乌干达", UM: "美国本土外小岛屿", US: "美国", UY: "乌拉圭", UZ: "乌兹别克斯坦", VA: "梵蒂冈", VC: "圣文森特和格林纳丁斯", VE: "委内瑞拉", VG: "英属维尔京群岛", VI: "美属维尔京群岛", VN: "越南", VU: "瓦努阿图", WF: "瓦利斯和富图纳", WS: "萨摩亚", XK: "科索沃", YE: "也门", YT: "马约特", ZA: "南非", ZM: "赞比亚", ZW: "津巴布韦", }; const CN_REGION_ZH = { AH: "安徽", BJ: "北京", CQ: "重庆", FJ: "福建", GD: "广东", GS: "甘肃", GX: "广西", GZ: "贵州", HA: "河南", HB: "湖北", HE: "河北", HI: "海南", HK: "香港", HL: "黑龙江", HN: "湖南", JL: "吉林", JS: "江苏", JX: "江西", LN: "辽宁", MO: "澳门", NM: "内蒙古", NX: "宁夏", QH: "青海", SC: "四川", SD: "山东", SH: "上海", SN: "陕西", SX: "山西", TJ: "天津", TW: "台湾", XJ: "新疆", XZ: "西藏", YN: "云南", ZJ: "浙江", }; const CN_CITY_ZH = { "Beijing": "北京", "Shanghai": "上海", "Tianjin": "天津", "Chongqing": "重庆", "Shijiazhuang": "石家庄", "Tangshan": "唐山", "Qinhuangdao": "秦皇岛", "Handan": "邯郸", "Xingtai": "邢台", "Baoding": "保定", "Zhangjiakou": "张家口", "Chengde": "承德", "Cangzhou": "沧州", "Langfang": "廊坊", "Hengshui": "衡水", "Taiyuan": "太原", "Datong": "大同", "Yangquan": "阳泉", "Changzhi": "长治", "Jincheng": "晋城", "Shuozhou": "朔州", "Jinzhong": "晋中", "Yuncheng": "运城", "Xinzhou": "忻州", "Linfen": "临汾", "Lüliang": "吕梁", "Lvliang": "吕梁", "Luliang": "吕梁", "Hohhot": "呼和浩特", "Baotou": "包头", "Wuhai": "乌海", "Chifeng": "赤峰", "Tongliao": "通辽", "Ordos": "鄂尔多斯", "Hulunbuir": "呼伦贝尔", "Hulun Buir": "呼伦贝尔", "Bayannur": "巴彦淖尔", "Ulanqab": "乌兰察布", "Wulanchabu": "乌兰察布", "Hinggan": "兴安", "Xilingol": "锡林郭勒", "Xilin Gol": "锡林郭勒", "Alxa": "阿拉善", "Alashan": "阿拉善", "Shenyang": "沈阳", "Dalian": "大连", "Anshan": "鞍山", "Fushun": "抚顺", "Benxi": "本溪", "Dandong": "丹东", "Jinzhou": "锦州", "Yingkou": "营口", "Fuxin": "阜新", "Liaoyang": "辽阳", "Panjin": "盘锦", "Tieling": "铁岭", "Chaoyang": "朝阳", "Huludao": "葫芦岛", "Changchun": "长春", "Jilin": "吉林", "Siping": "四平", "Liaoyuan": "辽源", "Tonghua": "通化", "Baishan": "白山", "Songyuan": "松原", "Baicheng": "白城", "Yanbian": "延边", "Harbin": "哈尔滨", "Qiqihar": "齐齐哈尔", "Jixi": "鸡西", "Hegang": "鹤岗", "Shuangyashan": "双鸭山", "Daqing": "大庆", "Jiamusi": "佳木斯", "Qitaihe": "七台河", "Mudanjiang": "牡丹江", "Heihe": "黑河", "Suihua": "绥化", "Daxing'anling": "大兴安岭", "Daxinganling": "大兴安岭", "Nanjing": "南京", "Wuxi": "无锡", "Xuzhou": "徐州", "Changzhou": "常州", "Nantong": "南通", "Lianyungang": "连云港", "Huai'an": "淮安", "Huaian": "淮安", "Yancheng": "盐城", "Yangzhou": "扬州", "Zhenjiang": "镇江", "Suqian": "宿迁", "Hangzhou": "杭州", "Ningbo": "宁波", "Wenzhou": "温州", "Jiaxing": "嘉兴", "Huzhou": "湖州", "Shaoxing": "绍兴", "Jinhua": "金华", "Quzhou": "衢州", "Zhoushan": "舟山", "Lishui": "丽水", "Hefei": "合肥", "Wuhu": "芜湖", "Bengbu": "蚌埠", "Huainan": "淮南", "Ma'anshan": "马鞍山", "Maanshan": "马鞍山", "Huaibei": "淮北", "Tongling": "铜陵", "Anqing": "安庆", "Huangshan": "黄山", "Chuzhou": "滁州", "Fuyang": "阜阳", "Lu'an": "六安", "Luan": "六安", "Bozhou": "亳州", "Chizhou": "池州", "Xuancheng": "宣城", "Xiamen": "厦门", "Putian": "莆田", "Sanming": "三明", "Quanzhou": "泉州", "Zhangzhou": "漳州", "Nanping": "南平", "Longyan": "龙岩", "Ningde": "宁德", "Nanchang": "南昌", "Jingdezhen": "景德镇", "Pingxiang": "萍乡", "Jiujiang": "九江", "Xinyu": "新余", "Yingtan": "鹰潭", "Ganzhou": "赣州", "Ji'an": "吉安", "Jian": "吉安", "Shangrao": "上饶", "Jinan": "济南", "Qingdao": "青岛", "Zibo": "淄博", "Zaozhuang": "枣庄", "Dongying": "东营", "Yantai": "烟台", "Weifang": "潍坊", "Jining": "济宁", "Tai'an": "泰安", "Taian": "泰安", "Weihai": "威海", "Rizhao": "日照", "Linyi": "临沂", "Dezhou": "德州", "Liaocheng": "聊城", "Binzhou": "滨州", "Heze": "菏泽", "Zhengzhou": "郑州", "Kaifeng": "开封", "Luoyang": "洛阳", "Pingdingshan": "平顶山", "Anyang": "安阳", "Hebi": "鹤壁", "Xinxiang": "新乡", "Jiaozuo": "焦作", "Puyang": "濮阳", "Xuchang": "许昌", "Luohe": "漯河", "Sanmenxia": "三门峡", "Nanyang": "南阳", "Shangqiu": "商丘", "Xinyang": "信阳", "Zhoukou": "周口", "Zhumadian": "驻马店", "Jiyuan": "济源", "Wuhan": "武汉", "Huangshi": "黄石", "Shiyan": "十堰", "Yichang": "宜昌", "Xiangyang": "襄阳", "Ezhou": "鄂州", "Jingmen": "荆门", "Xiaogan": "孝感", "Jingzhou": "荆州", "Huanggang": "黄冈", "Xianning": "咸宁", "Suizhou": "随州", "Enshi": "恩施", "Xiantao": "仙桃", "Qianjiang": "潜江", "Tianmen": "天门", "Shennongjia": "神农架", "Changsha": "长沙", "Zhuzhou": "株洲", "Xiangtan": "湘潭", "Hengyang": "衡阳", "Shaoyang": "邵阳", "Yueyang": "岳阳", "Changde": "常德", "Zhangjiajie": "张家界", "Yiyang": "益阳", "Chenzhou": "郴州", "Yongzhou": "永州", "Huaihua": "怀化", "Loudi": "娄底", "Xiangxi": "湘西", "Guangzhou": "广州", "Shaoguan": "韶关", "Shenzhen": "深圳", "Zhuhai": "珠海", "Shantou": "汕头", "Foshan": "佛山", "Jiangmen": "江门", "Zhanjiang": "湛江", "Maoming": "茂名", "Zhaoqing": "肇庆", "Huizhou": "惠州", "Meizhou": "梅州", "Shanwei": "汕尾", "Heyuan": "河源", "Yangjiang": "阳江", "Qingyuan": "清远", "Dongguan": "东莞", "Zhongshan": "中山", "Chaozhou": "潮州", "Jieyang": "揭阳", "Yunfu": "云浮", "Nanning": "南宁", "Liuzhou": "柳州", "Guilin": "桂林", "Wuzhou": "梧州", "Beihai": "北海", "Fangchenggang": "防城港", "Qinzhou": "钦州", "Guigang": "贵港", "Baise": "百色", "Bose": "百色", "Hechi": "河池", "Laibin": "来宾", "Chongzuo": "崇左", "Hezhou": "贺州", "Haikou": "海口", "Sanya": "三亚", "Sansha": "三沙", "Danzhou": "儋州", "Wuzhishan": "五指山", "Wanning": "万宁", "Dongfang": "东方", "Qionghai": "琼海", "Wenchang": "文昌", "Chengdu": "成都", "Zigong": "自贡", "Panzhihua": "攀枝花", "Luzhou": "泸州", "Deyang": "德阳", "Mianyang": "绵阳", "Guangyuan": "广元", "Suining": "遂宁", "Neijiang": "内江", "Leshan": "乐山", "Nanchong": "南充", "Meishan": "眉山", "Yibin": "宜宾", "Guang'an": "广安", "Guangan": "广安", "Dazhou": "达州", "Ya'an": "雅安", "Yaan": "雅安", "Bazhong": "巴中", "Ziyang": "资阳", "Aba": "阿坝", "Ngawa": "阿坝", "Garze": "甘孜", "Ganzi": "甘孜", "Liangshan": "凉山", "Guiyang": "贵阳", "Liupanshui": "六盘水", "Zunyi": "遵义", "Anshun": "安顺", "Bijie": "毕节", "Tongren": "铜仁", "Qianxinan": "黔西南", "Qiandongnan": "黔东南", "Qiannan": "黔南", "Kunming": "昆明", "Qujing": "曲靖", "Yuxi": "玉溪", "Baoshan": "保山", "Zhaotong": "昭通", "Lijiang": "丽江", "Pu'er": "普洱", "Puer": "普洱", "Lincang": "临沧", "Chuxiong": "楚雄", "Honghe": "红河", "Wenshan": "文山", "Xishuangbanna": "西双版纳", "Dali": "大理", "Dehong": "德宏", "Nujiang": "怒江", "Diqing": "迪庆", "Deqen": "迪庆", "Lhasa": "拉萨", "Chamdo": "昌都", "Changdu": "昌都", "Shannan": "山南", "Lhoka": "山南", "Shigatse": "日喀则", "Xigaze": "日喀则", "Nagqu": "那曲", "Naqu": "那曲", "Ngari": "阿里", "Ali": "阿里", "Nyingchi": "林芝", "Linzhi": "林芝", "Xi'an": "西安", "Xian": "西安", "Tongchuan": "铜川", "Baoji": "宝鸡", "Xianyang": "咸阳", "Weinan": "渭南", "Yan'an": "延安", "Yanan": "延安", "Hanzhong": "汉中", "Ankang": "安康", "Shangluo": "商洛", "Lanzhou": "兰州", "Jiayuguan": "嘉峪关", "Jinchang": "金昌", "Baiyin": "白银", "Tianshui": "天水", "Wuwei": "武威", "Zhangye": "张掖", "Pingliang": "平凉", "Jiuquan": "酒泉", "Qingyang": "庆阳", "Dingxi": "定西", "Longnan": "陇南", "Linxia": "临夏", "Gannan": "甘南", "Xining": "西宁", "Haidong": "海东", "Haibei": "海北", "Huangnan": "黄南", "Guoluo": "果洛", "Golog": "果洛", "Yushu": "玉树", "Haixi": "海西", "Yinchuan": "银川", "Shizuishan": "石嘴山", "Wuzhong": "吴忠", "Guyuan": "固原", "Zhongwei": "中卫", "Urumqi": "乌鲁木齐", "Karamay": "克拉玛依", "Turpan": "吐鲁番", "Turfan": "吐鲁番", "Hami": "哈密", "Changji": "昌吉", "Bortala": "博尔塔拉", "Bole": "博乐", "Bayingol": "巴音郭楞", "Bayingolin": "巴音郭楞", "Aksu": "阿克苏", "Kizilsu": "克孜勒苏", "Kashgar": "喀什", "Hotan": "和田", "Ili": "伊犁", "Yili": "伊犁", "Tacheng": "塔城", "Altay": "阿勒泰", "Shihezi": "石河子", "Aral": "阿拉尔", "Alar": "阿拉尔", "Tumxuk": "图木舒克", "Tumushuke": "图木舒克", "Wujiaqu": "五家渠", "Beitun": "北屯", "Hong Kong": "香港", "Macau": "澳门", "Macao": "澳门", "Taipei": "台北", "New Taipei": "新北", "Kaohsiung": "高雄", "Taichung": "台中", "Tainan": "台南", "Hsinchu": "新竹", "Keelung": "基隆", "Chiayi": "嘉义", "Taoyuan": "桃园", "Pingtung": "屏东", "Changhua": "彰化", "Hualien": "花莲", "Yilan": "宜兰", "Nantou": "南投", "Miaoli": "苗栗", "Yunlin": "云林", "Taitung": "台东", "Penghu": "澎湖", "Kinmen": "金门", }; const CN_CITY_DISAMBIG = { "Taizhou": { JS: "泰州", ZJ: "台州" }, "Fuzhou": { FJ: "福州", JX: "抚州" }, "Yichun": { HL: "伊春", JX: "宜春" }, "Yulin": { GX: "玉林", SN: "榆林" }, "Suzhou": { JS: "苏州", AH: "宿州" }, }; function getRegionName(country, region, regionCode) { if (country === "CN" && regionCode) return CN_REGION_ZH[regionCode] || region || ""; return region || ""; } function getCityName(country, city, regionCode) { if (country !== "CN" && country !== "HK" && country !== "MO" && country !== "TW") return city || ""; const disambig = CN_CITY_DISAMBIG[city]; if (disambig && regionCode && disambig[regionCode]) return disambig[regionCode]; return CN_CITY_ZH[city] || city || ""; } function jsonResponse(payload, reqUrl, { status = 200, headers = {} } = {}) { const pretty = reqUrl.searchParams.get("pretty"); const body = pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload); return new Response(body, { status, headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", ...headers, }, }); } export default { async fetch(request) { const url = new URL(request.url); const path = url.pathname; // --- CORS(给 API 路径用) --- const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,OPTIONS", "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Max-Age": "86400", }; // 预检 if (request.method === "OPTIONS") { return new Response(null, { status: 204, headers: corsHeaders }); } // 只提供 GET if (request.method !== "GET") { return new Response("Method Not Allowed", { status: 405 }); } // 取公网 IP(优先 CF-Connecting-IP) const ip = getClientIp(request); const ipVersion = classifyIp(ip); // Cloudflare 地理/网络信息(在边缘注入;本地 dev 可能为空) const cf = request.cf || {}; const apiPayload = { ip, ipVersion, userAgent: request.headers.get("User-Agent") || "", // 常见 cf 字段(不同账号/计划/场景可用字段会有差异) geo: { country: cf.country || "", countryName: COUNTRY_ZH[cf.country] || cf.country || "", region: cf.region || "", regionName: getRegionName(cf.country, cf.region, cf.regionCode), regionCode: cf.regionCode || "", city: cf.city || "", cityName: getCityName(cf.country, cf.city, cf.regionCode), postalCode: cf.postalCode || "", timezone: cf.timezone || "", continent: cf.continent || "", continentName: CONTINENT_ZH[cf.continent] || cf.continent || "", latitude: cf.latitude || "", longitude: cf.longitude || "", }, network: { asn: cf.asn || "", asOrganization: cf.asOrganization || "", colo: cf.colo || "", httpProtocol: cf.httpProtocol || "", }, ts: new Date().toISOString(), // 如果你想“全量返回”,可把 cf 全部带回去(注意体积) // cf, }; // API:/api if (path === "/api") { return jsonResponse(apiPayload, url, { headers: { ...corsHeaders } }); } // 仅返回 IPv4:/ipv4 if (path === "/ipv4") { const expected = "ipv4"; if (!ip || ipVersion === "unknown") { return jsonResponse( { ok: false, expected, got: ipVersion, ip, error: "IP not found", ts: new Date().toISOString(), }, url, { status: 400, headers: { ...corsHeaders } }, ); } if (ipVersion !== expected) { return jsonResponse( { ok: false, expected, got: ipVersion, ip, error: "Expected IPv4, but request reached Cloudflare over a non-IPv4 connection.", hint: "服务器无法强制切换 IP 协议栈;请在客户端侧使用 IPv4 访问(例如 curl -4),或检查网络/DNS 的优先级。", ts: new Date().toISOString(), }, url, { status: 400, headers: { ...corsHeaders } }, ); } return jsonResponse( { ok: true, ip, ipVersion, ts: new Date().toISOString() }, url, { headers: { ...corsHeaders } }, ); } // 仅返回 IPv6:/ipv6 if (path === "/ipv6") { const expected = "ipv6"; if (!ip || ipVersion === "unknown") { return jsonResponse( { ok: false, expected, got: ipVersion, ip, error: "IP not found", ts: new Date().toISOString(), }, url, { status: 400, headers: { ...corsHeaders } }, ); } if (ipVersion !== expected) { return jsonResponse( { ok: false, expected, got: ipVersion, ip, error: "Expected IPv6, but request reached Cloudflare over IPv4.", hint: [ "服务器端无法把一次 IPv4 请求“改成”IPv6;是否走 v4/v6 由客户端网络与 DNS 决定(Happy Eyeballs)。", `命令行可用:curl -6 ${url.origin}/ipv6`, "若你需要“强制 IPv6”,建议绑定一个仅 AAAA 解析的域名/子域名到本 Worker,再用该域名访问 /ipv6。", ], ts: new Date().toISOString(), }, url, { status: 400, headers: { ...corsHeaders } }, ); } return jsonResponse( { ok: true, ip, ipVersion, ts: new Date().toISOString() }, url, { headers: { ...corsHeaders } }, ); } // 网页:/ if (path === "/" || path === "/index.html") { const html = `
Cloudflare Workers: 根据访问者 IP 返回地理/网络信息(数据来自 request.cf 与 CF-Connecting-IP)。
GET /api:返回 JSON(支持 ?pretty=1)。GET /ipv4:返回 JSON(仅允许 IPv4 连接;否则 400)。GET /ipv6:返回 JSON(仅允许 IPv6 连接;否则 400)。API 默认允许跨域(Access-Control-Allow-Origin: *)。
提示:浏览器/系统会自动选择 IPv4/IPv6(可能优先走 IPv4)。如需强制测试 IPv6,请用 curl -6 或使用仅 AAAA 解析的域名访问。
curl ${url.origin}/api?pretty=1
curl -4 ${url.origin}/ipv4?pretty=1
curl -6 ${url.origin}/ipv6?pretty=1
Loading…