D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Всех приветствую, это моя вторая статья и я заранее извиняюсь, если что-то будет непонятно или повествование моих мыслей окажется неправильным. В этой статье мы рассмотрим, как создать защищенную и анонимную систему чатов, используя Tor для обеспечения конфиденциальности пользователей и шифрование сообщений + сделаем удобную админ-панель на React для управления чатами и отображением сообщений в реальном времени.
Начало. Установка Node.js и NPM (Node Package Manager)
1. Скачиваем установщик с официального сайта Node.js ( https://nodejs.org/en )
2. Устанавливаем все строго по инструкциям из установщика.
3. После установки открываем командную строку и выполняем следующую команду:
Создание React проекта.
1. Создадим React проект с помощью create-react-app:
Код: Скопировать в буфер обмена
и перейдем в наш созданный проект
Код: Скопировать в буфер обмена
2. Установим библиотеки для работы с WebSocket и шифрованием:
Код: Скопировать в буфер обмена
3. Создаем структуру нашего проекта:
Реализация наших /components из проекта
1. Компонент Chat.js
Импортируем нужные нам библиотеки:
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
2. Компонент MessageInput.js
Импортируем React:
JavaScript: Скопировать в буфер обмена
Спойлер: Полный код компонента MessageInput
JavaScript: Скопировать в буфер обмена
3. Компонент AdminPanel.js
Компонент AdminPanel предназначен для отображения списка активных чатов и управления ими. Он использует WebSocket для получения данных о чатов и их отображения в интерфейсе.
Импорт React и необходимых зависимостей:
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
Сделаем отображение списка чатов:
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
Спойлер: Пример админ-панели
Спойлер: Готовый код компонента AdminPanel.js
JavaScript: Скопировать в буфер обмена
Добавим красивые стили для нашего проекта (src/styles/App.css) :
CSS: Скопировать в буфер обмена
Теперь напишем код для App.js
В начале импорты стандартных зависимостей: React, хук useState, useEffect для состояния и побочных эффектов, а также другие зависимости - WebSocket для соединения и CryptoJS для шифрования сообщений.
Импортируются компоненты Chat и AdminPanel, которые будут использоваться в зависимости от роли пользователя.
JavaScript: Скопировать в буфер обмена
Добавим состояния компонента:
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
Функция generateChatId генерирует уникальный идентификатор чата с использованием метода Math.random() и устанавливает его в состояние chatId.
Добавим WebSocket соединение:
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
JavaScript: Скопировать в буфер обмена
В компоненте используется секретный ключ secretKey, который может быть использован для шифрования сообщений в чате. Это делается с помощью библиотеки CryptoJS, хотя в коде это пока не реализовано в полной мере.
Спойлер: Готовый код компонента App.js
JavaScript: Скопировать в буфер обмена
И напишем index.js для рендеринга нашего React-приложения.
JavaScript: Скопировать в буфер обмена
Установка Tor:
1. Добавим репозиторий Tor:
Код: Скопировать в буфер обмена
2. Установим Tor:
3. Запустим Tor:
Проверим, работает ли наш проект? Введем команду
В заключении я хотел бы хотел подметить плюсы использования такой панели:
С этим проектом вы можете быть уверены, что ваш чат будет защищен от внешнего вмешательства и легко доступен пользователям по всему миру через сеть Tor. Благодарю за прочтение статьи, с радостью почитаю ваши комментарии, а особенно профессиональных кодеров на JS.
Автор: AGN
Специально для xss.is
Начало. Установка Node.js и NPM (Node Package Manager)
1. Скачиваем установщик с официального сайта Node.js ( https://nodejs.org/en )
2. Устанавливаем все строго по инструкциям из установщика.
3. После установки открываем командную строку и выполняем следующую команду:
node -v
Если установка прошла успешно, то эта команда покажет версию установленного Node.js. Еще необходимо проверить наличие NPM командойnpm -v
Нажмите, чтобы раскрыть...
Создание React проекта.
1. Создадим React проект с помощью create-react-app:
Код: Скопировать в буфер обмена
npx create-react-app tor-chat
и перейдем в наш созданный проект
Код: Скопировать в буфер обмена
cd tor-chat
2. Установим библиотеки для работы с WebSocket и шифрованием:
Код: Скопировать в буфер обмена
npm install crypto-js websocket
3. Создаем структуру нашего проекта:
/src
├── /components
│ ├── Chat.js
│ ├── AdminPanel.js
│ └── MessageInput.js
├── /styles
│ └── App.css
├── App.js
└── index.js
Нажмите, чтобы раскрыть...
Реализация наших /components из проекта
1. Компонент Chat.js
Импортируем нужные нам библиотеки:
JavaScript: Скопировать в буфер обмена
Код:
import React, { useState, useEffect } from 'react';
import { WebSocket } from 'ws';
import CryptoJS from 'crypto-js';
import MessageInput from './MessageInput';
- useState, useEffect: хуки React, используемые для управления состоянием и побочными эффектами.
- WebSocket: Объект для создания WebSocket-соединения.
- CryptoJS: Библиотека для работы с криптографией, в частности для шифрования и дешифрования сообщений.
- MessageInput: Компонент для ввода сообщений.
JavaScript: Скопировать в буфер обмена
Код:
const [messages, setMessages] = useState([]);
const [socket, setSocket] = useState(null);
const [messageInput, setMessageInput] = useState('');
- messages: Массив, который хранит все сообщения чата.
- socket: Состояние для хранения объекта WebSocket.
- messageInput: Строка, которая хранит текущий ввод сообщения.
JavaScript: Скопировать в буфер обмена
Код:
const secretKey = 'SK';
const encryptMessage = (message) => {
return CryptoJS.AES.encrypt(message, secretKey).toString();
};
const decryptMessage = (encryptedMessage) => {
const bytes = CryptoJS.AES.decrypt(encryptedMessage, secretKey);
return bytes.toString(CryptoJS.enc.Utf8);
};
- Для шифрования и дешифрования здесь используется библиотека CryptoJS.
- Для шифрования используется AES.encrypt, для дешифрования AES.encrypt соответственно.
- Параметр secretKey - это секретный ключ, который используется для шифрования и дешифрования сообщений.
JavaScript: Скопировать в буфер обмена
Код:
useEffect(() => {
const ws = new WebSocket('wss://example.onion');
setSocket(ws);
ws.onopen = () => {
console.log('Connected to WebSocket server');
ws.send(
JSON.stringify({
type: 'joinChat',
chatId,
})
);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'message') {
setMessages((prevMessages) => [
...prevMessages,
{ text: decryptMessage(data.message), sender: 'Admin' },
]);
}
if (data.type === 'file') {
setMessages((prevMessages) => [
...prevMessages,
{
text: `${data.fileName} (${data.fileType})`,
sender: 'Admin',
isFile: true,
fileData: data.fileData,
},
]);
}
};
return () => {
ws.close();
};
}, [chatId]);
- В useEffect устанавливается WebSocket соединение с сервером по адресу wss://sample.onion. В моментах, когда соединение открыто скрипт отправляет запрос на подключение к чату с указанным chatId. После этого каждое полученное сообщение будет расшифровываться и добавляться в список сообщений.
- ws.onopen: Когда соединение с WebSocket открыто, отправляется сообщение на сервер с запросом о присоединении к чату.
- ws.onmessage: Когда сервер отправляет сообщение, оно расшифровывается и добавляется в список сообщений.
JavaScript: Скопировать в буфер обмена
Код:
const handleSendMessage = () => {
if (messageInput.trim()) {
const encryptedMessage = encryptMessage(messageInput);
socket.send(
JSON.stringify({
type: 'message',
chatId,
message: encryptedMessage,
})
);
setMessages((prevMessages) => [
...prevMessages,
{ text: messageInput, sender: 'You' },
]);
setMessageInput('');
}
};
- Когда пользователь вводит сообщение и нажимает отправить, это сообщение шифруется с использованием encryptMessage и отправляется на сервер через WebSocket.
JavaScript: Скопировать в буфер обмена
Код:
return (
<div className="chat-container">
<div className="message-list">
{messages.map((message, index) => (
<div
key={index}
className={`message ${message.sender === 'You' ? 'you' : ''}`}
>
<p>{message.text}</p>
{message.isFile && (
<a href={message.fileData} download>
Download {message.fileName}
</a>
)}
</div>
))}
</div>
<MessageInput
value={messageInput}
onChange={(e) => setMessageInput(e.target.value)}
onSend={handleSendMessage}
/>
</div>
);
- Все сообщения отображаются в списке, и если сообщение содержит файл (передается через WebSocket), оно будет отображено с ссылкой на файл для скачивания.
- messages.map: Перебирает все сообщения и отображает их. Если сообщение содержит файл, добавляется ссылка для его скачивания.
- MessageInput: Компонент для ввода текста сообщения, который использует пропсы для обработки ввода и отправки сообщения.
JavaScript: Скопировать в буфер обмена
Код:
// src/components/Chat.js
import React, { useState, useEffect } from 'react';
import { WebSocket } from 'ws';
import CryptoJS from 'crypto-js';
import MessageInput from './MessageInput';
const Chat = ({ chatId }) => {
const [messages, setMessages] = useState([]);
const [socket, setSocket] = useState(null);
const [messageInput, setMessageInput] = useState('');
const secretKey = 'SK';
const encryptMessage = (message) => {
return CryptoJS.AES.encrypt(message, secretKey).toString();
};
const decryptMessage = (encryptedMessage) => {
const bytes = CryptoJS.AES.decrypt(encryptedMessage, secretKey);
return bytes.toString(CryptoJS.enc.Utf8);
};
useEffect(() => {
const ws = new WebSocket('wss://example.onion');
setSocket(ws);
ws.onopen = () => {
console.log('Connected to WebSocket server');
ws.send(
JSON.stringify({
type: 'joinChat',
chatId,
})
);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'message') {
setMessages((prevMessages) => [
...prevMessages,
{ text: decryptMessage(data.message), sender: 'Admin' },
]);
}
if (data.type === 'file') {
setMessages((prevMessages) => [
...prevMessages,
{
text: `${data.fileName} (${data.fileType})`,
sender: 'Admin',
isFile: true,
fileData: data.fileData,
},
]);
}
};
return () => {
ws.close();
};
}, [chatId]);
const handleSendMessage = () => {
if (messageInput.trim()) {
const encryptedMessage = encryptMessage(messageInput);
socket.send(
JSON.stringify({
type: 'message',
chatId,
message: encryptedMessage,
})
);
setMessages((prevMessages) => [
...prevMessages,
{ text: messageInput, sender: 'You' },
]);
setMessageInput('');
}
};
return (
<div className="chat-container">
<div className="message-list">
{messages.map((message, index) => (
<div
key={index}
className={`message ${message.sender === 'You' ? 'you' : ''}`}
>
<p>{message.text}</p>
{message.isFile && (
<a href={message.fileData} download>
Download {message.fileName}
</a>
)}
</div>
))}
</div>
<MessageInput
value={messageInput}
onChange={(e) => setMessageInput(e.target.value)}
onSend={handleSendMessage}
/>
</div>
);
};
export default Chat;
2. Компонент MessageInput.js
Импортируем React:
import React from 'react';
далее:JavaScript: Скопировать в буфер обмена
Код:
const MessageInput = ({ value, onChange, onSend }) => {
return (
<div className="message-input">
<input
type="text"
value={value}
onChange={onChange}
placeholder="Type your message"
/>
<button onClick={onSend}>Send</button>
</div>
);
};
- value: Текущее значение поля ввода (в нем хранится текст сообщения).
- onChange: Функция для обработки изменений в поле ввода.
- onSend: Функция для отправки сообщения.
export default MessageInput;
Спойлер: Полный код компонента MessageInput
JavaScript: Скопировать в буфер обмена
Код:
// src/components/MessageInput.js
import React from 'react';
const MessageInput = ({ value, onChange, onSend }) => {
return (
<div className="message-input">
<input
type="text"
value={value}
onChange={onChange}
placeholder="Type your message"
/>
<button onClick={onSend}>Send</button>
</div>
);
};
export default MessageInput;
3. Компонент AdminPanel.js
Компонент AdminPanel предназначен для отображения списка активных чатов и управления ими. Он использует WebSocket для получения данных о чатов и их отображения в интерфейсе.
Импорт React и необходимых зависимостей:
JavaScript: Скопировать в буфер обмена
Код:
import React, { useState, useEffect } from 'react';
import { WebSocket } from 'ws';
- Как и в других компонентах, в начале импортируются React, хук useState для управления состоянием, и хук useEffect для выполнения побочных эффектов.
- Импортируется WebSocket для создания подключения к серверу WebSocket.
JavaScript: Скопировать в буфер обмена
Код:
const [chats, setChats] = useState([]);
const [socket, setSocket] = useState(null);
- chats: Это состояние, которое хранит список активных чатов. Его начальное значение — пустой массив.
- socket: Это состояние для хранения объекта WebSocket.
JavaScript: Скопировать в буфер обмена
Код:
useEffect(() => {
const ws = new WebSocket('wss://example.onion');
setSocket(ws);
ws.onopen = () => {
console.log('Connected to WebSocket server');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'chatList') {
setChats(data.chatIds);
}
};
return () => {
ws.close();
};
}, []);
- В useEffect создается WebSocket-соединение с сервером. После успешного открытия соединения в onopen выводится сообщение в консоль.
- В onmessage обрабатываются данные, полученные от сервера. Если это список чатов (chatList), то обновляется состояние chats.
- ws.onopen: Когда соединение с сервером открыто, выводится сообщение в консоль.
- ws.onmessage: Когда сервер отправляет данные, они обрабатываются. Если данные содержат список чатов, обновляется состояние chats.
JavaScript: Скопировать в буфер обмена
Код:
const handleJoinChat = (chatId) => {
socket.send(
JSON.stringify({
type: 'joinChat',
chatId,
})
);
};
Сделаем отображение списка чатов:
JavaScript: Скопировать в буфер обмена
Код:
return (
<div className="admin-panel">
<h2>Active Chats</h2>
<ul>
{chats.map((chatId) => (
<li key={chatId} onClick={() => handleJoinChat(chatId)}>
Chat {chatId}
</li>
))}
</ul>
</div>
);
- chats.map: Перебирает список чатов и для каждого чата создает элемент списка (<li>). Когда элемент списка кликается, вызывается функция handleJoinChat с соответствующим chatId.
JavaScript: Скопировать в буфер обмена
Код:
return () => {
ws.close();
};
Спойлер: Пример админ-панели

