websocket.go 6.5 KB

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