flask_socketio+pyautogui实现的具有加密传输功能的极简远程桌面:
python
import base64
import pyautogui
from flask import Flask
import time
from flask_socketio import SocketIO, emit
from io import BytesIO
import pyperclip
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad,unpad
from Crypto.Random import get_random_bytes
# 32字节AES密钥
global AES_KEY
AES_KEY='11111111112222222222333333333344'
# 加密函数
def aes_encrypt(plain_text, key):
"""AES加密"""
key_bytes=key.encode('utf-8')
iv=get_random_bytes(AES.block_size)
# 初始化AES加密器
cipher = AES.new(key_bytes, AES.MODE_CBC, iv)
# 对明文进行填充
plain_text_bytes=plain_text.encode('utf-8')
padded_bytes=pad(plain_text_bytes,AES.block_size)
cipher_text_bytes=cipher.encrypt(padded_bytes)
# 合并IV和密文
combined_bytes=iv+cipher_text_bytes
encrypted_base64=base64.b64encode(combined_bytes).decode('utf-8')
return encrypted_base64
# 解密函数
def aes_decrypt(encrypted_base64, key):
"""AES解密"""
key_bytes=key.encode('utf-8')
# 从base64编码中提取IV和密文
combined_bytes=base64.b64decode(encrypted_base64)
iv=combined_bytes[:AES.block_size]
cipher_text_bytes=combined_bytes[AES.block_size:]
# 初始化AES解密器
cipher = AES.new(key_bytes, AES.MODE_CBC, iv)
# 解密密文
padded_bytes=cipher.decrypt(cipher_text_bytes)
# 移除填充
plain_text_bytes=unpad(padded_bytes,AES.block_size)
plain_text=plain_text_bytes.decode('utf-8')
return plain_text
# original_text='Hello, World!'
# # 加密原始文本
# encrypted_text=aes_encrypt(original_text,AES_KEY)
# print('加密后的文本:', encrypted_text)
# # 解密加密后的文本
# decrypted_text=aes_decrypt(encrypted_text,AES_KEY)
# print('解密后的文本:', decrypted_text)
app=Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)
@socketio.on('connect')
def handle_connect():
"""客户端连接时触发"""
print('客户端已连接')
emit('server_response', {'data': '连接成功'})
# 连接成功后,获取最新的远程屏幕
#client_get_screen()
@socketio.on('client_send_keyboard_input')
def client_send_keyboard_input(json):
"""处理来自客户端的消息"""
print('收到客户端消息:', json)
# 模拟键盘输入
client_input=json['data']
print('客户端输入:', client_input)
# typewrite只会输入字母,其他字符会被忽略
pyautogui.typewrite(client_input)
client_get_screen()
@socketio.on('client_send_paste_key')
def client_send_paste_key(json):
"""处理来自客户端的粘贴消息"""
print('收到客户端粘贴消息:', json)
# 模拟粘贴操作
client_paste_text=json['data']
print('客户端粘贴文本:', client_paste_text)
# 先将文本复制到剪贴板
pyperclip.copy(client_paste_text)
# 模拟粘贴键
pyautogui.hotkey('ctrl', 'v')
client_get_screen()
@socketio.on('client_get_screen')
def client_get_screen():
"""处理获取远程屏幕请求"""
print('收到获取远程屏幕请求')
# 循环刷新3次
for i in range(5):
# 延迟0.2秒
time.sleep(0.2)
screen = pyautogui.screenshot()
# 将屏幕图片转换为base64编码
buffered = BytesIO()
screen.save(buffered, format="PNG")
# 得到图片的base64编码
screen_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
#print('原始图片base64编码:', screen_base64[:100])
# 将图片base64编码进行aes加密
global AES_KEY
encrypted_screen_base64=aes_encrypt(screen_base64,AES_KEY)
# 发送aes加密后的屏幕图片给所有客户端
emit('remote_screen_update', {'data': encrypted_screen_base64},broadcast=False)
#emit('remote_screen_update', {'data': screen_base64},broadcast=False)
@socketio.on('client_click_screen')
def handle_client_click(json):
"""处理来自客户端的点击事件"""
print('收到客户端点击事件:', json)
x, y = json['x'], json['y']
# 映射到主机屏幕坐标
screen_width, screen_height = pyautogui.size()
x, y = int(x * screen_width), int(y * screen_height)
pyautogui.moveTo(x, y)
pyautogui.click(x, y)
# emit('server_response', {'data': f'已点击屏幕位置: ({x}, {y})'},broadcast=False)
# 点击鼠标事件后,获取最新的远程屏幕
client_get_screen()
@socketio.on('client_dbclick_screen')
def handle_client_dbclick(json):
"""处理来自客户端的双击事件"""
print('收到客户端双击事件:', json)
x, y = json['x'], json['y']
# 映射到主机屏幕坐标
screen_width, screen_height = pyautogui.size()
x, y = int(x * screen_width), int(y * screen_height)
pyautogui.doubleClick(x, y)
# 双击鼠标事件后,获取最新的远程屏幕
client_get_screen()
@socketio.on('client_send_enter_key')
def handle_client_send_enter_key():
"""处理来自客户端的Enter键事件"""
print('收到客户端Enter键事件')
# 模拟键盘输入Enter键
pyautogui.press('enter')
client_get_screen()
@socketio.on('client_send_delete_key')
def handle_client_send_delete_key():
"""处理来自客户端的Delete键事件"""
print('收到客户端Delete键事件')
# 模拟键盘输入Delete键
pyautogui.press('backspace')
client_get_screen()
@socketio.on('client_send_scroll_up_key')
def handle_client_send_scroll_up_key():
"""处理来自客户端的上翻键事件"""
print('收到客户端上翻键事件')
# 模拟键盘输入上翻键
pyautogui.scroll(100)
client_get_screen()
@socketio.on('client_send_scroll_down_key')
def handle_client_send_scroll_down_key():
"""处理来自客户端的下翻键事件"""
print('收到客户端下翻键事件')
# 模拟键盘输入下翻键
pyautogui.scroll(-100)
client_get_screen()
@app.route('/')
def index():
"""渲染主页"""
return '''
<!DOCTYPE html>
<html>
<head>
<meta lang="zh-CN">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>远程桌面</title>
<style>
body{
background-color:black;
}
#telescreen {
position: relative;
background-color:skyblue;
box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
border:1em solid gray;
border-radius: 1em;
resize: both;
overflow: auto;
}
#remote_screen {
display: none;
position: relative;
top: 0;
left: 0;
width: 100%;
}
#control_panel{
background-color: gray;
padding: 1em;
border-radius: 1em;
}
button,input
{
margin: 0.5em;
padding: 0.5em 1em;
border-radius: 0.5em;
border: 1px solid #666;
}
input{
width: 80%;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<script type="text/javascript">
// 解密AES函数
function decryptAES(ciphertext, key, options = {}) {
try {
// 设置默认选项
const mode = options.mode || 'CBC';
const padding = options.padding || 'Pkcs7';
// 准备解密参数
const decryptOptions = {
padding: CryptoJS.pad[padding]
};
// 获取对应的加密模式
const cryptoMode = CryptoJS.mode[mode];
// 如果指定了IV,使用指定的IV;否则尝试从密文中提取IV
if (options.iv) {
decryptOptions.iv = CryptoJS.enc.Utf8.parse(options.iv);
} else if (mode === 'CBC' || mode === 'CFB' || mode === 'OFB') {
// 注意:这部分逻辑取决于加密时IV的存储方式
// 有些实现会将IV附加在密文前面一起进行base64编码
// 这里提供一个常见的处理方式作为示例
const rawData = CryptoJS.enc.Base64.parse(ciphertext);
const iv = CryptoJS.lib.WordArray.create(rawData.words.slice(0, 4), 16); // 前16字节作为IV
const actualCiphertext = CryptoJS.lib.WordArray.create(rawData.words.slice(4)); // 剩余部分作为实际密文
decryptOptions.iv = iv;
ciphertext = CryptoJS.enc.Base64.stringify(actualCiphertext);
}
// 确保密钥格式正确
let keyWordArray;
if (key.length === 16 || key.length === 24 || key.length === 32) {
// 如果密钥长度符合AES标准,直接使用
keyWordArray = CryptoJS.enc.Utf8.parse(key);
} else {
// 否则使用MD5哈希生成一个32字节的密钥
console.warn('密钥长度不符合AES标准,将使用MD5哈希处理');
keyWordArray = CryptoJS.MD5(key);
}
// 执行解密
const decrypted = CryptoJS.AES.decrypt(ciphertext, keyWordArray, decryptOptions);
// 将解密结果转换为字符串并返回
return decrypted.toString(CryptoJS.enc.Utf8);
} catch (error) {
console.error('AES解密失败:', error);
throw new Error('解密失败,请检查密文和密钥是否正确');
}
}
// 连接到服务器
document.addEventListener('DOMContentLoaded', () => {
// 连接到服务器
const socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port);
// 接收服务器消息
socket.on('server_response', (data) => {
console.log("收到服务器响应:", data);
});
// 接收远程屏幕
socket.on('remote_screen_update', (data) => {
console.log("收到远程屏幕:", data);
// 解密远程屏幕数据
let secret_key = document.getElementById('secret_key').value.trim();
console.log('密钥:', secret_key);
if (!secret_key) {
console.log('请输入密钥');
return;
}
let decryptedImageData = decryptAES(data.data, secret_key);
console.log('解密后的远程屏幕数据:', decryptedImageData);
if (!decryptedImageData) {
console.log('密钥错误');
return;
}
// 清除旧的远程屏幕图片
while (document.getElementById('telescreen').firstChild) {
document.getElementById('telescreen').removeChild(document.getElementById('telescreen').firstChild);
}
//创建新的远程屏幕图片
const remote_screen = document.createElement('img');
remote_screen.id = 'remote_screen';
remote_screen.src=`data:image/png;base64,${decryptedImageData}`;
remote_screen.style.display = 'block';
//remote_screen.src=`data:image/png;base64,${data.data}`;
remote_screen.alt = '远程屏幕';
remote_screen.addEventListener('click', (e) => {
const rect = e.target.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = (e.clientY - rect.top) / rect.height;
console.log(`点击位置: (${x}, ${y})`);
socket.emit('client_click_screen', {x, y});
});
remote_screen.addEventListener('dbclick', (e) => {
const rect = e.target.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = (e.clientY - rect.top) / rect.height;
console.log(`点击位置: (${x}, ${y})`);
socket.emit('client_dbclick_screen', {x, y});
});
document.getElementById('telescreen').appendChild(remote_screen);
});
// 绑定获取屏幕按钮事件
document.getElementById('get_screen').addEventListener('click', () => {
socket.emit('client_get_screen');
});
// 绑定发送按钮事件
document.getElementById('send_button').addEventListener('click', (e) => {
const input = document.getElementById('message_input');
socket.emit('client_send_keyboard_input', {data: input.value});
input.value = '';
});
// 绑定粘贴按钮事件
document.getElementById('paste_button').addEventListener('click', (e) => {
const input = document.getElementById('message_input');
socket.emit('client_send_paste_key', {data: input.value});
input.value = '';
});
// 绑定Enter键事件
document.getElementById('enter_button').addEventListener('click', (e) => {
socket.emit('client_send_enter_key');
});
// 绑定Delete键事件
document.getElementById('delete_button').addEventListener('click', (e) => {
socket.emit('client_send_delete_key');
});
// 绑定上翻键事件
document.getElementById('scroll_up_button').addEventListener('click', (e) => {
socket.emit('client_send_scroll_up_key');
});
// 绑定下翻键事件
document.getElementById('scroll_down_button').addEventListener('click', (e) => {
socket.emit('client_send_scroll_down_key');
});
});
</script>
</head>
<body>
<div id="telescreen">
<img id="remote_screen" src="" alt="远程屏幕">
</div>
<div id="control_panel">
<input type="password" id="secret_key" placeholder="输入正确密钥才能刷新远程屏幕">
<br/>
<button id="get_screen">刷新远程屏幕</button>
<br/>
<input type="text" id="message_input" placeholder="输入要发送给远程屏幕的消息">
<br/>
<button id="send_button">发送输入文本</button>
<br/>
<button id="paste_button">粘贴输入文本</button>
<button id="delete_button">Delete</button>
<br/>
<button id="enter_button">Enter</button>
<button id="scroll_up_button">上翻</button>
<button id="scroll_down_button">下翻</button>
</div>
</body>
</html>
'''
if __name__ == '__main__':
socketio.run(app, debug=True,host='0.0.0.0',port=5000)