|
// 小米IoT平台设备控制 Cloudflare Worker |
|
|
|
addEventListener('fetch', event => { |
|
event.respondWith(handleRequest(event.request)) |
|
}) |
|
|
|
/** |
|
* 处理HTTP请求 |
|
* @param {Request} request |
|
*/ |
|
async function handleRequest(request) { |
|
// 允许跨域请求 |
|
const corsHeaders = { |
|
'Access-Control-Allow-Origin': '*', |
|
'Access-Control-Allow-Methods': 'POST, OPTIONS', |
|
'Access-Control-Allow-Headers': 'Content-Type' |
|
} |
|
|
|
// 处理OPTIONS请求 |
|
if (request.method === 'OPTIONS') { |
|
return new Response(null, { headers: corsHeaders }) |
|
} |
|
|
|
// 只接受POST请求 |
|
if (request.method !== 'POST') { |
|
return new Response('请使用POST方法', { |
|
status: 405, |
|
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' } |
|
}) |
|
} |
|
|
|
try { |
|
// 解析请求体 |
|
const requestBody = await request.json() |
|
const { |
|
userId, |
|
serviceToken, |
|
deviceId, |
|
securityToken, |
|
action, |
|
params |
|
} = requestBody |
|
|
|
// 验证必要参数 |
|
if (!userId || !serviceToken || !deviceId || !securityToken) { |
|
return new Response('缺少身份验证参数', { |
|
status: 400, |
|
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' } |
|
}) |
|
} |
|
|
|
// 根据不同操作执行不同功能 |
|
if (action === 'set_properties') { |
|
// 控制设备功能 |
|
if (!params || !Array.isArray(params) || params.length === 0) { |
|
return new Response('缺少设备控制参数', { |
|
status: 400, |
|
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' } |
|
}) |
|
} |
|
|
|
// 执行设备控制 |
|
const result = await setProperties(userId, serviceToken, deviceId, securityToken, params) |
|
|
|
return new Response(JSON.stringify(result), { |
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' } |
|
}) |
|
} else if (action === 'get_property') { |
|
// 获取设备属性功能 |
|
if (!params || !Array.isArray(params) || params.length === 0) { |
|
return new Response('缺少设备属性参数', { |
|
status: 400, |
|
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' } |
|
}) |
|
} |
|
|
|
// 执行获取设备属性 |
|
const result = await getProperties(userId, serviceToken, deviceId, securityToken, params); |
|
|
|
return new Response(JSON.stringify(result), { |
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' } |
|
}) |
|
} else if (action === 'call_action') { |
|
// 调用设备方法功能 |
|
if (!params || !Array.isArray(params) || params.length === 0) { |
|
return new Response('缺少设备方法参数', { |
|
status: 400, |
|
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' } |
|
}) |
|
} |
|
|
|
// 执行调用设备方法 |
|
const result = await callActions(userId, serviceToken, deviceId, securityToken, params); |
|
|
|
return new Response(JSON.stringify(result), { |
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' } |
|
}) |
|
} else { |
|
return new Response('未知操作类型', { |
|
status: 400, |
|
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' } |
|
}) |
|
} |
|
|
|
} catch (error) { |
|
return new Response('处理请求时出错: ' + error.message, { |
|
status: 500, |
|
headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=UTF-8' } |
|
}) |
|
} |
|
} |
|
|
|
/** |
|
* 设置设备属性(支持多设备) |
|
*/ |
|
async function setProperties(userId, serviceToken, deviceId, securityToken, params) { |
|
// 设置属性URI |
|
const uri = "/miotspec/prop/set"; |
|
|
|
// 构造参数 |
|
const data = {"params": params}; |
|
|
|
// 使用通用HTTP请求函数 |
|
return await httpRequest(userId, serviceToken, deviceId, uri, securityToken, data); |
|
} |
|
|
|
/** |
|
* 获取设备属性(支持多设备) |
|
*/ |
|
async function getProperties(userId, serviceToken, deviceId, securityToken, params) { |
|
// 获取属性URI |
|
const uri = "/miotspec/prop/get"; |
|
|
|
// 构造参数 |
|
const data = {"params": params}; |
|
|
|
// 使用通用HTTP请求函数 |
|
return await httpRequest(userId, serviceToken, deviceId, uri, securityToken, data); |
|
} |
|
|
|
/** |
|
* 调用设备方法(支持多设备) |
|
*/ |
|
async function callActions(userId, serviceToken, deviceId, securityToken, params) { |
|
// 调用方法URI |
|
const uri = "/miotspec/action"; |
|
|
|
// 构造参数 |
|
const data = {"params": params}; |
|
|
|
// 使用通用HTTP请求函数 |
|
return await httpRequest(userId, serviceToken, deviceId, uri, securityToken, data); |
|
} |
|
|
|
/** |
|
* 通用HTTP请求函数 |
|
* @param {string} userId 用户ID |
|
* @param {string} serviceToken 服务Token |
|
* @param {string} deviceId 设备ID |
|
* @param {string} uri 请求路径 |
|
* @param {string} securityToken 安全Token,POST请求需要 |
|
* @param {object} data 请求数据,有则POST请求,无则GET请求 |
|
*/ |
|
async function httpRequest(userId, serviceToken, deviceId, uri, securityToken = null, data = null) { |
|
// 设置cookie |
|
const cookie = `serviceToken=${serviceToken}; userId=${userId}; PassportDeviceId=${deviceId}`; |
|
|
|
// 构造请求URL |
|
const baseUrl = "https://api.io.mi.com/app"; |
|
const url = baseUrl + uri; |
|
|
|
// 通用请求头 |
|
const headers = { |
|
'User-Agent': 'APP/com.xiaomi.mihome APPV/6.0.103 iosPassportSDK/3.9.0 iOS/14.4 miHSTS', |
|
'x-xiaomi-protocal-flag-cli': 'PROTOCAL-HTTP2', |
|
'Cookie': cookie |
|
}; |
|
|
|
if (data) { |
|
// POST请求 |
|
if (!securityToken) { |
|
throw new Error('POST请求需要提供securityToken'); |
|
} |
|
|
|
// 签名数据 |
|
const nonce = generateNonce(); |
|
const signedNonce = await generateSignedNonce(securityToken, nonce); |
|
const dataString = typeof data === 'string' ? data : JSON.stringify(data); |
|
const signature = await generateSignature(uri, signedNonce, nonce, dataString); |
|
|
|
// 构造请求参数 |
|
const params = { |
|
_nonce: nonce, |
|
data: dataString, |
|
signature: signature |
|
}; |
|
|
|
// 发送POST请求 |
|
const response = await fetch(url, { |
|
method: 'POST', |
|
headers: { |
|
...headers, |
|
'Content-Type': 'application/x-www-form-urlencoded' |
|
}, |
|
body: new URLSearchParams(params) |
|
}); |
|
|
|
// 解析响应 |
|
if (response.ok) { |
|
return await response.json(); |
|
} else { |
|
throw new Error(`请求失败: ${response.status} ${response.statusText}`); |
|
} |
|
} else { |
|
// GET请求 |
|
const response = await fetch(url, { |
|
method: 'GET', |
|
headers: headers |
|
}); |
|
|
|
// 解析响应 |
|
if (response.ok) { |
|
return await response.json(); |
|
} else { |
|
throw new Error(`请求失败: ${response.status} ${response.statusText}`); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* 生成随机数 |
|
*/ |
|
function generateNonce() { |
|
// 生成8字节随机数 |
|
const randomBytes = new Uint8Array(8) |
|
crypto.getRandomValues(randomBytes) |
|
|
|
// 获取当前时间,除以60并转为4字节 |
|
const timeBytes = new Uint8Array(4) |
|
const time = Math.floor(Date.now() / 1000 / 60) |
|
timeBytes[0] = (time >> 24) & 0xff |
|
timeBytes[1] = (time >> 16) & 0xff |
|
timeBytes[2] = (time >> 8) & 0xff |
|
timeBytes[3] = time & 0xff |
|
|
|
// 合并随机数和时间 |
|
const combined = new Uint8Array(12) |
|
combined.set(randomBytes) |
|
combined.set(timeBytes, 8) |
|
|
|
// Base64编码 |
|
return btoa(String.fromCharCode.apply(null, combined)) |
|
} |
|
|
|
/** |
|
* 生成签名随机数 |
|
* @param {string} secret Base64编码的密钥 |
|
* @param {string} nonce Base64编码的随机数 |
|
*/ |
|
async function generateSignedNonce(secret, nonce) { |
|
// 解码secret和nonce |
|
const secretBytes = base64ToUint8Array(secret) |
|
const nonceBytes = base64ToUint8Array(nonce) |
|
|
|
// 合并secret和nonce |
|
const combined = new Uint8Array(secretBytes.length + nonceBytes.length) |
|
combined.set(secretBytes) |
|
combined.set(nonceBytes, secretBytes.length) |
|
|
|
// 使用SHA-256哈希 |
|
const hash = await crypto.subtle.digest('SHA-256', combined) |
|
|
|
// Base64编码返回 |
|
return btoa(String.fromCharCode.apply(null, new Uint8Array(hash))) |
|
} |
|
|
|
/** |
|
* 生成签名 |
|
* @param {string} url 接口路径 |
|
* @param {string} signedNonce 签名随机数 |
|
* @param {string} nonce 随机数 |
|
* @param {string} data 数据 |
|
*/ |
|
async function generateSignature(url, signedNonce, nonce, data) { |
|
// 构造签名字符串 |
|
const signString = `${url}&${signedNonce}&${nonce}&data=${data}` |
|
|
|
// 解码signedNonce为密钥 |
|
const keyBytes = base64ToUint8Array(signedNonce) |
|
|
|
// 创建HMAC密钥 |
|
const key = await crypto.subtle.importKey( |
|
'raw', |
|
keyBytes, |
|
{ name: 'HMAC', hash: { name: 'SHA-256' } }, |
|
false, |
|
['sign'] |
|
) |
|
|
|
// 计算HMAC |
|
const signature = await crypto.subtle.sign( |
|
'HMAC', |
|
key, |
|
new TextEncoder().encode(signString) |
|
) |
|
|
|
// Base64编码返回 |
|
return btoa(String.fromCharCode.apply(null, new Uint8Array(signature))) |
|
} |
|
|
|
/** |
|
* Base64字符串转Uint8Array |
|
* @param {string} base64 Base64编码的字符串 |
|
*/ |
|
function base64ToUint8Array(base64) { |
|
const binary = atob(base64) |
|
const bytes = new Uint8Array(binary.length) |
|
for (let i = 0; i < binary.length; i++) { |
|
bytes[i] = binary.charCodeAt(i) |
|
} |
|
return bytes |
|
} |