console.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  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. URL returns the current connection URL.
  93. */
  94. URL() string
  95. /*
  96. Partition returns the current partition.
  97. */
  98. Partition() string
  99. /*
  100. Sets the current partition.
  101. */
  102. SetPartition(string)
  103. /*
  104. AskPassword asks the user for a password.
  105. */
  106. AskPassword() string
  107. /*
  108. Req is a convenience function to send common requests.
  109. */
  110. Req(endpoint string, method string, content []byte) (interface{}, error)
  111. /*
  112. SendRequest sends a request to the connected server. The calling code of the
  113. function can specify the contentType (e.g. application/json), the method
  114. (e.g. GET), the content (for POST, PUT and DELETE requests) and a request
  115. modifier function which can be used to modify the request object before the
  116. request to the server is being made.
  117. */
  118. SendRequest(endpoint string, contentType string, method string,
  119. content []byte, reqMod func(*http.Request)) (string, *http.Response, error)
  120. /*
  121. Out returns a writer which can be used to write to the console.
  122. */
  123. Out() io.Writer
  124. /*
  125. ExportBuffer returns a buffer which can be used to write exportable data.
  126. */
  127. ExportBuffer() *bytes.Buffer
  128. }
  129. /*
  130. CommError is a communication error from the ConsoleAPI.
  131. */
  132. type CommError struct {
  133. err error // Nice error message
  134. Resp *http.Response // Error response from the REST API
  135. }
  136. /*
  137. Error returns a textual representation of this error.
  138. */
  139. func (c *CommError) Error() string {
  140. return c.err.Error()
  141. }
  142. /*
  143. Command describes an available command.
  144. */
  145. type Command interface {
  146. /*
  147. Name returns the command name (as it should be typed).
  148. */
  149. Name() string
  150. /*
  151. ShortDescription returns a short description of the command (single line).
  152. */
  153. ShortDescription() string
  154. /*
  155. LongDescription returns an extensive description of the command (can be multiple lines).
  156. */
  157. LongDescription() string
  158. /*
  159. Run executes the command.
  160. */
  161. Run(args []string, capi CommandConsoleAPI) error
  162. }
  163. // EliasDB Console
  164. // ===============
  165. /*
  166. EliasDBConsole implements the basic console functionality like login and version.
  167. */
  168. type EliasDBConsole struct {
  169. url string // Current server url (e.g. http://localhost:9090)
  170. part string // Current partition
  171. out io.Writer // Output for this console
  172. export *bytes.Buffer // Export buffer
  173. childConsoles []CommandConsole // List of child consoles
  174. authCookie *http.Cookie // User token
  175. credsAsked bool // Flag if the credentials have been asked
  176. CommandMap map[string]Command // Map of registered commands
  177. GetCredentials func() (string, string) // Ask the user for credentials
  178. GetPassword func() string // Ask the user for a password
  179. }
  180. /*
  181. URL returns the current connected server URL.
  182. */
  183. func (c *EliasDBConsole) URL() string {
  184. return c.url
  185. }
  186. /*
  187. Out returns a writer which can be used to write to the console.
  188. */
  189. func (c *EliasDBConsole) Out() io.Writer {
  190. return c.out
  191. }
  192. /*
  193. Partition returns the current partition.
  194. */
  195. func (c *EliasDBConsole) Partition() string {
  196. return c.part
  197. }
  198. /*
  199. SetPartition sets the current partition.
  200. */
  201. func (c *EliasDBConsole) SetPartition(part string) {
  202. c.part = part
  203. }
  204. /*
  205. AskPassword asks the user for a password.
  206. */
  207. func (c *EliasDBConsole) AskPassword() string {
  208. return c.GetPassword()
  209. }
  210. /*
  211. ExportBuffer returns a buffer which can be used to write exportable data.
  212. */
  213. func (c *EliasDBConsole) ExportBuffer() *bytes.Buffer {
  214. return c.export
  215. }
  216. /*
  217. Run executes one or more commands. It returns an error if the command
  218. had an unexpected result and a flag if the command was handled.
  219. */
  220. func (c *EliasDBConsole) Run(cmd string) (bool, error) {
  221. // First split a line with multiple commands
  222. cmds := strings.Split(cmd, ";")
  223. for _, cmd := range cmds {
  224. // Run the command and return if there is an error
  225. if ok, err := c.RunCommand(cmd); err != nil {
  226. // Return if there was an unexpected error
  227. return false, err
  228. } else if !ok {
  229. // Try child consoles
  230. for _, c := range c.childConsoles {
  231. if ok, err := c.Run(cmd); err != nil || ok {
  232. return ok, err
  233. }
  234. }
  235. return false, fmt.Errorf("Unknown command")
  236. }
  237. }
  238. // Everything was handled
  239. return true, nil
  240. }
  241. /*
  242. RunCommand executes a single command. It returns an error for unexpected results and
  243. a flag if the command was handled.
  244. */
  245. func (c *EliasDBConsole) RunCommand(cmdString string) (bool, error) {
  246. cmdSplit := strings.Fields(cmdString)
  247. if len(cmdSplit) > 0 {
  248. cmd := cmdSplit[0]
  249. args := cmdSplit[1:]
  250. // Reset the export buffer if we are not exporting
  251. if cmd != CommandExport {
  252. c.export.Reset()
  253. }
  254. if config.Bool(config.EnableAccessControl) {
  255. // Extra commands when access control is enabled
  256. if cmd == "logout" {
  257. // Special command "logout" to remove the current auth token
  258. c.authCookie = nil
  259. fmt.Fprintln(c.out, "Current user logged out.")
  260. } else if cmd != "ver" && cmd != "whoami" && cmd != "help" &&
  261. cmd != "?" && cmd != "export" {
  262. // Do not authenticate if running local commands
  263. // Authenticate user this is a NOP if the user is authenticated unless
  264. // the command "login" is given. Then the user is reauthenticated.
  265. c.Authenticate(cmd == "login")
  266. }
  267. }
  268. if cmdObj, ok := c.CommandMap[cmd]; ok {
  269. return true, cmdObj.Run(args, c)
  270. } else if cmd == "?" {
  271. return true, c.CommandMap["help"].Run(args, c)
  272. }
  273. }
  274. return false, nil
  275. }
  276. /*
  277. Commands returns a sorted list of all available commands.
  278. */
  279. func (c *EliasDBConsole) Commands() []Command {
  280. var res []Command
  281. for _, c := range c.CommandMap {
  282. res = append(res, c)
  283. }
  284. sort.Slice(res, func(i, j int) bool {
  285. return res[i].Name() < res[j].Name()
  286. })
  287. return res
  288. }
  289. /*
  290. Authenticate authenticates the user if necessary.
  291. */
  292. func (c *EliasDBConsole) Authenticate(force bool) {
  293. // Only do the authentication if we haven't asked yet or it is
  294. // explicitly desired
  295. if !c.credsAsked || force {
  296. c.credsAsked = false
  297. for !c.credsAsked {
  298. // Ask for credentials
  299. user, pass := c.GetCredentials()
  300. if user == "" {
  301. // User doesn't want to authenticate - do nothing
  302. fmt.Fprintln(c.out, "Skipping authentication")
  303. c.credsAsked = true
  304. return
  305. }
  306. content, err := json.Marshal(map[string]interface{}{
  307. "user": user,
  308. "pass": pass,
  309. })
  310. errorutil.AssertOk(err) // Json marshall should never fail
  311. res, resp, err := c.SendRequest(ac.EndpointLogin, "application/json", "POST", content, nil)
  312. if err == nil {
  313. if resp.StatusCode == http.StatusOK && len(resp.Cookies()) > 0 {
  314. fmt.Fprintln(c.out, "Login as user", user)
  315. c.authCookie = resp.Cookies()[0]
  316. c.credsAsked = true
  317. return
  318. }
  319. }
  320. fmt.Fprintln(c.out, fmt.Sprintf("Login failed for user %s: %s (error=%v)", user, res, err))
  321. }
  322. }
  323. }
  324. /*
  325. Req is a convenience function to send common requests.
  326. */
  327. func (c *EliasDBConsole) Req(endpoint string, method string, content []byte) (interface{}, error) {
  328. var res interface{}
  329. bodyStr, resp, err := c.SendRequest(endpoint, "application/json", method, content,
  330. func(r *http.Request) {})
  331. if err == nil {
  332. // Try json decoding
  333. if jerr := json.Unmarshal([]byte(bodyStr), &res); jerr != nil {
  334. res = bodyStr
  335. // Check if we got an error back
  336. if resp.StatusCode != http.StatusOK {
  337. return nil, &CommError{
  338. fmt.Errorf("%s request to %s failed: %s", method, endpoint, bodyStr),
  339. resp,
  340. }
  341. }
  342. }
  343. }
  344. return res, err
  345. }
  346. /*
  347. SendRequest sends a request to the connected server. The calling code of the
  348. function can specify the contentType (e.g. application/json), the method
  349. (e.g. GET), the content (for POST, PUT and DELETE requests) and a request
  350. modifier function which can be used to modify the request object before the
  351. request to the server is being made.
  352. */
  353. func (c *EliasDBConsole) SendRequest(endpoint string, contentType string, method string,
  354. content []byte, reqMod func(*http.Request)) (string, *http.Response, error) {
  355. var bodyStr string
  356. var req *http.Request
  357. var resp *http.Response
  358. var err error
  359. if content != nil {
  360. req, err = http.NewRequest(method, c.url+endpoint, bytes.NewBuffer(content))
  361. } else {
  362. req, err = http.NewRequest(method, c.url+endpoint, nil)
  363. }
  364. if err == nil {
  365. req.Header.Set("Content-Type", contentType)
  366. // Set auth cookie
  367. if c.authCookie != nil {
  368. req.AddCookie(c.authCookie)
  369. }
  370. if reqMod != nil {
  371. reqMod(req)
  372. }
  373. // Console client does not verify the SSL keys
  374. tlsConfig := &tls.Config{
  375. InsecureSkipVerify: true,
  376. }
  377. transport := &http.Transport{TLSClientConfig: tlsConfig}
  378. client := &http.Client{
  379. Transport: transport,
  380. }
  381. resp, err = client.Do(req)
  382. if err == nil {
  383. defer resp.Body.Close()
  384. body, _ := ioutil.ReadAll(resp.Body)
  385. bodyStr = strings.Trim(string(body), " \n")
  386. }
  387. }
  388. // Just return the body
  389. return bodyStr, resp, err
  390. }
  391. // Util functions
  392. // ==============
  393. /*
  394. cmdStartsWithKeyword checks if a given command line starts with a given list
  395. of keywords.
  396. */
  397. func cmdStartsWithKeyword(cmd string, keywords []string) bool {
  398. ss := strings.Fields(strings.ToLower(cmd))
  399. if len(ss) > 0 {
  400. firstCmd := strings.ToLower(ss[0])
  401. for _, k := range keywords {
  402. if k == firstCmd || strings.HasPrefix(firstCmd, k) {
  403. return true
  404. }
  405. }
  406. }
  407. return false
  408. }