// ==UserScript== // @name 数英通用验证码 // @namespace http://tampermonkey.net/ // @version 2.0 // @description 数字和字母通用验证码,识别率可达95%以上。 // @author 哈士奇 // @include http://* // @include https://* // @license MIT // @grant unsafeWindow // @grant GM_addStyle // @grant GM_listValues // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_log // @grant GM_getResourceText // @grant GM_getResourceURL // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_getTab // @grant GM_saveTab // @grant GM_getTabs // @grant GM_notification // @grant GM_setClipboard // @grant GM_info // @grant GM_xmlhttpRequest // @connect * // @require https://cdn.jsdelivr.net/npm/vue@2.6.12 // @require https://unpkg.com/element-ui/lib/index.js // @resource elementUIcss https://unpkg.com/element-ui/lib/theme-chalk/index.css // @run-at document-end // @downloadURL none // ==/UserScript== (function() { // GM_setValue('tipsConfig',"") var elementUIcss = GM_getResourceText("elementUIcss"); GM_addStyle(elementUIcss); function getStyle(el) { // 获取元素样式 if (window.getComputedStyle) { return window.getComputedStyle(el, null); } else { return el.currentStyle; } } function init() { //简化各种api和初始化全局变量 CUR_URL = window.location.href; DOMAIN = CUR_URL.split("//")[1].split("/")[0]; selector = document.querySelector.bind(document); selectorAll = document.querySelectorAll.bind(document); getItem = localStorage.getItem.bind(localStorage); setItem = localStorage.setItem.bind(localStorage); } class BaiDuCloud { // 百度网盘自动填写验证码 constructor() { this.findBaiduCloudLink(); this.getCode(); // this.autoDownLoad() // 根据需要自主选择是否开启自动点击下载网盘文件 } findBaiduCloudLink() { let text = document.body.innerText.replace(/\x0A/gm, ""); // let panLink = text.match(/(pan.baidu.com[^\u4e00-\u9fa5]+)(提取码|密码).?\s?(\w{4})/gm); let panLink = text.match( /pan.baidu.com[^\u4e00-\u9fa5]+\s*\n?[\u4e00-\u9fa5]+.*?([a-z\d]{4})/gm ); if (!panLink) { return; } panLink.forEach((el) => { let link = el.match(/(pan.baidu.com[^\u4e00-\u9fa5]+)/gm)[0]; let code = el.match(/([a-z\d]{4}$)/gm)[0]; let key = "https://" + link.trim(); GM_gstValue(key, code); }); } getCode() { if (!CUR_URL.includes("pan.baidu.com") || !CUR_URL.includes("share")) { return; } let sourceId = CUR_URL.split("surl=")[1]; let url = "https://pan.baidu.com/s/1" + sourceId; let code = GM_getValue(url); if (code && selector("#accessCode") && selector("#accessCode")) { selector("#accessCode").value = code; selector("#submitBtn .submit-a").click(); setTimeout(() => { if (selector("#mpz1nz1")) { GM_log("哈士奇小助手:检测到提取码错误,当前密码:" + code); } }, 1000); } else { GM_log("哈士奇小助手:没有找到可使用的网盘验证码"); } } autoDownLoad() { // 到网盘下载界面后是否自动点击下载 if (!CUR_URL.includes("pan.baidu.com") || !CUR_URL.includes("/s")) { return; } let open = GM_getValue("autoDownload"); let filterUnuseLink = GM_getValue("filterUnuseLink"); if (!open || filterUnuseLink) { return; } let downBtn = selectorAll("a.g-button")[1]; let hasFileList = selector("#shareqr"); let text = document.body.innerText; if (hasFileList) { // 有文件列表,需要先勾选 selector(".zbyDdwb").click(); downBtn.click(); } else if ( document.body && (text.includes("不存在") || text.includes("删除") || text.includes("无法")) ) { setTimeout(() => { window.close(); }, 3000); } else if (downBtn) { //单个文件直接下载 downBtn.click(); } let timer = setInterval(() => { if ( selector(".module-yun-tip") && selector(".module-yun-tip").innerText.includes("启动网盘") ) { clearInterval(timer); } }, 1000); } } class Captcha { // 识别网页中的验证码 constructor() { this.imgCache = []; this.inputTags = []; this.recommendPath = {}; window.addEventListener("load", async() => { this.manualLocateCaptcha(); try { this.recommendPath = await this.getRecommendPath(); } catch (error) {} this.findCaptcha(); }); } dataURLtoFile(dataURL, filename = "captcha.jpg") { // base64转图片文件 var arr = dataURL.split(","), mime = (arr[0].match(/:(.*?);/) && arr[0].match(/:(.*?);/)[1]) || "image/png", bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new File([u8arr], filename, { type: mime }); } getRecommendPath() { let requestUrl = "http://101.43.206.185:7000/cssPath?href=" + location.href.split("?")[0]; try { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "get", url: requestUrl, onload: (res) => { if (res.status === 200 && res.response) { let data = JSON.parse(res.response); if (data.path && data.recommendTimes) { resolve(data); } } resolve({}); }, onerror: function(err) { console.log("推荐路径请求失败:" + err); resolve({}); }, }); }); } catch (error) { console.log(error); Promise.resolve({}); } } getCaptchaFeature(el) { // 获取验证码特征 let checkList = []; checkList.push(el.getAttribute("id")); checkList.push(el.className); checkList.push(el.getAttribute("alt")); checkList.push(el.getAttribute("src")); checkList.push(el.getAttribute("name")); return checkList; } cssPath = (el) => { // 获取元素css path if (!(el instanceof Element)) return; var path = []; while (el.nodeType === Node.ELEMENT_NODE) { var selector = el.nodeName.toLowerCase(); if (el.id) { selector += "#" + el.id; path.unshift(selector); break; } else { var sib = el, nth = 1; while ((sib = sib.previousElementSibling)) { if (sib.nodeName.toLowerCase() == selector) nth++; } if (nth != 1) selector += ":nth-of-type(" + nth + ")"; } path.unshift(selector); el = el.parentNode; } return path.join(" > "); }; manualLocateCaptcha() { let imgs = []; let inputTags = []; let cssPathStore = {}; let finish = false; this.vue = new Vue(); this.isIframe = top !== self; var onTagClick = (e) => { let el = e.target; let tagName = el.tagName; if (tagName.toLowerCase() === "input") { let type = el.getAttribute("type"); if (type && type !== "text") { this.vue.$message.error( "提醒:当前点击输入框type=" + type + ",请选择文本输入框" ); } else { cssPathStore.input = this.cssPath(el); this.vue.$message.success("您已成功选择输入框"); } } else { cssPathStore.img = this.cssPath(el); this.vue.$message.success("您已成功选择验证码图片"); } if (cssPathStore.input && cssPathStore.img) { GM_setValue("cssPath" + location.href, JSON.stringify(cssPathStore)); imgs.forEach((img) => { img && img.removeEventListener("click", onTagClick); }, false); inputTags.forEach((input) => { input.removeEventListener("click", onTagClick); }, false); setTimeout(() => { this.vue.$message.success("选择完毕,赶快试试吧"); }, 3000); finish = true; } }; var onMenuClick = (e) => { if (this.isIframe) { alert("当前脚本处于iframe中,暂不支持该操作,快让作者优化吧"); return; } finish = false; cssPathStore = {}; GM_deleteValue("cssPath" + location.href); this.vue.$alert("接下来请点击验证码图片和输入框", "操作提示", { confirmButtonText: "确定", callback: () => { setTimeout(() => { imgs.forEach((img) => { img && img.removeEventListener("click", onTagClick); }, false); inputTags.forEach((input) => { input.removeEventListener("click", onTagClick); }, false); if (!finish) { this.vue.$notify.success({ title: "提示", message: "已退出手动选择验证码模式。", offset: 100, }); } }, 20000); }, }); // alert("请点击验证码和输入框各一次。"); imgs = [...selectorAll("img")]; inputTags = [...selectorAll("input")]; imgs.forEach((img) => { img.addEventListener("click", onTagClick); }, false); inputTags.forEach((input) => { input.addEventListener("click", onTagClick); }, false); }; GM_registerMenuCommand("手动选择验证码和输入框", onMenuClick); } handleImg(img) { return new Promise((resolve, reject) => { try { // 图片没设置跨域,可采用图片转canvas转base64的方式 let dataURL = null; if (!img.src.includes(";base64,")) { let canvas = document.createElement("canvas"); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; let ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight); dataURL = canvas.toDataURL("image/png"); } else { dataURL = img.src; } resolve(dataURL); } catch (error) { console.error("error:" + error); // 这块处理比较复杂,待优化 // 图片设置跨域,重新请求图片内容后转base64,相当于替用户点击了“换一张图片” // if (this.times >= 1) { // return; // } // if (typeof Vue !== "undefined") { // new Vue().$notify.success({ // title: "温馨提示", // message: "当前验证码结果可能和图片显示不一致,请放心提交。", // offset: 100, // }); // } // this.times++; // GM_xmlhttpRequest({ // method: "get", // url: img.src, // responseType: "blob", // onload: (res) => { // if (res.status === 200) { // let blob = res.response; // let fileReader = new FileReader(); // fileReader.onloadend = (e) => { // let base64 = e.target.result; // resolve(base64); // }; // fileReader.readAsDataURL(blob); // } else { // console.log("图片转换blob失败"); // console.log(res); // reject(); // } // }, // onerror: function(err) { // console.log("图片请求失败:" + err); // reject(); // }, // }); } }); } hasRequest(dataURL, config = {}) { let imgClips = dataURL.slice(dataURL.length - 100, dataURL.length); if (this.imgCache.includes(imgClips)) { return true; } if (config.record) { this.imgCache.push(imgClips); } return false; } request(file, path, src) { try { if (!file) { console.error("缺少file参数"); return Promise.reject(); } return new Promise((resolve, reject) => { let host = location.href; let href = location.href.split("?")[0]; if (self === top) { host = location.host; } let formData = new FormData(); let detail = { path, src, host, href, }; formData.append("img", file); formData.append("detail", JSON.stringify(detail)); // let requestUrl = "http://192.168.31.184:7000/captcha"; let requestUrl = "http://101.43.206.185:7000/captcha"; GM_xmlhttpRequest({ method: "post", url: requestUrl, data: formData, onload: function(response) { if (response.status === -1) { console.error("获取验证码失败:" + response); reject(); } else { let data = response.response; if (data.length < 50) { data = JSON.parse(data); if (data.code) { resolve(data.code); } else { let date = new Date().getDate(); let tipsConfig = { date, times: 1, }; let cache = GM_getValue("tipsConfig") && JSON.parse(GM_getValue("tipsConfig")); if (cache && cache.times > 3) {} else { if (!cache) { GM_setValue("tipsConfig", JSON.stringify(tipsConfig)); } else { cache.times = cache.times + 1; GM_setValue("tipsConfig", JSON.stringify(cache)); } if (typeof Vue !== "undefined") { new Vue().$message.error(data.msg); } } console.error("获取验证码失败:" + data.msg); reject(); } } else { console.error("获取验证码失败:" + data); reject(); } } }, onerror: function(err) { console.error(err); reject(); }, }); }); } catch (error) { console.log(error); } } async findCaptcha() { let timer = setInterval(async() => { // 先读取用户手动设置的验证码配置 let cache = GM_getValue("cssPath" + location.href); let captchaPath = cache && JSON.parse(cache); if ( captchaPath && captchaPath.input && captchaPath.img && selector(captchaPath.input) && selector(captchaPath.img) && selector(captchaPath.img).getAttribute("src") ) { let dataURL = await this.handleImg(selector(captchaPath.img)); try { if (!this.hasRequest(dataURL, { record: true })) { selector(captchaPath.input).value = await this.request( this.dataURLtoFile(dataURL), this.cssPath(selector(captchaPath.input)) + "$$" + this.cssPath(selector(captchaPath.img)), selector(captchaPath.img).getAttribute("src") ); console.log("正在使用用户自定义验证码位置数据获取验证码"); } } catch (error) { console.log(error); } return; } // 自动寻找验证码和输入框 let captchaMap = []; let imgs = [...selectorAll("img")]; imgs.forEach((img) => { let checkList = [ ...this.getCaptchaFeature(img), ...this.getCaptchaFeature(img.parentNode), ]; checkList = checkList.filter((item) => item); let isInvalid = ["#", "about:blank"].includes(img.getAttribute("src")) || !img.getAttribute("src"); for (let i = 0; i < checkList.length; i++) { if ( /.*(code|captcha|验证码|login|点击|verify|yzm|yanzhengma).*/im.test( checkList[i].toLowerCase() ) && img.width > 30 && img.width < 150 && img.height < 80 && !isInvalid ) { captchaMap.push({ img: img, input: null }); break; } } }); captchaMap.forEach((item) => { let imgEle = item.img; let parentNode = imgEle.parentNode; for (let i = 0; i < 4; i++) { // 以当前可能是验证码的图片为基点,向上遍历四层查找可能的Input输入框 if (!parentNode) { return; } let inputTags = [...parentNode.querySelectorAll("input")]; if (inputTags.length) { let input = inputTags.pop(); let type = input.getAttribute("type"); while (type !== "text" && inputTags.length) { if (type === "password") { break; } input = inputTags.pop(); type = input.getAttribute("type"); } let inputWidth = getStyle(input).width.replace(/[^0-9]/gi, ""); // let inputHeight = getStyle(input).height.replace(/[^0-9]/gi, ""); if (!type || (type === "text" && inputWidth > 50)) { // 兼容各种奇葩情况 item.input = input; break; } if (type === "password") { // 验证码一般在密码框后面,遍历到密码框了就大概率说明没有验证码 break; } } parentNode = parentNode.parentNode; } }); // console.log(captchaMap); const { path, recommendTimes = 0 } = this.recommendPath; if (!captchaMap.length || recommendTimes > 1) { // 至少两个2网友推荐才会采用该数据,一定程度上避免他人污染数据 // 获取网友共享验证码位置数据 if (path) { let inputSelector = path.split("$$")[0]; let imgSelector = path.split("$$")[1]; if ( selector(inputSelector) && selector(imgSelector) && selector(imgSelector).getAttribute("src") ) { let dataURL = await this.handleImg(selector(imgSelector)); try { if (!this.hasRequest(dataURL, { record: true })) { let code = await this.request( this.dataURLtoFile(dataURL), path, selector(imgSelector).getAttribute("src") ); if (code) { selector(inputSelector).value = code; if (typeof Vue !== "undefined") { new Vue().$message.success("获取验证码成功"); } console.log("正在使用推荐验证码位置数据获取验证码"); } else { console.error("验证码为空,请检查图片是否正确"); } } return; } catch (error) { console.log(error); // if (typeof Vue !== "undefined") { // new Vue().$message.error("获取验证码失败"); // } } } } } captchaMap = captchaMap.filter((item) => item.input); captchaMap.forEach(async(item) => { let dataURL = await this.handleImg(item.img); try { if (!this.hasRequest(dataURL, { record: true })) { let code = await this.request( this.dataURLtoFile(dataURL), path, item.img.getAttribute("src") ); if (code) { item.input.value = code; if (typeof Vue !== "undefined") { new Vue().$message.success("获取验证码成功"); } console.log("正在使用自动寻找验证码功能获取验证码"); } else { console.error("验证码为空,请检查图片是否正确"); } } } catch (error) { console.log(error); // if (typeof Vue !== "undefined") { // new Vue().$message.error("获取验证码失败"); // } } }); }, 1000); window.addEventListener("beforeunload", () => { window.clearInterval(timer); }); } } function main() { window.addEventListener("DOMContentLoaded", function() { init(); new BaiDuCloud(); new Captcha(); }); } main(); })();