完成同步功能

This commit is contained in:
lyswhut
2021-08-01 12:57:13 +08:00
parent 33ec3de1b4
commit bcf3dde60d
38 changed files with 1975 additions and 120 deletions
+69
View File
@@ -0,0 +1,69 @@
const { aesEncrypt, aesDecrypt, createClientKeyInfo, getClientKeyInfo, setClientKeyInfo } = require('./utils')
const authMsg = 'lx-music auth::'
const helloMsg = 'Hello~::^-^::'
exports.authCode = async(req, res, authCode) => {
let code = 401
let msg = 'Forbidden'
// console.log(req.headers)
if (req.headers.m) {
label:
if (req.headers.i) {
const keyInfo = getClientKeyInfo(req.headers.i)
if (!keyInfo) break label
let text
try {
text = aesDecrypt(req.headers.m, keyInfo.key, keyInfo.iv)
} catch (err) {
break label
}
console.log(text)
if (text.startsWith(authMsg)) {
code = 200
const deviceName = text.replace(authMsg, '') || 'Unknown'
if (deviceName != keyInfo.deviceName) {
keyInfo.deviceName = deviceName
setClientKeyInfo(keyInfo)
}
msg = aesEncrypt(helloMsg, keyInfo.key, keyInfo.iv)
}
} else {
let key = ''.padStart(16, Buffer.from(authCode).toString('hex'))
const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
key = Buffer.from(key).toString('base64')
// console.log(authCode, key, iv)
let text
try {
text = aesDecrypt(req.headers.m, key, iv)
} catch (err) {
break label
}
console.log(text)
if (text.startsWith(authMsg)) {
code = 200
const deviceName = text.replace(authMsg, '') || 'Unknown'
msg = aesEncrypt(JSON.stringify(createClientKeyInfo(deviceName)), key, iv)
}
}
}
res.writeHead(code)
res.end(msg)
}
exports.authConnect = async req => {
const { i, t } = req._query
label:
if (i && t) {
const keyInfo = getClientKeyInfo(i)
if (!keyInfo) break label
let text
try {
text = aesDecrypt(t, keyInfo.key, keyInfo.iv)
} catch (err) {
break label
}
if (text == 'lx-music connect') return
}
throw new Error('failed')
}
+7
View File
@@ -0,0 +1,7 @@
const { startServer, stopServer, getStatus } = require('./server')
module.exports = {
startServer,
stopServer,
getStatus,
}
+165
View File
@@ -0,0 +1,165 @@
const http = require('http')
const sio = require('socket.io')
const { createHttpTerminator } = require('http-terminator')
const modules = require('../modules')
const { authCode, authConnect } = require('./auth')
const { getAddress, getServerId, generateCode, getClientKeyInfo } = require('./utils')
const syncList = require('./syncList')
let status = {
status: false,
message: '',
address: [],
code: '',
devices: [],
}
const handleConnection = (io, socket) => {
console.log('connection')
// console.log(socket.handshake.query)
for (const module of Object.values(modules)) {
module.registerListHandler(io, socket)
}
}
const authConnection = (req, callback) => {
// console.log(req.headers)
// // console.log(req.auth)
// console.log(req._query.authCode)
authConnect(req).then(() => {
callback(null, true)
}).catch(err => {
callback(err, false)
})
}
let httpTerminator = null
let io = null
const handleStartServer = (port = 9527) => new Promise((resolve, reject) => {
const httpServer = http.createServer((req, res) => {
// console.log(req.url)
let code
let msg
switch (req.url) {
case '/hello':
code = 200
msg = 'Hello~::^-^::'
break
case '/id':
code = 200
msg = 'OjppZDo6' + getServerId()
break
case '/ah':
authCode(req, res, status.code)
break
default:
code = 401
msg = 'Forbidden'
break
}
if (!code) return
res.writeHead(code)
res.end(msg)
})
httpTerminator = createHttpTerminator({
server: httpServer,
})
io = sio(httpServer, {
path: '/sync',
serveClient: false,
connectTimeout: 10000,
pingTimeout: 30000,
maxHttpBufferSize: 3e6,
allowRequest: authConnection,
transports: ['websocket'],
})
io.on('connection', async socket => {
socket.on('disconnect', reason => {
console.log('disconnect', reason)
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo.clientId), 1)
global.lx_event.sync.status(status)
})
const keyInfo = getClientKeyInfo(socket.handshake.query.i)
// socket.lx_keyInfo = keyInfo
socket.data.keyInfo = keyInfo
try {
await syncList(io, socket)
} catch (err) {
console.log(err)
return
}
status.devices.push(keyInfo)
handleConnection(io, socket, keyInfo)
global.lx_event.sync.status(status)
})
httpServer.on('error', error => {
console.log(error)
reject(error)
})
httpServer.on('listening', () => {
const addr = httpServer.address()
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
console.info(`Listening on ${bind}`)
resolve()
})
httpServer.listen(port)
})
const handleStopServer = async() => {
if (!httpTerminator) return
await io.close()
await httpTerminator.terminate().catch(() => {})
io = null
httpTerminator = null
}
exports.stopServer = async() => {
if (!status.status) return
console.log('stoping sync server...')
return handleStopServer().then(() => {
console.log('sync server stoped')
status.status = false
status.message = ''
status.address = []
status.code = ''
}).catch(err => {
console.log(err)
status.message = err.message
}).finally(() => {
global.lx_event.sync.status(status)
})
}
exports.startServer = async port => {
if (status.status) await handleStopServer()
console.log('starting sync server...')
return handleStartServer(port).then(() => {
console.log('sync server started')
status.status = true
status.message = ''
status.address = getAddress()
status.code = generateCode()
}).catch(err => {
console.log(err)
status.status = false
status.message = err.message
status.address = []
status.code = ''
}).finally(() => {
global.lx_event.sync.status(status)
})
}
exports.getStatus = () => status
exports.generateCode = async() => {
status.code = generateCode()
global.lx_event.sync.status(status)
return status.code
}
+388
View File
@@ -0,0 +1,388 @@
const path = require('path')
const fs = require('fs')
const fsPromises = fs.promises
const { app } = require('electron')
const { encryptMsg, decryptMsg } = require('./utils')
const SYNC_EVENT_NAMES = require('../event/name')
const { common: COMMON_EVENT_NAME } = require('@main/events/_name')
const { throttle } = require('@common/utils')
let io
// const checkFile = path => {
// fsPromises.access(path, fs.constants.R_OK | fs.constants.W_OK)
// .then(() => console.log('can access'))
// .catch(() => console.error('cannot access'))
// }
const getRemoteListData = socket => new Promise((resolve, reject) => {
console.log('getRemoteListData')
const handleError = reason => {
reject(new Error(reason))
}
const handleSuccess = enData => {
socket.removeListener('disconnect', handleError)
socket.removeListener('list:sync', handleSuccess)
console.log('getRemoteListData', 'handleSuccess')
const data = JSON.parse(decryptMsg(socket.data.keyInfo, enData))
if (!data) return reject(new Error('Get remote list data failed'))
if (data.action != 'getData') return
resolve(data.data)
}
socket.on('disconnect', handleError)
socket.on('list:sync', handleSuccess)
socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'getData', data: 'all' })))
})
const getLocalListData = () => new Promise((resolve, reject) => {
const handleSuccess = ({ action, data }) => {
if (action !== 'getData') return
global.lx_event.sync.off(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
resolve(data)
}
global.lx_event.sync.on(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
global.lx_event.sync.sync_list({
action: 'getData',
})
})
const getSyncMode = keyInfo => new Promise((resolve, reject) => {
const handleSuccess = ({ action, data }) => {
if (action !== 'selectMode') return
global.lx_event.sync.off(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
resolve(data)
}
global.lx_event.sync.on(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
global.lx_event.sync.sync_list({
action: 'selectMode',
data: keyInfo,
})
})
const finishedSync = socket => {
return socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({
action: 'finished',
})))
}
const setLocalList = listData => {
global.lx_event.sync.sync_list({
action: 'setData',
data: listData,
})
}
const setRemotelList = async(socket, listData) => {
if (!io) return
const sockets = await io.fetchSockets()
for (const socket of sockets) {
// if (excludeIds.includes(socket.data.keyInfo.clientId)) continue
socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'setData', data: listData })))
}
}
let writeFilePromises = {}
const updateSnapshot = (path, data) => {
console.log('updateSnapshot', path)
let writeFilePromise = writeFilePromises[path] || Promise.resolve()
return writeFilePromise.then(() => {
writeFilePromise = writeFilePromises[path] = fsPromises.writeFile(path, data)
return writeFilePromise
})
}
const createListDataObj = listData => {
const listDataObj = {}
for (const list of listData.userList) listDataObj[list.id] = list
return listDataObj
}
const handleMergeList = (sourceList, targetList, addMusicLocationType) => {
let newList
switch (addMusicLocationType) {
case 'top':
newList = [...targetList.list, ...sourceList.list]
break
case 'bottom':
default:
newList = [...sourceList.list, ...targetList.list]
break
}
const map = {}
const ids = []
switch (addMusicLocationType) {
case 'top':
newList = [...targetList.list, ...sourceList.list]
for (let i = newList.length - 1; i > -1; i--) {
const item = newList[i]
if (map[item.songmid]) continue
ids.unshift(item.songmid)
map[item.songmid] = item
}
break
case 'bottom':
default:
newList = [...sourceList.list, ...targetList.list]
for (const item of newList) {
if (map[item.songmid]) continue
ids.push(item.songmid)
map[item.songmid] = item
}
break
}
return {
...sourceList,
list: ids.map(id => map[id]),
}
}
const mergeList = (sourceListData, targetListData) => {
const addMusicLocationType = global.appSetting.list.addMusicLocationType
const newListData = {}
newListData.defaultList = handleMergeList(sourceListData.defaultList, targetListData.defaultList, addMusicLocationType)
newListData.loveList = handleMergeList(sourceListData.loveList, targetListData.loveList, addMusicLocationType)
const listDataObj = createListDataObj(sourceListData)
newListData.userList = [...sourceListData.userList]
for (const list of targetListData.userList) {
const targetList = listDataObj[list.id]
if (targetList) {
targetList.list = handleMergeList(targetList, list, addMusicLocationType).list
} else {
newListData.userList.push(list)
}
}
return newListData
}
const overwriteList = (sourceListData, targetListData) => {
const newListData = {}
newListData.defaultList = sourceListData.defaultList
newListData.loveList = sourceListData.loveList
const listDataObj = createListDataObj(sourceListData)
newListData.userList = [...sourceListData.userList]
for (const list of targetListData.userList) {
const targetList = listDataObj[list.id]
if (targetList) continue
newListData.userList.push(list)
}
return newListData
}
const handleMergeListData = async socket => {
let isSelectingMode = false
const handleDisconnect = () => {
if (!isSelectingMode) return
global.lx_event.sync.sync_list({
action: 'closeSelectMode',
})
}
socket.on('disconnect', handleDisconnect)
isSelectingMode = true
const mode = await getSyncMode(socket.data.keyInfo)
isSelectingMode = false
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
console.log('handleMergeListData', 'remoteListData, localListData')
let listData
switch (mode) {
case 'merge_local_remote':
listData = mergeList(localListData, remoteListData)
break
case 'merge_remote_local':
listData = mergeList(remoteListData, localListData)
break
case 'overwrite_local_remote':
listData = overwriteList(localListData, remoteListData)
break
case 'overwrite_remote_local':
listData = overwriteList(remoteListData, localListData)
break
case 'overwrite_local_remote_full':
listData = localListData
break
case 'overwrite_remote_local_full':
listData = remoteListData
break
case 'none': return
case 'cancel':
socket.disconnect(true)
throw new Error('cancel')
}
return listData
}
const handleSyncList = async socket => {
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
console.log('handleSyncList', 'remoteListData, localListData')
const listData = {}
if (localListData.defaultList.list.length || localListData.loveList.list.length || localListData.userList.length) {
if (remoteListData.defaultList.list.length || remoteListData.loveList.list.length || remoteListData.userList.length) {
const mergedList = await handleMergeListData(socket)
console.log('handleMergeListData', 'mergedList')
console.log(mergedList)
if (!mergedList) return
listData.defaultList = mergedList.defaultList
listData.loveList = mergedList.loveList
listData.userList = mergedList.userList
setLocalList(mergedList)
setRemotelList(socket, mergedList)
} else {
setRemotelList(socket, localListData)
listData.defaultList = localListData.defaultList
listData.loveList = localListData.loveList
listData.userList = localListData.userList
}
} else {
if (remoteListData.defaultList.list.length || remoteListData.loveList.list.length || remoteListData.userList.length) {
setLocalList(remoteListData)
listData.defaultList = remoteListData.defaultList
listData.loveList = remoteListData.loveList
listData.userList = remoteListData.userList
} else {
listData.defaultList = localListData.defaultList
listData.loveList = localListData.loveList
listData.userList = localListData.userList
}
}
return updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
defaultList: listData.defaultList,
loveList: listData.loveList,
userList: listData.userList,
})).then(() => {
socket.data.isCreatedSnapshot = true
return listData
})
}
const mergeListDataFromSnapshot = (sourceList, targetList, snapshotList, addMusicLocationType) => {
const removedListIds = new Set()
const sourceListItemIds = new Set()
const targetListItemIds = new Set()
for (const m of sourceList.list) sourceListItemIds.add(m.songmid)
for (const m of targetList.list) targetListItemIds.add(m.songmid)
for (const m of snapshotList.list) {
if (!sourceListItemIds.has(m.songmid)) removedListIds.add(m.songmid)
}
for (const m of snapshotList.list) {
if (!targetListItemIds.has(m.songmid)) removedListIds.add(m.songmid)
}
let newList
const map = {}
const ids = []
switch (addMusicLocationType) {
case 'top':
newList = [...targetList.list, ...sourceList.list]
for (let i = newList.length - 1; i > -1; i--) {
const item = newList[i]
if (map[item.songmid] || removedListIds.has(item.songmid)) continue
ids.unshift(item.songmid)
map[item.songmid] = item
}
break
case 'bottom':
default:
newList = [...sourceList.list, ...targetList.list]
for (const item of newList) {
if (map[item.songmid] || removedListIds.has(item.songmid)) continue
ids.push(item.songmid)
map[item.songmid] = item
}
break
}
return {
...sourceList,
list: ids.map(id => map[id]),
}
}
const handleMergeListDataFromSnapshot = async(socket, snapshot) => {
const addMusicLocationType = global.appSetting.list.addMusicLocationType
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
console.log('handleMergeListDataFromSnapshot', 'remoteListData, localListData')
const newListData = {}
newListData.defaultList = mergeListDataFromSnapshot(localListData.defaultList, remoteListData.defaultList, snapshot.defaultList, addMusicLocationType)
newListData.loveList = mergeListDataFromSnapshot(localListData.loveList, remoteListData.loveList, snapshot.loveList, addMusicLocationType)
const localUserListData = createListDataObj(localListData)
const remoteUserListData = createListDataObj(remoteListData)
const snapshotUserListData = createListDataObj(snapshot)
const removedListIds = new Set()
const localUserListIds = new Set()
const remoteUserListIds = new Set()
for (const l of localListData.userList) localUserListIds.add(l.id)
for (const l of remoteListData.userList) remoteUserListIds.add(l.id)
for (const l of snapshot.userList) {
if (!localUserListIds.has(l.id)) removedListIds.add(l.id)
}
for (const l of snapshot.userList) {
if (!remoteUserListIds.has(l.id)) removedListIds.add(l.id)
}
let newUserList = []
for (const list of localListData.userList) {
if (removedListIds.has(list.id)) continue
const remoteList = remoteUserListData[list.id]
let newList
if (remoteList) {
newList = mergeListDataFromSnapshot(list, remoteList, snapshotUserListData[list.id], addMusicLocationType)
} else {
newList = { ...list }
}
newUserList.push(newList)
}
for (const list of remoteListData.userList) {
if (removedListIds.has(list.id) || localUserListData[list.id]) continue
newUserList.push({ ...list })
}
newListData.userList = newUserList
setLocalList(newListData)
setRemotelList(socket, newListData)
return updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
defaultList: newListData.defaultList,
loveList: newListData.loveList,
userList: newListData.userList,
})).then(() => {
socket.data.isCreatedSnapshot = true
return newListData
})
}
const registerUpdateSnapshotTask = (socket, snapshot) => {
if (!socket.data.isCreatedSnapshot) return
const handleUpdateSnapshot = throttle(({ defaultList, loveList, userList }) => {
if (defaultList != null) snapshot.defaultList = defaultList
if (loveList != null) snapshot.loveList = loveList
if (userList != null) snapshot.userList = userList
updateSnapshot(socket.data.snapshotFilePath, JSON.stringify(snapshot))
}, 10000)
global.lx_event.common.on(COMMON_EVENT_NAME.saveMyList, handleUpdateSnapshot)
socket.on('disconnect', () => {
global.lx_event.common.off(COMMON_EVENT_NAME.saveMyList, handleUpdateSnapshot)
})
}
const syncList = async socket => {
socket.data.snapshotFilePath = path.join(app.getPath('userData'), `snapshot-${Buffer.from(socket.data.keyInfo.clientId).toString('hex').substring(0, 10)}.json`)
let fileData
let isSyncRequired = false
try {
fileData = await fsPromises.readFile(socket.data.snapshotFilePath)
fileData = JSON.parse(fileData)
} catch (err) {
if (err.code !== 'ENOENT') throw err
isSyncRequired = true
}
console.log('isSyncRequired', isSyncRequired)
if (isSyncRequired) return handleSyncList(socket)
return handleMergeListDataFromSnapshot(socket, fileData)
}
module.exports = (_io, socket) => {
io = _io
return syncList(socket).then(newListData => {
registerUpdateSnapshotTask(socket, { ...newListData })
return finishedSync(socket)
})
}
+93
View File
@@ -0,0 +1,93 @@
const { networkInterfaces } = require('os')
const { randomBytes, createCipheriv, createDecipheriv } = require('crypto')
const getStore = require('@common/store')
const STORE_NAME = 'sync'
exports.getAddress = () => {
const nets = networkInterfaces()
const results = []
// console.log(nets)
for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
if (net.family === 'IPv4' && !net.internal) {
results.push(net.address)
}
}
}
return results
}
let serverId
exports.getServerId = () => {
if (serverId) return serverId
const store = getStore(STORE_NAME)
serverId = store.get('serverId')
if (!serverId) {
serverId = randomBytes(4 * 4).toString('base64')
store.set('serverId', serverId)
}
return serverId
}
let keyInfos
exports.createClientKeyInfo = deviceName => {
const keyInfo = {
clientId: randomBytes(4 * 4).toString('base64'),
key: randomBytes(16).toString('base64'),
iv: randomBytes(16).toString('base64'),
deviceName,
}
const store = getStore(STORE_NAME)
if (!keyInfos) keyInfos = store.get('keys') || {}
if (Object.keys(keyInfos).length > 101) throw new Error('max keys')
keyInfos[keyInfo.clientId] = keyInfo
store.set('keys', keyInfos)
return keyInfo
}
exports.setClientKeyInfo = keyInfo => {
keyInfos[keyInfo.clientId] = keyInfo
const store = getStore(STORE_NAME)
store.set('keys', keyInfos)
}
exports.getClientKeyInfo = clientId => {
if (!keyInfos) {
const store = getStore(STORE_NAME)
keyInfos = store.get('keys') || {}
}
return keyInfos[clientId] || null
}
exports.generateCode = () => {
return Math.random().toString().substring(2, 8)
}
exports.aesEncrypt = (buffer, key, iv) => {
const cipher = createCipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
return Buffer.concat([cipher.update(buffer), cipher.final()]).toString('base64')
}
exports.aesDecrypt = (text, key, iv) => {
const decipher = createDecipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()
}
exports.encryptMsg = (keyInfo, msg) => {
return msg
// if (!keyInfo) return ''
// return exports.aesEncrypt(msg, keyInfo.key, keyInfo.iv)
}
exports.decryptMsg = (keyInfo, enMsg) => {
return enMsg
// if (!keyInfo) return ''
// let msg = ''
// try {
// msg = exports.aesDecrypt(enMsg, keyInfo.key, keyInfo.iv)
// } catch (err) {
// console.log(err)
// }
// return msg
}