|
|
@ -45,40 +45,50 @@ export class WebSocketConnection { |
|
|
|
|
|
|
|
|
|
|
|
stop = () => { |
|
|
|
stop = () => { |
|
|
|
this.socket.close(); |
|
|
|
this.socket.close(); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
onClose = (code: number, reason: string) => { |
|
|
|
onClose = (code: number, reason: string) => { |
|
|
|
log.debug({ code, reason }, "WebSocketConnection closing"); |
|
|
|
log.debug({ code, reason }, "WebSocketConnection closing"); |
|
|
|
this.disposers.forEach((disposer) => disposer()); |
|
|
|
this.disposers.forEach(disposer => disposer()); |
|
|
|
this.deviceSubscriptions.forEach((disposer) => disposer()); |
|
|
|
this.deviceSubscriptions.forEach(disposer => disposer()); |
|
|
|
this.api.removeClient(this); |
|
|
|
this.api.removeClient(this); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
subscribeBrokerConnection() { |
|
|
|
subscribeBrokerConnection() { |
|
|
|
this.disposers.push(autorun(() => { |
|
|
|
this.disposers.push( |
|
|
|
|
|
|
|
autorun(() => { |
|
|
|
const updateData: ws.IBrokerConnectionUpdate = { |
|
|
|
const updateData: ws.IBrokerConnectionUpdate = { |
|
|
|
brokerConnected: this.state.mqttClient.connected, |
|
|
|
brokerConnected: this.state.mqttClient.connected |
|
|
|
}; |
|
|
|
}; |
|
|
|
this.sendNotification("brokerConnectionUpdate", updateData); |
|
|
|
this.sendNotification("brokerConnectionUpdate", updateData); |
|
|
|
})); |
|
|
|
}) |
|
|
|
|
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
checkAuthorization() { |
|
|
|
checkAuthorization() { |
|
|
|
if (!this.userId || !this.user) { |
|
|
|
if (!this.userId || !this.user) { |
|
|
|
throw new RpcError("this WebSocket session has not been authenticated", |
|
|
|
throw new RpcError( |
|
|
|
ErrorCode.Unauthorized); |
|
|
|
"this WebSocket session has not been authenticated", |
|
|
|
|
|
|
|
ErrorCode.Unauthorized |
|
|
|
|
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
checkDevice(devId: string) { |
|
|
|
checkDevice(devId: string) { |
|
|
|
const userDevice = this.user!.devices!.find((dev) => dev.deviceId === devId); |
|
|
|
const userDevice = this.user!.devices!.find(dev => dev.deviceId === devId); |
|
|
|
if (userDevice == null) { |
|
|
|
if (userDevice == null) { |
|
|
|
throw new RpcError("you do not have permission to subscribe to device", |
|
|
|
throw new RpcError( |
|
|
|
ErrorCode.NoPermission, { id: devId }); |
|
|
|
"you do not have permission to subscribe to device", |
|
|
|
|
|
|
|
ErrorCode.NoPermission, |
|
|
|
|
|
|
|
{ id: devId } |
|
|
|
|
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
const deviceId = userDevice.deviceId; |
|
|
|
const deviceId = userDevice.deviceId; |
|
|
|
if (!deviceId) { |
|
|
|
if (!deviceId) { |
|
|
|
throw new RpcError("device has no associated device prefix", ErrorCode.Internal); |
|
|
|
throw new RpcError( |
|
|
|
|
|
|
|
"device has no associated device prefix", |
|
|
|
|
|
|
|
ErrorCode.Internal |
|
|
|
|
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
return userDevice; |
|
|
|
return userDevice; |
|
|
|
} |
|
|
|
} |
|
|
@ -89,25 +99,28 @@ export class WebSocketConnection { |
|
|
|
|
|
|
|
|
|
|
|
sendNotification<Method extends ws.ServerNotificationMethod>( |
|
|
|
sendNotification<Method extends ws.ServerNotificationMethod>( |
|
|
|
method: Method, |
|
|
|
method: Method, |
|
|
|
data: ws.IServerNotificationTypes[Method]) { |
|
|
|
data: ws.IServerNotificationTypes[Method] |
|
|
|
|
|
|
|
) { |
|
|
|
this.sendMessage({ type: "notification", method, data }); |
|
|
|
this.sendMessage({ type: "notification", method, data }); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sendResponse<Method extends ws.ClientRequestMethods>( |
|
|
|
sendResponse<Method extends ws.ClientRequestMethods>( |
|
|
|
method: Method, |
|
|
|
method: Method, |
|
|
|
id: number, |
|
|
|
id: number, |
|
|
|
data: ws.ServerResponseData<Method>) { |
|
|
|
data: ws.ServerResponseData<Method> |
|
|
|
|
|
|
|
) { |
|
|
|
this.sendMessage({ type: "response", method, id, ...data }); |
|
|
|
this.sendMessage({ type: "response", method, id, ...data }); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
handleSocketMessage = (socketData: WebSocket.Data) => { |
|
|
|
handleSocketMessage = (socketData: WebSocket.Data) => { |
|
|
|
this.doHandleSocketMessage(socketData) |
|
|
|
this.doHandleSocketMessage(socketData).catch(err => { |
|
|
|
.catch((err) => { |
|
|
|
|
|
|
|
this.onError({ err }, "unhandled error on handling socket message"); |
|
|
|
this.onError({ err }, "unhandled error on handling socket message"); |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
async doDeviceCallRequest(requestData: ws.IDeviceCallRequest): Promise<deviceRequests.Response> { |
|
|
|
async doDeviceCallRequest( |
|
|
|
|
|
|
|
requestData: ws.IDeviceCallRequest |
|
|
|
|
|
|
|
): Promise<deviceRequests.Response> { |
|
|
|
const userDevice = this.checkDevice(requestData.deviceId); |
|
|
|
const userDevice = this.checkDevice(requestData.deviceId); |
|
|
|
const deviceId = userDevice.deviceId!; |
|
|
|
const deviceId = userDevice.deviceId!; |
|
|
|
const device = this.state.mqttClient.acquireDevice(deviceId); |
|
|
|
const device = this.state.mqttClient.acquireDevice(deviceId); |
|
|
@ -121,23 +134,32 @@ export class WebSocketConnection { |
|
|
|
|
|
|
|
|
|
|
|
private async doHandleSocketMessage(socketData: WebSocket.Data) { |
|
|
|
private async doHandleSocketMessage(socketData: WebSocket.Data) { |
|
|
|
if (typeof socketData !== "string") { |
|
|
|
if (typeof socketData !== "string") { |
|
|
|
return this.onError({ type: typeof socketData }, |
|
|
|
return this.onError( |
|
|
|
"received invalid socket data type from client", ErrorCode.Parse); |
|
|
|
{ type: typeof socketData }, |
|
|
|
|
|
|
|
"received invalid socket data type from client", |
|
|
|
|
|
|
|
ErrorCode.Parse |
|
|
|
|
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
let data: ws.ClientMessage; |
|
|
|
let data: ws.ClientMessage; |
|
|
|
try { |
|
|
|
try { |
|
|
|
data = JSON.parse(socketData); |
|
|
|
data = JSON.parse(socketData); |
|
|
|
} catch (err) { |
|
|
|
} catch (err) { |
|
|
|
return this.onError({ socketData, err }, "received invalid websocket message from client", |
|
|
|
return this.onError( |
|
|
|
ErrorCode.Parse); |
|
|
|
{ socketData, err }, |
|
|
|
|
|
|
|
"received invalid websocket message from client", |
|
|
|
|
|
|
|
ErrorCode.Parse |
|
|
|
|
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
switch (data.type) { |
|
|
|
switch (data.type) { |
|
|
|
case "request": |
|
|
|
case "request": |
|
|
|
await this.handleRequest(data); |
|
|
|
await this.handleRequest(data); |
|
|
|
break; |
|
|
|
break; |
|
|
|
default: |
|
|
|
default: |
|
|
|
return this.onError({ data }, "received invalid message type from client", |
|
|
|
return this.onError( |
|
|
|
ErrorCode.BadRequest); |
|
|
|
{ data }, |
|
|
|
|
|
|
|
"received invalid message type from client", |
|
|
|
|
|
|
|
ErrorCode.BadRequest |
|
|
|
|
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -154,19 +176,28 @@ export class WebSocketConnection { |
|
|
|
log.debug({ err }, "rpc error"); |
|
|
|
log.debug({ err }, "rpc error"); |
|
|
|
response = { result: "error", error: err.toJSON() }; |
|
|
|
response = { result: "error", error: err.toJSON() }; |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
log.error({ method: request.method, err }, "unhandled error during processing of client request"); |
|
|
|
log.error( |
|
|
|
|
|
|
|
{ method: request.method, err }, |
|
|
|
|
|
|
|
"unhandled error during processing of client request" |
|
|
|
|
|
|
|
); |
|
|
|
response = { |
|
|
|
response = { |
|
|
|
result: "error", error: { |
|
|
|
result: "error", |
|
|
|
code: ErrorCode.Internal, message: "unhandled error during processing of client request", |
|
|
|
error: { |
|
|
|
data: err.toString(), |
|
|
|
code: ErrorCode.Internal, |
|
|
|
}, |
|
|
|
message: "unhandled error during processing of client request", |
|
|
|
|
|
|
|
data: err.toString() |
|
|
|
|
|
|
|
} |
|
|
|
}; |
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
this.sendResponse(request.method, request.id, response); |
|
|
|
this.sendResponse(request.method, request.id, response); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private onError(data: any, message: string, code: number = ErrorCode.Internal) { |
|
|
|
private onError( |
|
|
|
|
|
|
|
data: any, |
|
|
|
|
|
|
|
message: string, |
|
|
|
|
|
|
|
code: number = ErrorCode.Internal |
|
|
|
|
|
|
|
) { |
|
|
|
log.error(data, message); |
|
|
|
log.error(data, message); |
|
|
|
const errorData: ws.IError = { code, message, data }; |
|
|
|
const errorData: ws.IError = { code, message, data }; |
|
|
|
this.sendNotification("error", errorData); |
|
|
|
this.sendNotification("error", errorData); |
|
|
@ -174,8 +205,10 @@ export class WebSocketConnection { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class WebSocketRequestHandlers implements ws.ClientRequestHandlers { |
|
|
|
class WebSocketRequestHandlers implements ws.ClientRequestHandlers { |
|
|
|
async authenticate(this: WebSocketConnection, data: ws.IAuthenticateRequest): |
|
|
|
async authenticate( |
|
|
|
Promise<ws.ServerResponseData<"authenticate">> { |
|
|
|
this: WebSocketConnection, |
|
|
|
|
|
|
|
data: ws.IAuthenticateRequest |
|
|
|
|
|
|
|
): Promise<ws.ServerResponseData<"authenticate">> { |
|
|
|
if (!data.accessToken) { |
|
|
|
if (!data.accessToken) { |
|
|
|
throw new RpcError("no token specified", ErrorCode.BadRequest); |
|
|
|
throw new RpcError("no token specified", ErrorCode.BadRequest); |
|
|
|
} |
|
|
|
} |
|
|
@ -186,34 +219,51 @@ class WebSocketRequestHandlers implements ws.ClientRequestHandlers { |
|
|
|
throw new RpcError("invalid token", ErrorCode.BadToken, e); |
|
|
|
throw new RpcError("invalid token", ErrorCode.BadToken, e); |
|
|
|
} |
|
|
|
} |
|
|
|
this.userId = claims.aud; |
|
|
|
this.userId = claims.aud; |
|
|
|
this.user = await this.state.database.users. |
|
|
|
this.user = |
|
|
|
findById(this.userId, { devices: true }) || null; |
|
|
|
(await this.state.database.users.findById(this.userId, { |
|
|
|
|
|
|
|
devices: true |
|
|
|
|
|
|
|
})) || null; |
|
|
|
if (!this.user) { |
|
|
|
if (!this.user) { |
|
|
|
throw new RpcError("user no longer exists", ErrorCode.BadToken); |
|
|
|
throw new RpcError("user no longer exists", ErrorCode.BadToken); |
|
|
|
} |
|
|
|
} |
|
|
|
log.debug({ userId: claims.aud, name: claims.name }, "authenticated websocket client"); |
|
|
|
log.debug( |
|
|
|
|
|
|
|
{ userId: claims.aud, name: claims.name }, |
|
|
|
|
|
|
|
"authenticated websocket client" |
|
|
|
|
|
|
|
); |
|
|
|
this.subscribeBrokerConnection(); |
|
|
|
this.subscribeBrokerConnection(); |
|
|
|
return { |
|
|
|
return { |
|
|
|
result: "success", |
|
|
|
result: "success", |
|
|
|
data: { authenticated: true, message: "authenticated", user: this.user.toJSON() }, |
|
|
|
data: { |
|
|
|
|
|
|
|
authenticated: true, |
|
|
|
|
|
|
|
message: "authenticated", |
|
|
|
|
|
|
|
user: this.user.toJSON() |
|
|
|
|
|
|
|
} |
|
|
|
}; |
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async deviceSubscribe(this: WebSocketConnection, data: ws.IDeviceSubscribeRequest): |
|
|
|
async deviceSubscribe( |
|
|
|
Promise<ws.ServerResponseData<"deviceSubscribe">> { |
|
|
|
this: WebSocketConnection, |
|
|
|
|
|
|
|
data: ws.IDeviceSubscribeRequest |
|
|
|
|
|
|
|
): Promise<ws.ServerResponseData<"deviceSubscribe">> { |
|
|
|
this.checkAuthorization(); |
|
|
|
this.checkAuthorization(); |
|
|
|
const userDevice = this.checkDevice(data.deviceId); |
|
|
|
const userDevice = this.checkDevice(data.deviceId); |
|
|
|
const deviceId = userDevice.deviceId!; |
|
|
|
const deviceId = userDevice.deviceId!; |
|
|
|
if (!this.deviceSubscriptions.has(deviceId)) { |
|
|
|
if (!this.deviceSubscriptions.has(deviceId)) { |
|
|
|
const device = this.state.mqttClient.acquireDevice(deviceId); |
|
|
|
const device = this.state.mqttClient.acquireDevice(deviceId); |
|
|
|
log.debug({ deviceId, userId: this.userId }, "websocket client subscribed to device"); |
|
|
|
log.debug( |
|
|
|
|
|
|
|
{ deviceId, userId: this.userId }, |
|
|
|
|
|
|
|
"websocket client subscribed to device" |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const autorunDisposer = autorun(() => { |
|
|
|
const autorunDisposer = autorun( |
|
|
|
|
|
|
|
() => { |
|
|
|
const json = serialize(schema.sprinklersDevice, device); |
|
|
|
const json = serialize(schema.sprinklersDevice, device); |
|
|
|
log.trace({ device: json }); |
|
|
|
log.trace({ device: json }); |
|
|
|
const updateData: ws.IDeviceUpdate = { deviceId, data: json }; |
|
|
|
const updateData: ws.IDeviceUpdate = { deviceId, data: json }; |
|
|
|
this.sendNotification("deviceUpdate", updateData); |
|
|
|
this.sendNotification("deviceUpdate", updateData); |
|
|
|
}, { delay: 100 }); |
|
|
|
}, |
|
|
|
|
|
|
|
{ delay: 100 } |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
this.deviceSubscriptions.set(deviceId, () => { |
|
|
|
this.deviceSubscriptions.set(deviceId, () => { |
|
|
|
autorunDisposer(); |
|
|
|
autorunDisposer(); |
|
|
@ -223,13 +273,15 @@ class WebSocketRequestHandlers implements ws.ClientRequestHandlers { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const response: ws.IDeviceSubscribeResponse = { |
|
|
|
const response: ws.IDeviceSubscribeResponse = { |
|
|
|
deviceId, |
|
|
|
deviceId |
|
|
|
}; |
|
|
|
}; |
|
|
|
return { result: "success", data: response }; |
|
|
|
return { result: "success", data: response }; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async deviceUnsubscribe(this: WebSocketConnection, data: ws.IDeviceSubscribeRequest): |
|
|
|
async deviceUnsubscribe( |
|
|
|
Promise<ws.ServerResponseData<"deviceUnsubscribe">> { |
|
|
|
this: WebSocketConnection, |
|
|
|
|
|
|
|
data: ws.IDeviceSubscribeRequest |
|
|
|
|
|
|
|
): Promise<ws.ServerResponseData<"deviceUnsubscribe">> { |
|
|
|
this.checkAuthorization(); |
|
|
|
this.checkAuthorization(); |
|
|
|
const userDevice = this.checkDevice(data.deviceId); |
|
|
|
const userDevice = this.checkDevice(data.deviceId); |
|
|
|
const deviceId = userDevice.deviceId!; |
|
|
|
const deviceId = userDevice.deviceId!; |
|
|
@ -240,18 +292,20 @@ class WebSocketRequestHandlers implements ws.ClientRequestHandlers { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const response: ws.IDeviceSubscribeResponse = { |
|
|
|
const response: ws.IDeviceSubscribeResponse = { |
|
|
|
deviceId, |
|
|
|
deviceId |
|
|
|
}; |
|
|
|
}; |
|
|
|
return { result: "success", data: response }; |
|
|
|
return { result: "success", data: response }; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async deviceCall(this: WebSocketConnection, data: ws.IDeviceCallRequest): |
|
|
|
async deviceCall( |
|
|
|
Promise<ws.ServerResponseData<"deviceCall">> { |
|
|
|
this: WebSocketConnection, |
|
|
|
|
|
|
|
data: ws.IDeviceCallRequest |
|
|
|
|
|
|
|
): Promise<ws.ServerResponseData<"deviceCall">> { |
|
|
|
this.checkAuthorization(); |
|
|
|
this.checkAuthorization(); |
|
|
|
try { |
|
|
|
try { |
|
|
|
const response = await this.doDeviceCallRequest(data); |
|
|
|
const response = await this.doDeviceCallRequest(data); |
|
|
|
const resData: ws.IDeviceCallResponse = { |
|
|
|
const resData: ws.IDeviceCallResponse = { |
|
|
|
data: response, |
|
|
|
data: response |
|
|
|
}; |
|
|
|
}; |
|
|
|
return { result: "success", data: resData }; |
|
|
|
return { result: "success", data: resData }; |
|
|
|
} catch (err) { |
|
|
|
} catch (err) { |
|
|
|