Спойлер: Готовый код компонента AdminPanel.js
JavaScript: Скопировать в буфер обмена
Код:
// src/components/AdminPanel.js
import React, { useState, useEffect } from 'react';
import { WebSocket } from 'ws';
const AdminPanel = () => {
const [chats, setChats] = useState([]);
const [socket, setSocket] = useState(null);
useEffect(() => {
const ws = new WebSocket('wss://example.onion');
setSocket(ws);
ws.onopen = () => {
console.log('Connected to WebSocket server');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'chatList') {
setChats(data.chatIds);
}
};
return () => {
ws.close();
};
}, []);
const handleJoinChat = (chatId) => {
socket.send(
JSON.stringify({
type: 'joinChat',
chatId,
})
);
};
return (
<div className="admin-panel">
<h2>Active Chats</h2>
<ul>
{chats.map((chatId) => (
<li key={chatId} onClick={() => handleJoinChat(chatId)}>
Chat {chatId}
</li>
))}
</ul>
</div>
);
};
export default AdminPanel;
Добавим красивые стили для нашего проекта (src/styles/App.css) :
CSS: Скопировать в буфер обмена
Код:
/* src/styles/App.css */
body {
font-family: Arial, sans-serif;
background-color: #f4f4f9;
color: #333;
}
.app {
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 20px;
text-align: center;
}
h1 {
color: #5c6bc0;
}
.chat-container {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-top: 20px;
min-height: 300px;
}
.message-list {
max-height: 400px;
overflow-y: auto;
margin-bottom: 20px;
}
.message {
padding: 10px;
background-color: #e3e3e3;
border-radius: 5px;
margin: 5px 0;
}
.message.you {
background-color: #b2ff59;
align-self: flex-end;
}
.message-input {
display: flex;
justify-content: space-between;
}
.message-input input {
padding: 10px;
width: 80%;
border: 1px solid #ccc;
border-radius: 4px;
}
.message-input button {
padding: 10px;
background-color: #5c6bc0;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.message-input button:hover {
background-color: #3f4b8f;
}
.admin-panel {
text-align: left;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.admin-panel ul {
list-style: none;
padding: 0;
}
.admin-panel li {
padding: 10px;
background-color: #5c6bc0;
color: white;
border-radius: 4px;
margin-bottom: 5px;
cursor: pointer;
}
.admin-panel li:hover {
background-color: #3f4b8f;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f9;
}
#root {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #fafafa;
}
}
Теперь напишем код для App.js
В начале импорты стандартных зависимостей: React, хук useState, useEffect для состояния и побочных эффектов, а также другие зависимости - WebSocket для соединения и CryptoJS для шифрования сообщений.
Импортируются компоненты Chat и AdminPanel, которые будут использоваться в зависимости от роли пользователя.
JavaScript: Скопировать в буфер обмена
Код:
import React, { useState, useEffect } from 'react';
import './App.css';
import { WebSocket } from 'ws';
import CryptoJS from 'crypto-js';
import Chat from './components/Chat';
import AdminPanel from './components/AdminPanel';
Добавим состояния компонента:
JavaScript: Скопировать в буфер обмена
Код:
const [isAdmin, setIsAdmin] = useState(false);
const [chatId, setChatId] = useState(null);
const [socket, setSocket] = useState(null);
- isAdmin: Указывает, является ли пользователь администратором (по умолчанию — нет).
- chatId: Хранит идентификатор чата, который генерируется при старте чата.
- socket: Хранит объект WebSocket, который используется для связи с сервером.
JavaScript: Скопировать в буфер обмена
Код:
const generateChatId = () => {
const id = Math.random().toString(36).substring(7);
setChatId(id);
return id;
};
Добавим WebSocket соединение:
JavaScript: Скопировать в буфер обмена
Код:
useEffect(() => {
if (chatId) {
const ws = new WebSocket(`wss://example.onion/chat/${chatId}`);
setSocket(ws);
ws.onopen = () => {
console.log('Connected to WebSocket server');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'message') {
console.log('Received message: ', data.message);
}
};
return () => {
ws.close();
};
}
}, [chatId]);
- В useEffect создается WebSocket-соединение при наличии chatId. Соединение устанавливается по URL, включающему этот chatId.
- После открытия соединения выводится сообщение в консоль.
- Когда приходит сообщение от сервера, оно обрабатывается в onmessage, где можно, например, вывести его в консоль (в реальном приложении можно обновить состояние чата с новым сообщением).
- Важно, что соединение закрывается при размонтировании компонента, что предотвращает утечки памяти.
JavaScript: Скопировать в буфер обмена
Код:
return (
<div className="app">
<h1>Secure Tor Chat</h1>
{isAdmin ? (
<AdminPanel />
) : chatId ? (
<Chat chatId={chatId} socket={socket} secretKey={secretKey} />
) : (
<div>
<button onClick={() => setIsAdmin(true)}>Admin Login</button>
<button onClick={generateChatId}>Start Chat</button>
</div>
)}
</div>
);
- Если isAdmin равно true, то отображается компонент AdminPanel, который предоставляет администратору доступ к управлению чатами.
- Если есть chatId, отображается компонент Chat, передавая ему chatId, socket и secretKey для работы с шифрованием.
- Если ни одно из этих условий не выполнено (например, еще не создан чат), показываются кнопки для входа как администратор и создания нового чата.
JavaScript: Скопировать в буфер обмена
const secretKey = 'Sk';
В компоненте используется секретный ключ secretKey, который может быть использован для шифрования сообщений в чате. Это делается с помощью библиотеки CryptoJS, хотя в коде это пока не реализовано в полной мере.
Спойлер: Готовый код компонента App.js
JavaScript: Скопировать в буфер обмена
Код:
// src/App.js
import React, { useState, useEffect } from 'react';
import './App.css';
import { WebSocket } from 'ws';
import CryptoJS from 'crypto-js';
import Chat from './components/Chat';
import AdminPanel from './components/AdminPanel';
const App = () => {
const [isAdmin, setIsAdmin] = useState(false);
const [chatId, setChatId] = useState(null);
const [socket, setSocket] = useState(null);
const secretKey = 'Sk';
const generateChatId = () => {
const id = Math.random().toString(36).substring(7);
setChatId(id);
return id;
};
useEffect(() => {
if (chatId) {
const ws = new WebSocket(`wss://example.onion/chat/${chatId}`); // Подключение через Tor
setSocket(ws);
ws.onopen = () => {
console.log('Connected to WebSocket server');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'message') {
console.log('Received message: ', data.message);
}
};
return () => {
ws.close();
};
}
}, [chatId]);
return (
<div className="app">
<h1>Secure Tor Chat</h1>
{isAdmin ? (
<AdminPanel />
) : chatId ? (
<Chat chatId={chatId} socket={socket} secretKey={secretKey} />
) : (
<div>
<button onClick={() => setIsAdmin(true)}>Admin Login</button>
<button onClick={generateChatId}>Start Chat</button>
</div>
)}
</div>
);
};
export default App;
И напишем index.js для рендеринга нашего React-приложения.
JavaScript: Скопировать в буфер обмена
Код:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
reportWebVitals();
Установка Tor:
1. Добавим репозиторий Tor:
Код: Скопировать в буфер обмена
Код:
sudo add-apt-repository ppa:torproject/tor-browser
sudo apt update
2. Установим Tor:
sudo apt install tor
3. Запустим Tor:
sudo service tor start
Проверим, работает ли наш проект? Введем команду
npm start
в консоль, и если все работает, то ваш сайт будет доступен по адресу: wss://example.onion.В заключении я хотел бы хотел подметить плюсы использования такой панели:
- Безопасность: использование этой панели затрудняет ваш поиск различными OSINT организациями + позволяет оставаться анонимным (не разглашать свои контакты и т.д.)
- Экономия времени: таргету не придется устанавливать XMPP-клиент, регистрировать аккаунт, включать OTR шифрование и т.д.
- Удобство использования: таргету будет гораздо удобнее перейти по ссылке и вести диалог.
- как самостоятельно создать безопасную панель для общения между "админ - таргет" ;
- как выполнить деплой проекта в инфраструктуре Tor ;
С этим проектом вы можете быть уверены, что ваш чат будет защищен от внешнего вмешательства и легко доступен пользователям по всему миру через сеть Tor. Благодарю за прочтение статьи, с радостью почитаю ваши комментарии, а особенно профессиональных кодеров на JS.
Автор: AGN
Специально для xss.is