websocket.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. package websocket
  2. import (
  3. "net/http"
  4. "sync"
  5. apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
  6. "github.com/OliveTin/OliveTin/internal/executor"
  7. ws "github.com/gorilla/websocket"
  8. log "github.com/sirupsen/logrus"
  9. "google.golang.org/protobuf/encoding/protojson"
  10. "google.golang.org/protobuf/reflect/protoreflect"
  11. )
  12. var upgrader = ws.Upgrader{
  13. CheckOrigin: checkOriginPermissive,
  14. }
  15. var (
  16. sendmutex = sync.Mutex{}
  17. )
  18. type WebsocketClient struct {
  19. conn *ws.Conn
  20. }
  21. var clients []*WebsocketClient
  22. var marshalOptions = protojson.MarshalOptions{
  23. UseProtoNames: false, // eg: canExec for js instead of can_exec from protobuf
  24. EmitUnpopulated: true,
  25. }
  26. var ExecutionListener WebsocketExecutionListener
  27. type WebsocketExecutionListener struct{}
  28. func (WebsocketExecutionListener) OnExecutionStarted(ile *executor.InternalLogEntry) {
  29. broadcast(&apiv1.EventExecutionStarted{
  30. LogEntry: internalLogEntryToPb(ile),
  31. })
  32. }
  33. func OnEntityChanged() {
  34. broadcast(&apiv1.EventEntityChanged{})
  35. }
  36. func (WebsocketExecutionListener) OnActionMapRebuilt() {
  37. broadcast(&apiv1.EventConfigChanged{})
  38. }
  39. /*
  40. The default checkOrigin function checks that the origin (browser) matches the
  41. request origin. However in OliveTin we expect many users to deliberately proxy
  42. the connection with reverse proxies.
  43. So, we just permit any origin. After some searching I'm not sure if this exposes
  44. OliveTin to security issues, but it seems probably not. It would be possible to
  45. create a config option like PermitWebsocketConnectionsFrom or something, but
  46. I'd prefer if OliveTin works as much as possible "out of the box".
  47. If this does expose OliveTin to security issues, it will be changed in the
  48. future obviously.
  49. */
  50. func checkOriginPermissive(r *http.Request) bool {
  51. return true
  52. }
  53. func (WebsocketExecutionListener) OnOutputChunk(chunk []byte, executionTrackingId string) {
  54. log.Tracef("outputchunk: %s", string(chunk))
  55. oc := &apiv1.EventOutputChunk{
  56. Output: string(chunk),
  57. ExecutionTrackingId: executionTrackingId,
  58. }
  59. broadcast(oc)
  60. }
  61. func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.InternalLogEntry) {
  62. evt := &apiv1.EventExecutionFinished{
  63. LogEntry: internalLogEntryToPb(logEntry),
  64. }
  65. log.Infof("WS Execution finished: %v", evt.LogEntry)
  66. broadcast(evt)
  67. }
  68. func broadcast(pbmsg protoreflect.ProtoMessage) {
  69. payload, err := marshalOptions.Marshal(pbmsg)
  70. if err != nil {
  71. log.Errorf("websocket marshal error: %v", err)
  72. return
  73. }
  74. messageType := pbmsg.ProtoReflect().Descriptor().FullName()
  75. // <EVIL>
  76. // So, the websocket wants to encode messages using the same protomarshaller
  77. // as the REST API - this gives consistency instead of using encoding/json
  78. // and allows us to set specific marshalOptions.
  79. //
  80. // However, the protomarshaller will marshal the type, but the JavaScript at
  81. // the other end has no idea what type this object is - as we're just sending
  82. // it as JSON over the websocket.
  83. //
  84. // Therefore, we wrap the nicely marsheled bytes in a hacky JSON string
  85. // literal and encode that string just with a byte array cast.
  86. hackyMessageEnvelope := "{\"type\": \"" + messageType + "\", \"payload\": "
  87. hackyMessage := []byte{}
  88. hackyMessage = append(hackyMessage, []byte(hackyMessageEnvelope)...)
  89. hackyMessage = append(hackyMessage, payload...)
  90. hackyMessage = append(hackyMessage, []byte("}")...)
  91. // </EVIL>
  92. sendmutex.Lock()
  93. for _, client := range clients {
  94. client.conn.WriteMessage(ws.TextMessage, hackyMessage)
  95. }
  96. sendmutex.Unlock()
  97. }
  98. func (c *WebsocketClient) messageLoop() {
  99. for {
  100. mt, message, err := c.conn.ReadMessage()
  101. if err != nil {
  102. log.Debugf("err: %v", err)
  103. break
  104. }
  105. log.Tracef("websocket recv: %s %d", message, mt)
  106. }
  107. }
  108. func HandleWebsocket(w http.ResponseWriter, r *http.Request) bool {
  109. c, err := upgrader.Upgrade(w, r, nil)
  110. if err != nil {
  111. log.Warnf("Websocket issue: %v", err)
  112. return false
  113. }
  114. // defer c.Close()
  115. wsclient := &WebsocketClient{
  116. conn: c,
  117. }
  118. sendmutex.Lock()
  119. clients = append(clients, wsclient)
  120. sendmutex.Unlock()
  121. go wsclient.messageLoop()
  122. return true
  123. }
  124. func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *apiv1.LogEntry {
  125. return &apiv1.LogEntry{
  126. ActionTitle: logEntry.ActionTitle,
  127. ActionIcon: logEntry.ActionIcon,
  128. ActionId: logEntry.ActionId,
  129. DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
  130. DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
  131. Output: logEntry.Output,
  132. TimedOut: logEntry.TimedOut,
  133. Blocked: logEntry.Blocked,
  134. ExitCode: logEntry.ExitCode,
  135. Tags: logEntry.Tags,
  136. ExecutionTrackingId: logEntry.ExecutionTrackingID,
  137. ExecutionStarted: logEntry.ExecutionStarted,
  138. ExecutionFinished: logEntry.ExecutionFinished,
  139. User: logEntry.Username,
  140. }
  141. }