console.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. /*
  2. * EliasDB
  3. *
  4. * Copyright 2016 Matthias Ladkau. All rights reserved.
  5. *
  6. * This Source Code Form is subject to the terms of the Mozilla Public
  7. * License, v. 2.0. If a copy of the MPL was not distributed with this
  8. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  9. */
  10. /*
  11. Package console contains the console command processor for EliasDB.
  12. */
  13. package console
  14. import (
  15. "bytes"
  16. "crypto/tls"
  17. "encoding/json"
  18. "fmt"
  19. "io"
  20. "io/ioutil"
  21. "net/http"
  22. "sort"
  23. "strings"
  24. "devt.de/krotik/common/errorutil"
  25. "devt.de/krotik/eliasdb/api/ac"
  26. "devt.de/krotik/eliasdb/config"
  27. )
  28. /*
  29. NewConsole creates a new Console object which can parse and execute given
  30. commands from the given Reader and outputs the result to the Writer. It
  31. optionally exports data with the given export function via the save command.
  32. Export is disabled if no export function is defined.
  33. */
  34. func NewConsole(url string, out io.Writer, getCredentials func() (string, string),
  35. getPassword func() string, exportFunc func([]string, *bytes.Buffer) error) CommandConsole {
  36. cmdMap := make(map[string]Command)
  37. cmdMap[CommandHelp] = &CmdHelp{}
  38. cmdMap[CommandVer] = &CmdVer{}
  39. // Adding commands specific to access control
  40. if config.Bool(config.EnableAccessControl) {
  41. cmdMap[CommandLogin] = &CmdLogin{}
  42. cmdMap[CommandLogout] = &CmdLogout{}
  43. cmdMap[CommandWhoAmI] = &CmdWhoAmI{}
  44. cmdMap[CommandUsers] = &CmdUsers{}
  45. cmdMap[CommandGroups] = &CmdGroups{}
  46. cmdMap[CommandUseradd] = &CmdUseradd{}
  47. cmdMap[CommandGroupadd] = &CmdGroupadd{}
  48. cmdMap[CommandUserdel] = &CmdUserdel{}
  49. cmdMap[CommandGroupdel] = &CmdGroupdel{}
  50. cmdMap[CommandNewpass] = &CmdNewpass{}
  51. cmdMap[CommandJoingroup] = &CmdJoingroup{}
  52. cmdMap[CommandLeavegroup] = &CmdLeavegroup{}
  53. cmdMap[CommandGrantperm] = &CmdGrantperm{}
  54. cmdMap[CommandRevokeperm] = &CmdRevokeperm{}
  55. }
  56. cmdMap[CommandInfo] = &CmdInfo{}
  57. cmdMap[CommandPart] = &CmdPart{}
  58. cmdMap[CommandFind] = &CmdFind{}
  59. // Add export if we got an export function
  60. if exportFunc != nil {
  61. cmdMap[CommandExport] = &CmdExport{exportFunc}
  62. }
  63. c := &EliasDBConsole{url, "main", out, bytes.NewBuffer(nil), nil,
  64. nil, false, cmdMap, getCredentials, getPassword}
  65. c.childConsoles = []CommandConsole{&EQLConsole{c}, &GraphQLConsole{c}}
  66. return c
  67. }
  68. /*
  69. CommandConsole is the main interface for command processors.
  70. */
  71. type CommandConsole interface {
  72. /*
  73. Run executes one or more commands. It returns an error if the command
  74. had an unexpected result and a flag if the command was handled.
  75. */
  76. Run(cmd string) (bool, error)
  77. /*
  78. Commands returns a sorted list of all available commands.
  79. */
  80. Commands() []Command
  81. }
  82. /*
  83. CommandConsoleAPI is the console interface which commands can use to send communicate to the server.
  84. */
  85. type CommandConsoleAPI interface {
  86. CommandConsole
  87. /*
  88. Authenticate authenticates the user if necessary.
  89. */
  90. Authenticate(force bool)
  91. /*
  92. Partition returns the current partition.
  93. */
  94. Partition() string
  95. /*
  96. Sets the current partition.
  97. */
  98. SetPartition(string)
  99. /*
  100. AskPassword asks the user for a password.
  101. */
  102. AskPassword() string
  103. /*
  104. Req is a convenience function to send common requests.
  105. */
  106. Req(endpoint string, method string, content []byte) (interface{}, error)
  107. /*
  108. SendRequest sends a request to the connected server. The calling code of the
  109. function can specify the contentType (e.g. application/json), the method
  110. (e.g. GET), the content (for POST, PUT and DELETE requests) and a request
  111. modifier function which can be used to modify the request object before the
  112. request to the server is being made.
  113. */
  114. SendRequest(endpoint string, contentType string, method string,
  115. content []byte, reqMod func(*http.Request)) (string, *http.Response, error)
  116. /*
  117. Out returns a writer which can be used to write to the console.
  118. */
  119. Out() io.Writer
  120. /*
  121. ExportBuffer returns a buffer which can be used to write exportable data.
  122. */
  123. ExportBuffer() *bytes.Buffer
  124. }
  125. /*
  126. CommError is a communication error from the ConsoleAPI.
  127. */
  128. type CommError struct {
  129. err error // Nice error message
  130. Resp *http.Response // Error response from the REST API
  131. }
  132. /*
  133. Error returns a textual representation of this error.
  134. */
  135. func (c *CommError) Error() string {
  136. return c.err.Error()
  137. }
  138. /*
  139. Command describes an available command.
  140. */
  141. type Command interface {
  142. /*
  143. Name returns the command name (as it should be typed).
  144. */
  145. Name() string
  146. /*
  147. ShortDescription returns a short description of the command (single line).
  148. */
  149. ShortDescription() string
  150. /*
  151. LongDescription returns an extensive description of the command (can be multiple lines).
  152. */
  153. LongDescription() string
  154. /*
  155. Run executes the command.
  156. */
  157. Run(args []string, capi CommandConsoleAPI) error
  158. }
  159. // EliasDB Console
  160. // ===============
  161. /*
  162. EliasDBConsole implements the basic console functionality like login and version.
  163. */
  164. type EliasDBConsole struct {
  165. url string // Current server url (e.g. http://localhost:9090)
  166. part string // Current partition
  167. out io.Writer // Output for this console
  168. export *bytes.Buffer // Export buffer
  169. childConsoles []CommandConsole // List of child consoles
  170. authCookie *http.Cookie // User token
  171. credsAsked bool // Flag if the credentials have been asked
  172. CommandMap map[string]Command // Map of registered commands
  173. GetCredentials func() (string, string) // Ask the user for credentials
  174. GetPassword func() string // Ask the user for a password
  175. }
  176. /*
  177. Out returns a writer which can be used to write to the console.
  178. */
  179. func (c *EliasDBConsole) Out() io.Writer {
  180. return c.out
  181. }
  182. /*
  183. Partition returns the current partition.
  184. */
  185. func (c *EliasDBConsole) Partition() string {
  186. return c.part
  187. }
  188. /*
  189. SetPartition sets the current partition.
  190. */
  191. func (c *EliasDBConsole) SetPartition(part string) {
  192. c.part = part
  193. }
  194. /*
  195. AskPassword asks the user for a password.
  196. */
  197. func (c *EliasDBConsole) AskPassword() string {
  198. return c.GetPassword()
  199. }
  200. /*
  201. ExportBuffer returns a buffer which can be used to write exportable data.
  202. */
  203. func (c *EliasDBConsole) ExportBuffer() *bytes.Buffer {
  204. return c.export
  205. }
  206. /*
  207. Run executes one or more commands. It returns an error if the command
  208. had an unexpected result and a flag if the command was handled.
  209. */
  210. func (c *EliasDBConsole) Run(cmd string) (bool, error) {
  211. // First split a line with multiple commands
  212. cmds := strings.Split(cmd, ";")
  213. for _, cmd := range cmds {
  214. // Run the command and return if there is an error
  215. if ok, err := c.RunCommand(cmd); err != nil {
  216. // Return if there was an unexpected error
  217. return false, err
  218. } else if !ok {
  219. // Try child consoles
  220. for _, c := range c.childConsoles {
  221. if ok, err := c.Run(cmd); err != nil || ok {
  222. return ok, err
  223. }
  224. }
  225. return false, fmt.Errorf("Unknown command")
  226. }
  227. }
  228. // Everything was handled
  229. return true, nil
  230. }
  231. /*
  232. RunCommand executes a single command. It returns an error for unexpected results and
  233. a flag if the command was handled.
  234. */
  235. func (c *EliasDBConsole) RunCommand(cmdString string) (bool, error) {
  236. cmdSplit := strings.Fields(cmdString)
  237. if len(cmdSplit) > 0 {
  238. cmd := cmdSplit[0]
  239. args := cmdSplit[1:]
  240. // Reset the export buffer if we are not exporting
  241. if cmd != CommandExport {
  242. c.export.Reset()
  243. }
  244. if config.Bool(config.EnableAccessControl) {
  245. // Extra commands when access control is enabled
  246. if cmd == "logout" {
  247. // Special command "logout" to remove the current auth token
  248. c.authCookie = nil
  249. fmt.Fprintln(c.out, "Current user logged out.")
  250. } else if cmd != "ver" && cmd != "whoami" && cmd != "help" && cmd != "export" {
  251. // Do not authenticate if running local commands
  252. // Authenticate user this is a NOP if the user is authenticated unless
  253. // the command "login" is given. Then the user is reauthenticated.
  254. c.Authenticate(cmd == "login")
  255. }
  256. }
  257. if cmd, ok := c.CommandMap[cmd]; ok {
  258. return true, cmd.Run(args, c)
  259. }
  260. }
  261. return false, nil
  262. }
  263. /*
  264. Commands returns a sorted list of all available commands.
  265. */
  266. func (c *EliasDBConsole) Commands() []Command {
  267. var res []Command
  268. for _, c := range c.CommandMap {
  269. res = append(res, c)
  270. }
  271. sort.Slice(res, func(i, j int) bool {
  272. return res[i].Name() < res[j].Name()
  273. })
  274. return res
  275. }
  276. /*
  277. Authenticate authenticates the user if necessary.
  278. */
  279. func (c *EliasDBConsole) Authenticate(force bool) {
  280. // Only do the authentication if we haven't asked yet or it is
  281. // explicitly desired
  282. if !c.credsAsked || force {
  283. c.credsAsked = false
  284. for !c.credsAsked {
  285. // Ask for credentials
  286. user, pass := c.GetCredentials()
  287. if user == "" {
  288. // User doesn't want to authenticate - do nothing
  289. fmt.Fprintln(c.out, "Skipping authentication")
  290. c.credsAsked = true
  291. return
  292. }
  293. content, err := json.Marshal(map[string]interface{}{
  294. "user": user,
  295. "pass": pass,
  296. })
  297. errorutil.AssertOk(err) // Json marshall should never fail
  298. res, resp, err := c.SendRequest(ac.EndpointLogin, "application/json", "POST", content, nil)
  299. if err == nil {
  300. if resp.StatusCode == http.StatusOK && len(resp.Cookies()) > 0 {
  301. fmt.Fprintln(c.out, "Login as user", user)
  302. c.authCookie = resp.Cookies()[0]
  303. c.credsAsked = true
  304. return
  305. }
  306. }
  307. fmt.Fprintln(c.out, fmt.Sprintf("Login failed for user %s: %s (error=%v)", user, res, err))
  308. }
  309. }
  310. }
  311. /*
  312. Req is a convenience function to send common requests.
  313. */
  314. func (c *EliasDBConsole) Req(endpoint string, method string, content []byte) (interface{}, error) {
  315. var res interface{}
  316. bodyStr, resp, err := c.SendRequest(endpoint, "application/json", method, content,
  317. func(r *http.Request) {})
  318. if err == nil {
  319. // Try json decoding
  320. if jerr := json.Unmarshal([]byte(bodyStr), &res); jerr != nil {
  321. res = bodyStr
  322. // Check if we got an error back
  323. if resp.StatusCode != http.StatusOK {
  324. return nil, &CommError{
  325. fmt.Errorf("%s request to %s failed: %s", method, endpoint, bodyStr),
  326. resp,
  327. }
  328. }
  329. }
  330. }
  331. return res, err
  332. }
  333. /*
  334. SendRequest sends a request to the connected server. The calling code of the
  335. function can specify the contentType (e.g. application/json), the method
  336. (e.g. GET), the content (for POST, PUT and DELETE requests) and a request
  337. modifier function which can be used to modify the request object before the
  338. request to the server is being made.
  339. */
  340. func (c *EliasDBConsole) SendRequest(endpoint string, contentType string, method string,
  341. content []byte, reqMod func(*http.Request)) (string, *http.Response, error) {
  342. var bodyStr string
  343. var req *http.Request
  344. var err error
  345. if content != nil {
  346. req, err = http.NewRequest(method, c.url+endpoint, bytes.NewBuffer(content))
  347. } else {
  348. req, err = http.NewRequest(method, c.url+endpoint, nil)
  349. }
  350. req.Header.Set("Content-Type", contentType)
  351. // Set auth cookie
  352. if c.authCookie != nil {
  353. req.AddCookie(c.authCookie)
  354. }
  355. if reqMod != nil {
  356. reqMod(req)
  357. }
  358. // Console client does not verify the SSL keys
  359. tlsConfig := &tls.Config{
  360. InsecureSkipVerify: true,
  361. }
  362. transport := &http.Transport{TLSClientConfig: tlsConfig}
  363. client := &http.Client{
  364. Transport: transport,
  365. }
  366. resp, err := client.Do(req)
  367. if err == nil {
  368. defer resp.Body.Close()
  369. body, _ := ioutil.ReadAll(resp.Body)
  370. bodyStr = strings.Trim(string(body), " \n")
  371. }
  372. // Just return the body
  373. return bodyStr, resp, err
  374. }
  375. // Util functions
  376. // ==============
  377. /*
  378. cmdStartsWithKeyword checks if a given command line starts with a given list
  379. of keywords.
  380. */
  381. func cmdStartsWithKeyword(cmd string, keywords []string) bool {
  382. ss := strings.Fields(strings.ToLower(cmd))
  383. if len(ss) > 0 {
  384. firstCmd := strings.ToLower(ss[0])
  385. for _, k := range keywords {
  386. if k == firstCmd || strings.HasPrefix(firstCmd, k) {
  387. return true
  388. }
  389. }
  390. }
  391. return false
  392. }