recorder.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package recorder
  15. import (
  16. "strings"
  17. "sync"
  18. "time"
  19. "github.com/LeeEirc/terminalparser"
  20. "yunion.io/x/jsonutils"
  21. "yunion.io/x/log"
  22. "yunion.io/x/pkg/errors"
  23. "yunion.io/x/onecloud/pkg/mcclient"
  24. "yunion.io/x/onecloud/pkg/webconsole/models"
  25. "yunion.io/x/onecloud/pkg/webconsole/options"
  26. )
  27. type Recoder interface {
  28. Start()
  29. Write(userInput string, ptyOutput string)
  30. }
  31. type Object struct {
  32. Id string
  33. Name string
  34. Type string
  35. LoginUser string
  36. Notes jsonutils.JSONObject
  37. }
  38. func NewObject(id, name, oType, loginUser string, notes jsonutils.JSONObject) *Object {
  39. return &Object{
  40. Id: id,
  41. Name: name,
  42. Type: oType,
  43. LoginUser: loginUser,
  44. Notes: notes,
  45. }
  46. }
  47. type cmdRecoder struct {
  48. cs *mcclient.ClientSession
  49. sessionId string
  50. accessedAt time.Time
  51. userInputStart bool
  52. userInputBuff string
  53. ptyInitialOutput string
  54. ptyOutputBuff string
  55. ps1Parsed bool
  56. ps1 string
  57. cmdCh chan string
  58. object *Object
  59. wLock *sync.Mutex
  60. }
  61. func NewCmdRecorder(s *mcclient.ClientSession, obj *Object, sessionId string, accessedAt time.Time) Recoder {
  62. return &cmdRecoder{
  63. cs: s,
  64. sessionId: sessionId,
  65. accessedAt: accessedAt,
  66. userInputStart: false,
  67. userInputBuff: "",
  68. ptyOutputBuff: "",
  69. cmdCh: make(chan string),
  70. object: obj,
  71. wLock: new(sync.Mutex),
  72. }
  73. }
  74. func (r *cmdRecoder) Write(userInput string, ptyOutput string) {
  75. if !options.Options.EnableCommandRecording {
  76. return
  77. }
  78. r.wLock.Lock()
  79. defer r.wLock.Unlock()
  80. if len(userInput) != 0 {
  81. r.userInputStart = true
  82. }
  83. if !r.userInputStart && userInput == "" && len(ptyOutput) > 0 {
  84. r.ptyInitialOutput += ptyOutput
  85. }
  86. // try parse PS1
  87. if !r.ps1Parsed && r.userInputBuff != "" && userInput == "\r" && r.ptyInitialOutput != "" {
  88. outs := r.parsePtyOutputs(r.ptyInitialOutput)
  89. if len(outs) != 0 && len(outs) > 1 {
  90. r.ps1 = outs[len(outs)-1]
  91. }
  92. r.ps1Parsed = true
  93. }
  94. // user enter command
  95. if userInput == "\r" && r.userInputBuff != "" {
  96. r.sendMessage(r.ptyOutputBuff)
  97. return
  98. }
  99. r.userInputBuff += userInput
  100. r.ptyOutputBuff += ptyOutput
  101. }
  102. func (r *cmdRecoder) cleanCmd() *cmdRecoder {
  103. r.userInputBuff = ""
  104. r.ptyOutputBuff = ""
  105. return r
  106. }
  107. func (r *cmdRecoder) parsePtyOutputs(data string) []string {
  108. s := terminalparser.Screen{
  109. Rows: make([]*terminalparser.Row, 0, 1024),
  110. Cursor: &terminalparser.Cursor{},
  111. }
  112. return s.Parse([]byte(data))
  113. }
  114. func (r *cmdRecoder) sendMessage(ptyOutputBuff string) {
  115. ptyOuts := r.parsePtyOutputs(ptyOutputBuff)
  116. if len(ptyOuts) == 0 {
  117. return
  118. }
  119. cmd := ptyOuts[len(ptyOuts)-1]
  120. if r.ps1 != "" {
  121. cmd = strings.TrimPrefix(cmd, r.ps1)
  122. }
  123. r.cleanCmd()
  124. r.cmdCh <- cmd
  125. log.Debugf("sendMessage ps1: %q, ptyOuts: %#v, cmd: %q", r.ps1, ptyOuts, cmd)
  126. }
  127. func (r *cmdRecoder) Start() {
  128. for {
  129. select {
  130. case cmd := <-r.cmdCh:
  131. if err := r.save(cmd); err != nil {
  132. log.Errorf("save comand %q error: %v", cmd, err)
  133. }
  134. }
  135. }
  136. }
  137. func (r *cmdRecoder) save(command string) error {
  138. if r.object == nil {
  139. return nil
  140. }
  141. if command == "" {
  142. return nil
  143. }
  144. userCred := r.cs.GetToken()
  145. input := r.newModelInput(userCred, command)
  146. _, err := models.GetCommandLogManager().Create(r.cs.GetContext(), userCred, input)
  147. if err != nil {
  148. return errors.Wrapf(err, "Create command log by input: %s", jsonutils.Marshal(input))
  149. }
  150. return nil
  151. }
  152. func (r *cmdRecoder) newModelInput(userCred mcclient.TokenCredential, command string) *models.CommandLogCreateInput {
  153. return &models.CommandLogCreateInput{
  154. ObjId: r.object.Id,
  155. ObjName: r.object.Name,
  156. ObjType: r.object.Type,
  157. Notes: r.object.Notes,
  158. Action: "record",
  159. UserId: userCred.GetUserId(),
  160. User: userCred.GetUserName(),
  161. TenantId: userCred.GetTenantId(),
  162. Tenant: userCred.GetTenantName(),
  163. DomainId: userCred.GetDomainId(),
  164. Domain: userCred.GetDomainName(),
  165. ProjectDomainId: userCred.GetProjectDomainId(),
  166. ProjectDomain: userCred.GetProjectDomain(),
  167. Roles: strings.Join(userCred.GetRoles(), ","),
  168. SessionId: r.sessionId,
  169. AccessedAt: r.accessedAt,
  170. LoginUser: r.object.LoginUser,
  171. Type: models.CommandTypeSSH,
  172. StartTime: time.Now(),
  173. Ps1: r.ps1,
  174. Command: command,
  175. }
  176. }