// ==UserScript==
// @name kone base64 자동복호화
// @namespace http://tampermonkey.net/
// @version 1.3.1
// @description base64코드 자동복호화
// @author SYJ
// @match https://arca.live/*
// @match https://kone.gg/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=arca.live
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addValueChangeListener
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @license MIT
// ==/UserScript==
// 자주 바뀜. 취약한 셀렉터
const SHADOW_ROOT_SELECTOR = "body main div.prose-container";
const MAX_DECODE_COUNT = 10+1;
window.addEventListener('load', ()=>setTimeout(main, 1000));
async function main(){
observeUrlChange(renderUI);
const isAutoMode = await GM_getValue('toggleVal', true);
if (isAutoMode) {
observeUrlChange(applyAuto);
}
else {
setTimeout(applyManually, 1000);
}
}
function applyManually() {
document.body.addEventListener('dblclick', function(e) {
console.log('더블클릭 감지! 🎉',e.target,event.composedPath()[0]);
const el = e.composedPath()[0];
const nodes = Array.from(el.childNodes).filter(node=>node.nodeType ===Node.TEXT_NODE)
console.log(nodes)
for (const node of nodes){
const original = node.textContent;
const decodedLink = doDecode(original);
// console.log(node, original, decodedLink);
if (original === decodedLink) continue;
linkifyTextNode(node, decodedLink);
}
})
}
function applyAuto() {
const contents = Array.from(document.body.querySelectorAll(`main ${textTagNames}`));
const mainContents = Array.from(document.querySelector(SHADOW_ROOT_SELECTOR)?.shadowRoot?.querySelectorAll(textTagNames) ?? []);
contents.push(...mainContents);
for (const tag of contents) {
const nodes = Array.from(tag.childNodes).filter(node=>node.nodeType ===Node.TEXT_NODE)
for (const node of nodes){
const original = node.textContent;
const decodedLink = doDecode(original);
if (original === decodedLink) continue;
linkifyTextNode(node, decodedLink);
}
}
console.log('더이상 디코드할 수 없는 목록 :', nonBase64Collection);
}
const textTagNames = 'p, span, div, a, li,' + // 일반 컨테이너
'h1, h2, h3, h4, h5, h6,' + // 제목 요소
'em, strong, u, b, i, small, mark, ' + // 인라인 포맷팅 요소
'label, button, option, textarea' // 폼/인터페이스 요소
// 텍스트노드에 존재하는 url을 a태그로 바꿈. (텍스트노드 -> 텍스트노드1 + a태그 + 텍스트노드2)
function linkifyTextNode(Node, text) { // 텍스트노드 중 url을 찾아 a태그로 변환. (액션 포함)
// URL 매칭 (https:// 로 시작해서 공백 전까지)
const urlRegex = /(https?:\/\/[^\s]+)/;
Node.textContent = text;
if (!urlRegex.test(text)) { // URL 없으면 텍스트 덮어씌우고 종료
return;
}
let node = Node;
while(urlRegex.test(node?.textContent ?? '')){
const match = urlRegex.exec(node.textContent);
const url = match[0];
const start = match.index;
const urlLen = url.length;
// "텍스트1 URL 텍스트2" 꼴의 텍스트노드를 세 개로 분리
// 1) URL 앞부분과 뒤를 분리
const textNode = document.createTextNode(node.textContent);
const afterUrlStart = textNode.splitText(start);
const afterUrlEnd = afterUrlStart.splitText(urlLen);
const beforeUrlStart = textNode;
// 3) <a> 요소 생성 후 URL 텍스트 노드 대신 교체. parent
const a = makeATag(url)
node.parentNode.replaceChild(a, node);
node = afterUrlEnd;
a.before(beforeUrlStart);
a.after(afterUrlEnd);
}
function makeATag(link){
const aTag = document.createElement('a');
aTag.href = link;
aTag.textContent = link;
aTag.target = '_blank';
aTag.rel = 'noreferrer';
return aTag;
}
}
function doDecode(text) {
///'use strict';
let result = text;
result = dec(/[0-9A-Za-z+/]{6,}[=]{0,2}/g, result); //문자열 6회 + '=' 0~2회
return result;
function dec(reg, text) {
let result = text;
const originals = Array.from(result.match(reg) ?? []);
for (const original of originals){
const decoded = decodeNtime(original);
result = result.replace(original, decoded);
}
return result;
}
}
// 노드 하나에 존재하는 모든 base64구문을 복원함.
function doDecode(text) {
///'use strict';
let result = text;
result = dec(/[0-9A-Za-z+/]{6,}[=]{0,2}/g, result); //문자열 6회 + '=' 0~2회
return result;
function dec(reg, text) {
let result = text;
const maps = Array.from(result.match(reg) ?? []) // base64 청크
.map(o=>({before:o, after:decodeNtime(o)})) // base64 to 원본 매핑
maps.forEach(({before, after})=>{result = result.replace(before, after)}); // 적용
return result;
}
}
// 원문으로 가능한 패턴 (한영숫자 + 자주쓰는 특문 + 한자)
// 허용 범위
// 한글 : \uAC00-\uD7A3
// 히라가나 : \u3040-\u309F
// 카타카나 : \u30A0-\u30FF
// CJK 한자 : \u3400-\u4DBF, \u4E00-\u9FFF, \uF900-\uFAFF
// CJK 구두점·전각 특수문자: \u3000-\u303F
// 전각 괄호 : \uFF08-\uFF09
// 영숫자 : A-Za-z0-9
// 반각 특수문자 : !@#\$%\^&\*\(\)_\-\+=\[\]\{\}\\|;:'",.<>\/\?
const WORD_TEST = /^[ㄱ-ㅎ가-힣A-Za-z0-9!@#\$%\^&\*\(\)_\-\+=\[\]\{\}\\|;:/'",.<>\/\?!@#$%^&*()_+-=`~|\s\uAC00-\uD7A3\u3040-\u309F\u30A0-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\u3000-\u303F\uFF08-\uFF09]+$/;
const nonBase64Collection = [];
function decodeNtime(str) {
let decoded = str;
for (let i=0; i<MAX_DECODE_COUNT; i++){
const old = decoded;
decoded = decodeOneTime(decoded);
if (decoded === old) return decoded;
}
function decodeOneTime(str) {
try {
const decoded = base64DecodeUnicode(str)
if (!WORD_TEST.test(decoded)) {
nonBase64Collection.push(str);
throw new Error('[정상 유니코드 범위가 아님]'+JSON.stringify(str)+JSON.stringify(decoded));}
return decoded;
}
catch(e) {
//console.log('[FAIL]',str, e);
return str; }
}
function base64DecodeUnicode0(str) {
// 1) atob으로 디코딩 → 바이너리(한 글자당 1바이트) 문자열
// 2) 각 문자 코드를 16진수 %xx 형태로 변환
// 3) decodeURIComponent로 UTF-8 해석
const percentEncodedStr = Array
.from(atob(str))
.map(char => '%' + char.charCodeAt(0).toString(16).padStart(2, '0'))
.join('');
return decodeURIComponent(percentEncodedStr);
}
function base64DecodeUnicode(str) {
// 1) atob으로 디코딩 → 1바이트 문자열
const binary = atob(str);
// 2) 각 문자(=바이트)를 숫자로 뽑아 Uint8Array 생성
const bytes = new Uint8Array(
Array.from(binary, ch => ch.charCodeAt(0))
);
// 3) TextDecoder로 'utf-8' 디코딩
return new TextDecoder('utf-8').decode(bytes);
}
}
// UI
async function renderUI() {
// 1) 값 로드
let val = await GM_getValue('toggleVal', false);
let menuId;
// 2) 배지 생성
/* const badge = document.createElement('div');
Object.assign(badge.style, {
position: 'fixed',
top: '10px',
right: '10px',
padding: '4px 8px',
background: 'rgba(0,0,0,0.7)',
color: '#fff',
fontSize: '14px',
borderRadius: '4px',
zIndex: '9999',
});
document.body.append(badge);
*/
// 3) 렌더 함수
function render() {
// 메뉴 해제 후 다시 등록
if (menuId) GM_unregisterMenuCommand(menuId);
menuId = GM_registerMenuCommand(
`자동모드 토글 (현재: ${val?'ON':'OFF'})`,
toggleValue
);
// 배지 업데이트
//badge.textContent = `현재 값: ${val}`;
}
// 4) 토글 함수 (즉시 UI 업데이트 포함)
async function toggleValue() {
const newVal = !val;
await GM_setValue('toggleVal', newVal);
val = newVal; // 변수 갱신
render(); // 메뉴·배지 즉시 갱신
}
// 초기 렌더
render();
}
const observeUrlChange = (func) => {
func();
let oldHref = document.location.href;
const body = document.querySelector('body');
const observer = new MutationObserver(mutations => {
if (oldHref !== document.location.href) {
oldHref = document.location.href;
setTimeout(func, 1000);
}
});
observer.observe(body, { childList: true, subtree: true });
};