Crowdin CSRF Interceptor

Intercept Crowdin backend requests and display cookie and CSRF token

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Crowdin CSRF Interceptor
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Intercept Crowdin backend requests and display cookie and CSRF token
// @match        https://crowdin.com/*
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const PREFIX = "/backend/";
    let cookie = '', csrf = '';
    let triggerBtn = null, popup = null;

    function copyToClipboard(text) {
        navigator.clipboard.writeText(text).then(() => {
            console.log("%c[Crowdin] Copied to clipboard", "color: #10b981");
        });
    }

    function createTriggerButton() {
        if (triggerBtn) return;
        
        triggerBtn = document.createElement("button");
        Object.assign(triggerBtn.style, {
            position: 'fixed',
            bottom: '20px',
            right: '20px',
            zIndex: '999998',
            width: '48px',
            height: '48px',
            borderRadius: '50%',
            background: '#111827',
            color: '#ffffff',
            border: 'none',
            cursor: 'pointer',
            boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
            fontSize: '20px',
            display: 'none',
            transition: 'transform 0.2s, background 0.2s',
            fontFamily: 'monospace'
        });
        triggerBtn.textContent = '🔑';
        triggerBtn.title = 'Show Tokens';
        
        triggerBtn.onmouseover = () => triggerBtn.style.transform = 'scale(1.1)';
        triggerBtn.onmouseout = () => triggerBtn.style.transform = 'scale(1)';
        triggerBtn.onclick = showPopup;
        
        document.body.appendChild(triggerBtn);
    }

    function showPopup() {
        if (!cookie || !csrf || popup) return;

        popup = document.createElement("div");
        Object.assign(popup.style, {
            position: 'fixed',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%) scale(0.9)',
            opacity: '0',
            zIndex: '999999',
            background: '#ffffff',
            border: '1px solid #e5e7eb',
            borderRadius: '12px',
            padding: '24px',
            width: '420px',
            maxWidth: '90vw',
            boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
            fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
            fontSize: '14px',
            color: '#111827',
            transition: 'opacity 0.3s ease, transform 0.3s ease'
        });

        const escape = s => s.replace(/"/g, '"');
        const inputStyle = 'flex:1;padding:8px 12px;margin-top:6px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;font-family:monospace;background:#f9fafb;color:#111827';
        const btnStyle = 'padding:8px 12px;margin-top:6px;margin-left:8px;background:#111827;color:#ffffff;border:none;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;transition:background 0.2s;white-space:nowrap';

        popup.innerHTML = `
            <div style="font-size:18px;font-weight:600;margin-bottom:20px;color:#111827">Token Captured</div>
            <div style="margin-bottom:20px">
                <label style="display:block;font-weight:500;margin-bottom:6px;color:#374151">Cookie</label>
                <div style="display:flex;align-items:flex-start">
                    <input readonly value="${escape(cookie)}" style="${inputStyle}">
                    <button class="copy-cookie" style="${btnStyle}" onmouseover="this.style.background='#1f2937'" onmouseout="this.style.background='#111827'">Copy</button>
                </div>
            </div>
            <div style="margin-bottom:24px">
                <label style="display:block;font-weight:500;margin-bottom:6px;color:#374151">CSRF Token</label>
                <div style="display:flex;align-items:flex-start">
                    <input readonly value="${escape(csrf)}" style="${inputStyle}">
                    <button class="copy-csrf" style="${btnStyle}" onmouseover="this.style.background='#1f2937'" onmouseout="this.style.background='#111827'">Copy</button>
                </div>
            </div>
            <button class="close-btn" style="width:100%;padding:10px 16px;background:#f3f4f6;color:#111827;border:none;border-radius:6px;font-weight:500;cursor:pointer;transition:background 0.2s" onmouseover="this.style.background='#e5e7eb'" onmouseout="this.style.background='#f3f4f6'">Close</button>
        `;

        popup.querySelector('.copy-cookie').onclick = () => copyToClipboard(cookie);
        popup.querySelector('.copy-csrf').onclick = () => copyToClipboard(csrf);
        popup.querySelector('.close-btn').onclick = hidePopup;

        document.body.appendChild(popup);
        
        requestAnimationFrame(() => {
            popup.style.opacity = '1';
            popup.style.transform = 'translate(-50%, -50%) scale(1)';
        });
    }

    function hidePopup() {
        if (!popup) return;
        popup.style.opacity = '0';
        popup.style.transform = 'translate(-50%, -50%) scale(0.9)';
        setTimeout(() => {
            popup?.remove();
            popup = null;
        }, 300);
    }

    function handleCapture(newCookie, newCsrf) {
        cookie = newCookie;
        csrf = newCsrf;
        console.log("%c[Crowdin] Cookie:", "color: #3b82f6", cookie);
        console.log("%c[Crowdin] CSRF:", "color: #10b981", csrf);
        
        if (!triggerBtn) {
            const checkBody = setInterval(() => {
                if (document.body) {
                    clearInterval(checkBody);
                    createTriggerButton();
                    triggerBtn.style.display = 'block';
                }
            }, 100);
        } else {
            triggerBtn.style.display = 'block';
        }
    }

    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(_, url) {
        this._url = url;
        return originalOpen.apply(this, arguments);
    };

    const originalSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function() {
        this.addEventListener("load", () => {
            if (this._url?.startsWith(PREFIX)) {
                const token = this.__sentry_xhr_v2__?.request_headers?.["x-csrf-token"];
                if (token) handleCapture(document.cookie, token);
            }
        });
        return originalSend.apply(this, arguments);
    };
})();