console.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  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" &&
  251. cmd != "?" && cmd != "export" {
  252. // Do not authenticate if running local commands
  253. // Authenticate user this is a NOP if the user is authenticated unless
  254. // the command "login" is given. Then the user is reauthenticated.
  255. c.Authenticate(cmd == "login")
  256. }
  257. }
  258. if cmdObj, ok := c.CommandMap[cmd]; ok {
  259. return true, cmdObj.Run(args, c)
  260. } else if cmd == "?" {
  261. return true, c.CommandMap["help"].Run(args, c)
  262. }
  263. }
  264. return false, nil
  265. }
  266. /*
  267. Commands returns a sorted list of all available commands.
  268. */
  269. func (c *EliasDBConsole) Commands() []Command {
  270. var res []Command
  271. for _, c := range c.CommandMap {
  272. res = append(res, c)
  273. }
  274. sort.Slice(res, func(i, j int) bool {
  275. return res[i].Name() < res[j].Name()
  276. })
  277. return res
  278. }
  279. /*
  280. Authenticate authenticates the user if necessary.
  281. */
  282. func (c *EliasDBConsole) Authenticate(force bool) {
  283. // Only do the authentication if we haven't asked yet or it is
  284. // explicitly desired
  285. if !c.credsAsked || force {
  286. c.credsAsked = false
  287. for !c.credsAsked {
  288. // Ask for credentials
  289. user, pass := c.GetCredentials()
  290. if user == "" {
  291. // User doesn't want to authenticate - do nothing
  292. fmt.Fprintln(c.out, "Skipping authentication")
  293. c.credsAsked = true
  294. return
  295. }
  296. content, err := json.Marshal(map[string]interface{}{
  297. "user": user,
  298. "pass": pass,
  299. })
  300. errorutil.AssertOk(err) // Json marshall should never fail
  301. res, resp, err := c.SendRequest(ac.EndpointLogin, "application/json", "POST", content, nil)
  302. if err == nil {
  303. if resp.StatusCode == http.StatusOK && len(resp.Cookies()) > 0 {
  304. fmt.Fprintln(c.out, "Login as user", user)
  305. c.authCookie = resp.Cookies()[0]
  306. c.credsAsked = true
  307. return
  308. }
  309. }
  310. fmt.Fprintln(c.out, fmt.Sprintf("Login failed for user %s: %s (error=%v)", user, res, err))
  311. }
  312. }
  313. }
  314. /*
  315. Req is a convenience function to send common requests.
  316. */
  317. func (c *EliasDBConsole) Req(endpoint string, method string, content []byte) (interface{}, error) {
  318. var res interface{}
  319. bodyStr, resp, err := c.SendRequest(endpoint, "application/json", method, content,
  320. func(r *http.Request) {})
  321. if err == nil {
  322. // Try json decoding
  323. if jerr := json.Unmarshal([]byte(bodyStr), &res); jerr != nil {
  324. res = bodyStr
  325. // Check if we got an error back
  326. if resp.StatusCode != http.StatusOK {
  327. return nil, &CommError{
  328. fmt.Errorf("%s request to %s failed: %s", method, endpoint, bodyStr),
  329. resp,
  330. }
  331. }
  332. }
  333. }
  334. return res, err
  335. }
  336. /*
  337. SendRequest sends a request to the connected server. The calling code of the
  338. function can specify the contentType (e.g. application/json), the method
  339. (e.g. GET), the content (for POST, PUT and DELETE requests) and a request
  340. modifier function which can be used to modify the request object before the
  341. request to the server is being made.
  342. */
  343. func (c *EliasDBConsole) SendRequest(endpoint string, contentType string, method string,
  344. content []byte, reqMod func(*http.Request)) (string, *http.Response, error) {
  345. var bodyStr string
  346. var req *http.Request
  347. var resp *http.Response
  348. var err error
  349. if content != nil {
  350. req, err = http.NewRequest(method, c.url+endpoint, bytes.NewBuffer(content))
  351. } else {
  352. req, err = http.NewRequest(method, c.url+endpoint, nil)
  353. }
  354. if err == nil {
  355. req.Header.Set("Content-Type", contentType)
  356. // Set auth cookie
  357. if c.authCookie != nil {
  358. req.AddCookie(c.authCookie)
  359. }
  360. if reqMod != nil {
  361. reqMod(req)
  362. }
  363. // Console client does not verify the SSL keys
  364. tlsConfig := &tls.Config{
  365. InsecureSkipVerify: true,
  366. }
  367. transport := &http.Transport{TLSClientConfig: tlsConfig}
  368. client := &http.Client{
  369. Transport: transport,
  370. }
  371. resp, err = client.Do(req)
  372. if err == nil {
  373. defer resp.Body.Close()
  374. body, _ := ioutil.ReadAll(resp.Body)
  375. bodyStr = strings.Trim(string(body), " \n")
  376. }
  377. }
  378. // Just return the body
  379. return bodyStr, resp, err
  380. }
  381. // Util functions
  382. // ==============
  383. /*
  384. cmdStartsWithKeyword checks if a given command line starts with a given list
  385. of keywords.
  386. */
  387. func cmdStartsWithKeyword(cmd string, keywords []string) bool {
  388. ss := strings.Fields(strings.ToLower(cmd))
  389. if len(ss) > 0 {
  390. firstCmd := strings.ToLower(ss[0])
  391. for _, k := range keywords {
  392. if k == firstCmd || strings.HasPrefix(firstCmd, k) {
  393. return true
  394. }
  395. }
  396. }
  397. return false
  398. }