1/** 2 * 58pic Captcha SDK v1.0 3 * è¡ä¸ºéªè¯ç Web/H5 SDK 4 * 5 * ä½¿ç¨æ¹å¼ï¼ 6 * const captcha = new CaptchaSDK({ captchaId: 'your_captcha_id', apiBase: '/v1/captcha' }); 7 * captcha.render('#captcha-container'); 8 * captcha.onSuccess(validate => { // æäº¤ validate å°ä¸å¡å端 }); 9 */ 10(function (global) { 11 'use strict'; 12 13 // âââ èæ¬å¨æå è½½ âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 14 15 function _loadScript(src) { 16 return new Promise(function (resolve, reject) { 17 if (document.querySelector('script[src="' + src + '"]')) { resolve(); return; } 18 var s = document.createElement('script'); 19 s.src = src; s.onload = resolve; s.onerror = reject; 20 document.head.appendChild(s); 21 }); 22 } 23 24 function _settle(promise) { 25 return Promise.resolve(promise).then(function (value) { 26 return { status: 'fulfilled', value: value }; 27 }, function (reason) { 28 return { status: 'rejected', reason: reason }; 29 }); 30 } 31 32 function _buildQuery(params) { 33 var parts = []; 34 Object.keys(params).forEach(function (key) { 35 if (params[key] !== undefined && params[key] !== null && params[key] !== '') { 36 parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); 37 } 38 }); 39 return parts.join('&'); 40 } 41 42 function _dynamicImport(src) { 43 try { 44 return new Function('src', 'return ' + 'import(src)')(src); 45 } catch (e) { 46 return Promise.reject(e); 47 } 48 } 49 50 // âââ å å¯å·¥å · ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 51 52 const Crypto = { 53 /** 54 * çæéæº AES-256-GCM å¯é¥ 55 */ 56 async genAESKey() { 57 return crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt']); 58 }, 59 60 /** 61 * AES-GCM å å¯ï¼è¿å { iv, ciphertext } å为 Uint8Array 62 */ 63 async aesEncrypt(key, plaintext) { 64 const iv = crypto.getRandomValues(new Uint8Array(12)); 65 const enc = new TextEncoder(); 66 const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, enc.encode(plaintext)); 67 return { iv, ciphertext: new Uint8Array(ct) }; 68 }, 69 70 /** 71 * ç¨ RSA å ¬é¥ï¼PKCS#8 PEMï¼å å¯ AES keyï¼RSA-OAEP-SHA256ï¼ 72 */ 73 async rsaEncryptAESKey(pubKeyPem, aesKey) { 74 const rawKey = await crypto.subtle.exportKey('raw', aesKey); 75 const pubKey = await Crypto._importRSAPub(pubKeyPem); 76 const enc = await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, pubKey, rawKey); 77 return new Uint8Array(enc); 78 }, 79 80 async _importRSAPub(pem) { 81 const b64 = pem.replace(/-----[^-]+-----/g, '').replace(/\s/g, ''); 82 const der = Uint8Array.from(atob(b64), c => c.charCodeAt(0)); 83 return crypto.subtle.importKey( 84 'spki', der.buffer, 85 { name: 'RSA-OAEP', hash: 'SHA-256' }, 86 false, ['encrypt'] 87 ); 88 }, 89 90 toBase64(buf) { 91 return btoa(String.fromCharCode(...new Uint8Array(buf))); 92 }, 93 94 /** 95 * æå è¡ä¸ºæ°æ®ä¸º w åæ°ï¼Base64 JSONï¼ 96 */ 97 async packW(pubKeyPem, pubKeyId, payload) { 98 const aesKey = await Crypto.genAESKey(); 99 const { iv, ciphertext } = await Crypto.aesEncrypt(aesKey, JSON.stringify(payload)); 100 const encKey = await Crypto.rsaEncryptAESKey(pubKeyPem, aesKey); 101 const pkt = { 102 enc_key: Crypto.toBase64(encKey), 103 iv: Crypto.toBase64(iv), 104 ct: Crypto.toBase64(ciphertext), 105 kid: pubKeyId, 106 }; 107 return btoa(JSON.stringify(pkt)); 108 }, 109 }; 110 111 // âââ è¡ä¸ºéé ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 112 113 const Collector = { 114 /** 115 * éé设å¤ä¸ç¯å¢ä¿¡æ¯ 116 */ 117 device() { 118 const canvas = document.createElement('canvas'); 119 const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); 120 return { 121 ua: navigator.userAgent, 122 screen: `${screen.width}x${screen.height}`, 123 dpr: window.devicePixelRatio || 1, 124 tz: Intl.DateTimeFormat().resolvedOptions().timeZone, 125 lang: navigator.language, 126 canvas_fp: Collector._canvasFP(), 127 webgl_fp: gl ? Collector._webglFP(gl) : '', 128 fonts_cnt: 0, // M2 è¡¥å åä½åè¡¨æ¢æµ 129 }; 130 }, 131 132 env() { 133 return { 134 webdriver: !!navigator.webdriver, 135 headless: /HeadlessChrome/.test(navigator.userAgent), 136 plugins_cnt: navigator.plugins ? navigator.plugins.length : 0, 137 // æ£æµå¸¸è§èªå¨åæ¡æ¶ç¹å¾ 138 phantom: !!(window.callPhantom || window._phantom), 139 selenium: !!window.__selenium_evaluate || !!document.__selenium_evaluate, 140 }; 141 }, 142 143 _canvasFP() {
144 try { 145 const c = document.createElement('canvas'); 146 const ctx = c.getContext('2d'); 147 ctx.fillStyle = '#f60'; 148 ctx.fillRect(0, 0, 60, 20); 149 ctx.fillStyle = '#069'; 150 ctx.font = '14px Arial'; 151 ctx.fillText('58pic', 2, 15); 152 return c.toDataURL().slice(-32); 153 } catch (_) { return ''; } 154 }, 155 156 _webglFP(gl) { 157 try { 158 const ext = gl.getExtension('WEBGL_debug_renderer_info'); 159 return ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL).slice(0, 32) : ''; 160 } catch (_) { return ''; } 161 }, 162 163 /** 164 * 卿å®å ç´ ä¸ç»å®è½¨è¿¹ééï¼è¿å stop 彿° 165 */ 166 bindTrack(el, onTrack) { 167 const points = []; 168 const startTime = Date.now(); 169 170 function record(x, y) { 171 points.push([Math.round(x), Math.round(y * 100) / 100, Date.now() - startTime]); 172 } 173 174 function onMouseMove(e) { record(e.clientX, e.clientY); } 175 function onTouchMove(e) { 176 const t = e.touches[0]; 177 record(t.clientX, t.clientY); 178 e.preventDefault(); 179 } 180 181 el.addEventListener('mousemove', onMouseMove); 182 el.addEventListener('touchmove', onTouchMove, { passive: false }); 183 184 return function stop() { 185 el.removeEventListener('mousemove', onMouseMove); 186 el.removeEventListener('touchmove', onTouchMove); 187 onTrack(points); 188 }; 189 }, 190 }; 191 192 // âââ API 客æ·ç«¯ âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 193 194 function CaptchaAPI(apiBase) { 195 async function request(method, path, body) { 196 const res = await fetch(apiBase + path, { 197 method, 198 headers: { 'Content-Type': 'application/json' }, 199 body: body ? JSON.stringify(body) : undefined, 200 }); 201 const json = await res.json(); 202 if (json.code !== 0) throw Object.assign(new Error(json.msg), { code: json.code }); 203 return json.data; 204 } 205 206 return { 207 register(captchaId, clientType, riskAction, captchaType) { 208 const qs = _buildQuery({ 209 captcha_id: captchaId, 210 client_type: clientType, 211 risk_action: riskAction, 212 captcha_type: captchaType, 213 }); 214 return request('GET', `/register?${qs}`); 215 }, 216 load(lotNumber) { 217 return request('GET', `/load?lot_number=${lotNumber}`); 218 }, 219 async verify(lotNumber, captchaId, w) { 220 const res = await fetch(apiBase + '/verify', { 221 method: 'POST', 222 headers: { 'Content-Type': 'application/json' }, 223 body: JSON.stringify({ lot_number: lotNumber, captcha_id: captchaId, w }), 224 }); 225 const json = await res.json(); 226 if (json.code === 2001) { 227 return Object.assign({ captcha_failed: true, msg: json.msg }, json.data || {}); 228 } 229 if (json.code !== 0) throw Object.assign(new Error(json.msg), { code: json.code }); 230 return json.data; 231 }, 232 }; 233 } 234 235 // âââ æ»å UI âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 236 237 const CSS = ` 238.cp-wrap { position:relative; width:320px; background:#f7f7f7; border:1px solid #ddd; border-radius:4px; user-select:none; } 239.cp-img-box { position:relative; width:320px; height:160px; overflow:hidden; } 240.cp-bg { display:block; width:100%; height:100%; } 241.cp-slice { position:absolute; top:0; left:0; width:60px; height:60px; cursor:pointer; transition:left 0s; } 242.cp-bar { position:relative; height:40px; background:#f0f0f0; border-top:1px solid #ddd; border-radius:0 0 4px 4px; } 243.cp-track { position:absolute; left:0; top:0; height:100%; background:#d0e8ff; border-radius:0 0 0 4px; width:0; } 244.cp-btn { position:absolute; left:0; top:0; width:40px; height:40px; background:#fff; border:1px solid #ccc; border-radius:3px; cursor:grab; display:flex; align-items:center; justify-content:center; font-size:18px; } 245.cp-btn:active { cursor:grabbing; } 246.cp-tip { position:absolute; left:0; top:0; width:100%; height:100%; display:flex; align-items:center; justify-content:center; color:#999; font-size:13px; pointer-events:none; } 247.cp-status { position:absolute; left:0; top:0; width:100%; height:100%; display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:bold;
247 opacity:0; transition:opacity 0.3s; border-radius:0 0 4px 4px; pointer-events:none; } 248.cp-status.show { opacity:1; } 249.cp-status.ok { background:#d4f7d4; color:#2d7a2d; } 250.cp-status.fail { background:#ffd4d4; color:#a00; } 251.cp-refresh { position:absolute; right:6px; top:6px; cursor:pointer; font-size:16px; color:#999; background:rgba(255,255,255,.7); border-radius:50%; width:24px; height:24px; display:flex; align-items:center; justify-content:center; } 252.cp-loading { position:absolute; inset:0; background:rgba(255,255,255,.8); display:flex; align-items:center; justify-content:center; font-size:13px; color:#999; } 253/* ââ æ»å v2 ââ */ 254.cp-v2-wrap{position:relative;width:320px;background:#fff;border-radius:8px;user-select:none;overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,.1);} 255.cp-v2-accent{height:4px;background:linear-gradient(90deg,#1677ff,#40a9ff);} 256.cp-v2-title{padding:10px 14px 8px;font-size:14px;color:#333;text-align:center;font-weight:500;} 257.cp-v2-img-box{position:relative;width:320px;height:160px;overflow:hidden;} 258.cp-v2-bg{display:block;width:100%;height:100%;object-fit:cover;} 259.cp-v2-slice{position:absolute;left:0;width:auto;height:auto;pointer-events:none;} 260.cp-v2-bar-wrap{padding:8px 0 6px;} 261.cp-v2-bar{position:relative;height:44px;background:#f0f2f5;border-radius:22px;} 262.cp-v2-fill{position:absolute;left:0;top:0;height:100%;background:#dbeafe;border-radius:22px;width:44px;} 263.cp-v2-btn{position:absolute;left:0;top:0;width:44px;height:44px;background:#1677ff;border-radius:50%;cursor:grab;display:flex;align-items:center;justify-content:center;color:#fff;font-size:20px;box-shadow:0 2px 6px rgba(22,119,255,.5);z-index:2;} 264.cp-v2-btn:active{cursor:grabbing;} 265.cp-v2-tip{position:absolute;left:0;top:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#bbb;font-size:13px;pointer-events:none;padding-left:50px;} 266.cp-v2-status{position:absolute;left:0;right:0;top:0;bottom:0;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:bold;opacity:0;transition:opacity .3s;border-radius:22px;pointer-events:none;} 267.cp-v2-status.show{opacity:1;} 268.cp-v2-status.ok{background:rgba(212,247,212,.9);color:#2d7a2d;} 269.cp-v2-status.fail{background:rgba(255,212,212,.9);color:#a00;} 270.cp-v2-actions{display:flex;align-items:center;gap:18px;padding:4px 14px 10px;} 271.cp-v2-act{display:inline-flex;align-items:center;justify-content:center;color:#ccc;cursor:pointer;transition:color .15s;} 272.cp-v2-act:hover{color:#888;} 273/* ââ ç¹é v2 ââ */ 274.cp-ck-wrap{position:relative;width:320px;background:#fff;border-radius:8px;user-select:none;overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,.1);} 275.cp-ck-header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;} 276.cp-ck-instr{font-size:13px;color:#444;} 277.cp-ck-chars{border:2px solid #222;border-radius:2px;padding:2px 8px;font-size:16px;font-weight:700;color:#111;letter-spacing:2px;} 278.cp-ck-img-box{position:relative;width:320px;height:160px;overflow:hidden;cursor:pointer;} 279.cp-ck-bg{display:block;width:100%;height:100%;object-fit:cover;} 280.cp-ck-dot{position:absolute;width:26px;height:26px;border-radius:50%;background:rgba(22,119,255,.9);border:2px solid #fff;transform:translate(-50%,-50%);display:flex;align-items:center;justify-content:center;color:#fff;font-size:12px;font-weight:bold;pointer-events:none;box-shadow:0 1px 4px rgba(0,0,0,.3);} 281.cp-ck-btn-row{padding:8px 12px 6px;} 282.cp-ck-confirm{width:100%;height:36px;border:none;border-radius:4px;font-size:14px;background:#dce9f9;color:#a0bfe6;cursor:not-allowed;transition:background .2s,color .2s;} 283.cp-ck-confirm.ready{background:#1677ff;color:#fff;cursor:pointer;} 284.cp-ck-actions{display:flex;align-items:center;gap:18px;padding:4px 14px 10px;} 285.cp-ck-act{display:inline-flex;align-items:center;justify-content:center;color:#ccc;cursor:pointer;transition:color .15s;} 286.cp-ck-act:hover{color:#888;} 287`; 288 289 function injectCSS() { 290 if (document.getElementById('cp-style')) return; 291 const s = document.createElement('style'); 292 s.id = 'cp-style'; 293 s.textContent = CSS; 294 document.head.appendChild(s); 295 } 296 297 // âââ 主类 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 298 299 function CaptchaSDK(opts) { 300 const cfg = Object.assign({ 301 captchaId: '', 302 apiBase: '/v1/captcha', 303 clientType: 'web', 304 riskAction: '', 305 }, opts); 306 307 const api = CaptchaAPI(cfg.apiBase); 308 let _onSuccess = null;
309 let _onFail = null; 310 let _onNeedUI = null; 311 let _container = null; 312 let _session = null; 313 let _imgData = null; 314 let _validate = null; 315 316 // DOM èç¹å¼ç¨ 317 let elWrap, elBg, elSlice, elBtn, elTrack, elStatus, elLoading; 318 319 // âââ FingerprintJS + Botd åå§åï¼SDK å建æ¶ç«å³å¯å¨ï¼ä¸ register/load å¹¶è¡ï¼ 320 const _libBase = cfg.apiBase; 321 322 const _fpPromise = (async function () { 323 try { 324 await _loadScript(_libBase + '/lib/fp.min.js'); 325 const fp = await global.FingerprintJS.load(); 326 return await fp.get(); 327 } catch (_) { return null; } 328 }()); 329 330 const _botdPromise = (async function () { 331 try { 332 const mod = await _dynamicImport(_libBase + '/lib/botd.esm.js'); 333 const botd = await mod.load(); 334 return await botd.detect(); 335 } catch (_) { return null; } 336 }()); 337 338 function render(selector) { 339 _container = typeof selector === 'string' 340 ? document.querySelector(selector) : selector; 341 if (!_container) throw new Error('container not found: ' + selector); 342 injectCSS(); 343 _load(); 344 } 345 346 function _buildDOM() { 347 _container.innerHTML = ''; 348 elWrap = _el('div', 'cp-v2-wrap'); 349 350 const accent = _el('div', 'cp-v2-accent'); 351 352 const title = _el('div', 'cp-v2-title'); 353 title.textContent = '请æå¨æ»å宿æ¼å¾'; 354 355 const imgBox = _el('div', 'cp-v2-img-box'); 356 elBg = _el('img', 'cp-v2-bg'); 357 elBg.draggable = false; 358 elSlice = _el('img', 'cp-v2-slice'); 359 elSlice.draggable = false; 360 elLoading = _el('div', 'cp-loading'); 361 elLoading.textContent = 'å è½½ä¸â¦'; 362 imgBox.append(elBg, elSlice, elLoading); 363 364 const barWrap = _el('div', 'cp-v2-bar-wrap'); 365 const bar = _el('div', 'cp-v2-bar'); 366 elTrack = _el('div', 'cp-v2-fill'); 367 elBtn = _el('div', 'cp-v2-btn'); 368 elBtn.textContent = 'â'; 369 const tip = _el('div', 'cp-v2-tip'); 370 tip.textContent = 'å峿å¨å®æéªè¯'; 371 elStatus = _el('div', 'cp-v2-status'); 372 bar.append(elTrack, elBtn, tip, elStatus); 373 barWrap.appendChild(bar); 374 375 const actions = _el('div', 'cp-v2-actions'); 376 const closeAct = _el('span', 'cp-v2-act'); 377 closeAct.title = 'å ³é'; 378 closeAct.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>'; 379 closeAct.addEventListener('click', () => { if (_onFail) _onFail(new Error('user_cancelled')); }); 380 const refreshAct = _el('span', 'cp-v2-act'); 381 refreshAct.title = 'å·æ°'; 382 refreshAct.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>'; 383 refreshAct.addEventListener('click', _load); 384 actions.append(closeAct, refreshAct); 385 386 elWrap.append(accent, title, imgBox, barWrap, actions); 387 _container.appendChild(elWrap); 388 389 _bindDrag(); 390 } 391 392 async function _load(forceCaptchaType) { 393 _resetUI(); 394 _validate = null; 395 396 try { 397 const reg = await api.register(cfg.captchaId, cfg.clientType, cfg.riskAction, forceCaptchaType); 398 const pubKeyPem = reg.encrypt && reg.encrypt.pubkey_id 399 ? await _fetchPubKey(reg.encrypt.pubkey_id) 400 : null; 401 402 _session = { 403 lotNumber: reg.lot_number, 404 challenge: reg.challenge, 405 pubKeyId: reg.encrypt ? reg.encrypt.pubkey_id : '', 406 pubKeyPem, 407 captchaType: reg.captcha_type, 408 }; 409 410 const imgData = await api.load(reg.lot_number); 411 412 if (imgData.captcha_type === 'invisible') { 413 // æ ææ¨¡å¼ï¼ä¸æå»ºä»»ä½ UIï¼ç´æ¥éé»ééå¹¶æäº¤ 414 await _invisibleVerify(); 415 return; 416 } 417 418 // éè¦ç¨æ·äº¤äºï¼å éç¥ä¸å¡å±å±ç¤ºå®¹å¨ï¼åæå»º UI 419 if (_onNeedUI) _onNeedUI(imgData.captcha_type); 420 421 if (imgData.captcha_type === 'click') { 422 _buildClickDOM(imgData); 423 return; 424 } 425
426 // æ»åæµç¨ï¼å æå»º DOM åå¡«å å¾ç 427 _buildDOM(); 428 _setLoading(true); 429 _imgData = imgData; 430 elBg.src = imgData.bg; 431 elBg.onload = () => { 432 elSlice.src = imgData.slice; 433 elSlice.style.top = imgData.slice_y + 'px'; 434 elSlice.style.left = '0px'; 435 _setLoading(false); 436 }; 437 } catch (e) { 438 console.error('[captcha] load error:', e); 439 if (elStatus) _setStatus('å 载失败ï¼è¯·å·æ°', false); 440 } 441 } 442 443 // âââ ç¹ééªè¯ UI ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 444 445 function _buildClickDOM(imgData) { 446 _container.innerHTML = ''; 447 448 // ä» prompt "è¯·ä¾æ¬¡ç¹å»ï¼éªãè¯" 䏿åå符é¨å 449 const raw = imgData.prompt || ''; 450 const sep = raw.search(/[ï¼:]/); 451 const charsDisplay = sep >= 0 452 ? raw.slice(sep + 1).replace(/[ã,ï¼\s]/g, '') 453 : raw; 454 455 const wrap = _el('div', 'cp-ck-wrap'); 456 457 // 顶鍿 颿 458 const header = _el('div', 'cp-ck-header'); 459 const instr = _el('span', 'cp-ck-instr'); 460 instr.textContent = '请å¨ä¸å¾ä¾æ¬¡ç¹å»'; 461 const charsBox = _el('span', 'cp-ck-chars'); 462 charsBox.textContent = charsDisplay; 463 header.append(instr, charsBox); 464 465 // å¾çåº 466 const imgBox = _el('div', 'cp-ck-img-box'); 467 const bg = _el('img', 'cp-ck-bg'); 468 bg.src = imgData.bg; 469 bg.draggable = false; 470 imgBox.appendChild(bg); 471 472 // ç¡®å®æé®è¡ 473 const btnRow = _el('div', 'cp-ck-btn-row'); 474 const confirmBtn = _el('button', 'cp-ck-confirm'); 475 confirmBtn.textContent = 'ç¡® å®'; 476 confirmBtn.disabled = true; 477 btnRow.appendChild(confirmBtn); 478 479 // åºé¨æä½æ 480 const actions = _el('div', 'cp-ck-actions'); 481 const closeAct = _el('span', 'cp-ck-act'); 482 closeAct.title = 'å ³é'; 483 closeAct.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>'; 484 closeAct.addEventListener('click', () => { if (_onFail) _onFail(new Error('user_cancelled')); }); 485 const refreshAct = _el('span', 'cp-ck-act'); 486 refreshAct.title = 'å·æ°'; 487 refreshAct.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>'; 488 refreshAct.addEventListener('click', _load); 489 actions.append(closeAct, refreshAct); 490 491 wrap.append(header, imgBox, btnRow, actions); 492 _container.appendChild(wrap); 493 494 // ç¹å»ééé»è¾ 495 const clicks = []; 496 const targetCount = imgData.target_count || 2; 497 498 imgBox.addEventListener('click', function (e) { 499 if (_validate || clicks.length >= targetCount) return; 500 const rect = imgBox.getBoundingClientRect(); 501 const x = Math.round(e.clientX - rect.left); 502 const y = Math.round(e.clientY - rect.top); 503 504 const dot = _el('div', 'cp-ck-dot'); 505 dot.textContent = clicks.length + 1; 506 dot.style.left = x + 'px'; 507 dot.style.top = y + 'px'; 508 imgBox.appendChild(dot); 509 clicks.push([x, y]); 510 511 if (clicks.length >= targetCount) { 512 imgBox.style.pointerEvents = 'none'; 513 confirmBtn.disabled = false; 514 confirmBtn.classList.add('ready'); 515 } 516 }); 517 518 confirmBtn.addEventListener('click', async function () { 519 if (confirmBtn.disabled || clicks.length < targetCount) return; 520 confirmBtn.disabled = true; 521 confirmBtn.classList.remove('ready'); 522 confirmBtn.textContent = 'éªè¯ä¸â¦'; 523 await _clickVerify(clicks.slice()); 524 }); 525 } 526 527 async function _clickVerify(clicks) { 528 if (!_session) return; 529 const fp = await _collectFingerprint(); 530 const payload = { 531 challenge: _session.challenge, 532 setLeft: 0, passtime: 0, track: [], 533 clicks, 534 ...fp, 535 device: Collector.device(), 536 env: Collector.env(), 537 }; 538 539 let w;
540 try { 541 if (_session.pubKeyPem) { 542 w = await Crypto.packW(_session.pubKeyPem, _session.pubKeyId, payload); 543 } else { 544 w = btoa(JSON.stringify(payload)); 545 } 546 } catch (e) { return; } 547 548 try { 549 const res = await api.verify(_session.lotNumber, cfg.captchaId, w); 550 if (res.captcha_failed) throw Object.assign(new Error(res.msg || 'verify failed'), res); 551 _validate = res.validate; 552 const btn = _container.querySelector('.cp-ck-confirm'); 553 if (btn) { btn.style.background = '#52c41a'; btn.style.color = '#fff'; btn.textContent = 'éªè¯éè¿ â'; } 554 if (_onSuccess) _onSuccess(_validate, _successMeta()); 555 } catch (e) { 556 const btn = _container.querySelector('.cp-ck-confirm'); 557 if (btn) { btn.style.background = '#ff4d4f'; btn.style.color = '#fff'; btn.textContent = 'éªè¯å¤±è´¥ï¼è¯·éè¯'; } 558 setTimeout(_load, 1500); 559 if (_onFail) _onFail(e); 560 } 561 } 562 563 // æ æéªè¯ï¼æ¶é设å¤åç¯å¢ä¿¡å·ï¼æ éç¨æ·äº¤äº 564 // çå¾ FP + Botd ç»æï¼ä»»ä¸å¤±è´¥éé»é级 565 async function _collectFingerprint() { 566 const [fpResult, botdResult] = await Promise.all([_settle(_fpPromise), _settle(_botdPromise)]); 567 568 const fpVal = fpResult.status === 'fulfilled' ? fpResult.value : null; 569 const botdVal = botdResult.status === 'fulfilled' ? botdResult.value : null; 570 571 // ä» FingerprintJS components æåç¨äºä¸è´æ§æ ¡éªçåå§ä¿¡å· 572 let fpComponents = {}; 573 if (fpVal && fpVal.components) { 574 const c = fpVal.components; 575 const touchValue = c.touchSupport ? c.touchSupport.value : null; 576 fpComponents = { 577 fp_platform: c.platform ? String(c.platform.value || '') : '', 578 fp_touch_points: touchValue && touchValue.maxTouchPoints != null ? Number(touchValue.maxTouchPoints) : -1, 579 fp_screen: c.screenResolution ? (c.screenResolution.value || []) : [], 580 fp_hw_concurrency: c.hardwareConcurrency && c.hardwareConcurrency.value != null ? Number(c.hardwareConcurrency.value) : 0, 581 fp_color_depth: c.colorDepth && c.colorDepth.value != null ? Number(c.colorDepth.value) : 0, 582 }; 583 } 584 585 return { 586 visitor_id: fpVal ? fpVal.visitorId : '', 587 bot_detected: botdVal ? !!botdVal.bot : false, 588 bot_kind: botdVal ? (botdVal.botKind || '') : '', 589 ...fpComponents, 590 }; 591 } 592 593 async function _invisibleVerify() { 594 if (!_session) return; 595 596 const fp = await _collectFingerprint(); 597 const payload = { 598 challenge: _session.challenge, 599 setLeft: 0, 600 passtime: 0, 601 track: [], 602 ...fp, 603 device: Collector.device(), 604 env: Collector.env(), 605 }; 606 607 let w; 608 try { 609 if (_session.pubKeyPem) { 610 w = await Crypto.packW(_session.pubKeyPem, _session.pubKeyId, payload); 611 } else { 612 w = btoa(JSON.stringify(payload)); 613 console.warn('[captcha] pubkey not available, sending unencrypted w (dev only)'); 614 } 615 } catch (e) { 616 console.error('[captcha] encrypt error:', e); 617 return; 618 } 619 620 try { 621 const res = await api.verify(_session.lotNumber, cfg.captchaId, w); 622 623 if (res.next_captcha_type === 'slide' || res.next_captcha_type === 'click') { 624 await _load(res.next_captcha_type); 625 return; 626 } 627 if (res.captcha_failed) throw Object.assign(new Error(res.msg || 'verify failed'), res); 628 629 _validate = res.validate; 630 if (_onSuccess) _onSuccess(_validate, _successMeta()); 631 } catch (e) { 632 if (_onFail) _onFail(e); 633 } 634 } 635 636 // 仿å¡ç«¯æå RSA å ¬é¥ 637 async function _fetchPubKey(kid) { 638 try { 639 const res = await fetch(`${cfg.apiBase}/pubkey?kid=${encodeURIComponent(kid)}`); 640 const json = await res.json(); 641 if (json.code === 0 && json.data && json.data.pubkey) return json.data.pubkey; 642 } catch (_) {} 643 return window.__CAPTCHA_PUBKEY__ || null; // é级ï¼å ¨å±åé 644 } 645 646 function _bindDrag() { 647 let dragging = false;
648 let startX = 0; 649 let startTrack = null; 650 let trackPoints = []; 651 652 function onStart(clientX) { 653 if (!_imgData || _validate) return; 654 dragging = true; 655 startX = clientX; 656 trackPoints = []; 657 startTrack = Collector.bindTrack(elWrap, pts => { trackPoints = pts; }); 658 elBtn.style.transition = 'none'; 659 elTrack.style.transition = 'none'; 660 } 661 662 function onMove(clientX) { 663 if (!dragging) return; 664 const dx = Math.max(0, Math.min(clientX - startX, 320 - 44)); 665 elBtn.style.left = dx + 'px'; 666 elTrack.style.width = (dx + 44) + 'px'; 667 elSlice.style.left = dx + 'px'; 668 } 669 670 async function onEnd(clientX) { 671 if (!dragging) return; 672 dragging = false; 673 const stopTrack = startTrack; 674 startTrack = null; 675 if (stopTrack) stopTrack(); // 触å onTrack åè°ï¼å¡«å trackPoints 676 677 const setLeft = Math.round(Math.max(0, Math.min(clientX - startX, 320 - 44))); 678 const passtime = trackPoints.length 679 ? trackPoints[trackPoints.length - 1][2] - (trackPoints[0] ? trackPoints[0][2] : 0) 680 : 0; 681 682 await _verify(setLeft, passtime, trackPoints); 683 } 684 685 // Mouse 686 elBtn.addEventListener('mousedown', e => { onStart(e.clientX); e.preventDefault(); }); 687 document.addEventListener('mousemove', e => onMove(e.clientX)); 688 document.addEventListener('mouseup', e => onEnd(e.clientX)); 689 690 // Touch 691 elBtn.addEventListener('touchstart', e => { 692 onStart(e.touches[0].clientX); 693 e.preventDefault(); 694 }, { passive: false }); 695 document.addEventListener('touchmove', e => { 696 if (dragging) onMove(e.touches[0].clientX); 697 }); 698 document.addEventListener('touchend', e => { 699 if (dragging) onEnd(e.changedTouches[0].clientX); 700 }); 701 } 702 703 async function _verify(setLeft, passtime, track) { 704 if (!_session) return; 705 706 const fp = await _collectFingerprint(); 707 const payload = { 708 challenge: _session.challenge, 709 setLeft, 710 passtime, 711 track, 712 ...fp, 713 device: Collector.device(), 714 env: Collector.env(), 715 }; 716 717 let w; 718 try { 719 if (_session.pubKeyPem) { 720 w = await Crypto.packW(_session.pubKeyPem, _session.pubKeyId, payload); 721 } else { 722 // æ å ¬é¥é级ï¼base64 ææï¼ä» å¼åè°è¯ï¼çäº§å¿ é¡»å å¯ï¼ 723 w = btoa(JSON.stringify(payload)); 724 console.warn('[captcha] pubkey not available, sending unencrypted w (dev only)'); 725 } 726 } catch (e) { 727 console.error('[captcha] encrypt error:', e); 728 return; 729 } 730 731 try { 732 const res = await api.verify(_session.lotNumber, cfg.captchaId, w); 733 734 if (res.next_captcha_type === 'click') { 735 // æ»åå¤å®ä¸éè¿ï¼å级ç¹é 736 await _load('click'); 737 return; 738 } 739 if (res.captcha_failed) throw Object.assign(new Error(res.msg || 'verify failed'), res); 740 741 _validate = res.validate; 742 _setStatus('éªè¯éè¿', true); 743 elBtn.style.transition = ''; 744 if (_onSuccess) _onSuccess(_validate, _successMeta()); 745 } catch (e) { 746 _setStatus('éªè¯å¤±è´¥ï¼è¯·éè¯', false); 747 setTimeout(_load, 1500); 748 if (_onFail) _onFail(e); 749 } 750 } 751 752 function _resetUI() { 753 elBtn && (elBtn.style.left = '0px'); 754 elTrack && (elTrack.style.width = '44px'); 755 elStatus && elStatus.classList.remove('show', 'ok', 'fail'); 756 } 757 758 function _setStatus(msg, ok) { 759 elStatus.textContent = msg; 760 elStatus.className = 'cp-v2-status show ' + (ok ? 'ok' : 'fail'); 761 } 762 763 function _setLoading(show) { 764 if (elLoading) elLoading.style.display = show ? 'flex' : 'none'; 765 } 766 767 function _el(tag, cls) { 768 const el = document.createElement(tag); 769 if (cls) el.className = cls; 770 return el; 771 } 772 773 function _successMeta() { 774 return { 775 lotNumber: _session ? _session.lotNumber : null, 776 captchaType: _session ? _session.captchaType : null, 777 }; 778 } 779 780 return { 781 render, 782 onSuccess(fn) { _onSuccess = fn; return this; }, 783 onFail(fn) { _onFail = fn; return this; }, 784 onNeedUI(fn) { _onNeedUI = fn; return this; }, 785 reset: _load, 786 getValidate() { return _validate; }, 787 getLotNumber() { return _session ? _session.lotNumber : null; }, 788 }; 789 } 790
791 // æ´é²å°å ¨å±ï¼æµè§å¨ç¯å¢å§ç»æ windowï¼å ¼å®¹é¡µé¢ä¸å ¶ä»èæ¬æ±¡æ module åéçæ åµ 792 if (typeof window !== 'undefined') { 793 window.CaptchaSDK = CaptchaSDK; 794 } 795 if (typeof module !== 'undefined' && module.exports) { 796 module.exports = CaptchaSDK; 797 } 798 799}(typeof window !== 'undefined' ? window : this));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.