1
0
Fork 0
You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

234 lines
5.6 KiB
Go

7 years ago
// Command rqlite is the command-line interface for rqlite.
package main
import (
"crypto/tls"
"crypto/x509"
9 years ago
"encoding/json"
"fmt"
9 years ago
"io/ioutil"
"net/http"
"strings"
"github.com/Bowery/prompt"
"github.com/mkideal/cli"
)
const maxRedirect = 21
9 years ago
type argT struct {
cli.Helper
Protocol string `cli:"s,scheme" usage:"protocol scheme (http or https)" dft:"http"`
9 years ago
Host string `cli:"H,host" usage:"rqlited host address" dft:"127.0.0.1"`
Port uint16 `cli:"p,port" usage:"rqlited host port" dft:"4001"`
Prefix string `cli:"P,prefix" usage:"rqlited HTTP URL prefix" dft:"/"`
Insecure bool `cli:"i,insecure" usage:"do not verify rqlited HTTPS certificate" dft:"false"`
CACert string `cli:"c,ca-cert" usage:"path to trusted X.509 root CA certificate"`
9 years ago
}
7 years ago
const cliHelp = `.help Show this message
.indexes Show names of all indexes
.schema Show CREATE statements for all tables
.status Show status and diagnostic information for connected node
7 years ago
.expvar Show expvar (Go runtime) information for connected node
7 years ago
.tables List names of tables
`
func main() {
9 years ago
cli.SetUsageStyle(cli.ManualStyle)
cli.Run(new(argT), func(ctx *cli.Context) error {
argv := ctx.Argv().(*argT)
if argv.Help {
ctx.WriteUsage()
return nil
}
prefix := fmt.Sprintf("%s:%d>", argv.Host, argv.Port)
9 years ago
FOR_READ:
for {
line, err := prompt.Basic(prefix, false)
if err != nil {
return err
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
var (
index = strings.Index(line, " ")
cmd = line
)
if index >= 0 {
cmd = line[:index]
}
cmd = strings.ToUpper(cmd)
switch cmd {
case ".TABLES":
err = query(ctx, cmd, `SELECT name FROM sqlite_master WHERE type="table"`, argv)
case ".INDEXES":
err = query(ctx, cmd, `SELECT sql FROM sqlite_master WHERE type="index"`, argv)
case ".SCHEMA":
err = query(ctx, cmd, "SELECT sql FROM sqlite_master", argv)
case ".STATUS":
err = status(ctx, cmd, line, argv)
case ".EXPVAR":
err = expvar(ctx, cmd, line, argv)
7 years ago
case ".HELP":
err = help(ctx, cmd, line, argv)
case ".QUIT", "QUIT", "EXIT":
9 years ago
break FOR_READ
case "SELECT":
err = query(ctx, cmd, line, argv)
default:
err = execute(ctx, cmd, line, argv)
}
if err != nil {
ctx.String("%s %v\n", ctx.Color().Red("ERR!"), err)
}
}
ctx.String("bye~\n")
return nil
})
}
func makeJSONBody(line string) string {
data, err := json.Marshal([]string{line})
9 years ago
if err != nil {
return ""
}
return string(data)
}
7 years ago
func help(ctx *cli.Context, cmd, line string, argv *argT) error {
fmt.Printf(cliHelp)
return nil
}
func status(ctx *cli.Context, cmd, line string, argv *argT) error {
url := fmt.Sprintf("%s://%s:%d/status", argv.Protocol, argv.Host, argv.Port)
return cliJSON(ctx, cmd, line, url, argv)
}
func expvar(ctx *cli.Context, cmd, line string, argv *argT) error {
url := fmt.Sprintf("%s://%s:%d/debug/vars", argv.Protocol, argv.Host, argv.Port)
return cliJSON(ctx, cmd, line, url, argv)
}
func sendRequest(ctx *cli.Context, urlStr string, line string, argv *argT, ret interface{}) error {
data := makeJSONBody(line)
url := urlStr
var rootCAs *x509.CertPool
if argv.CACert != "" {
pemCerts, err := ioutil.ReadFile(argv.CACert)
if err != nil {
return err
}
rootCAs = x509.NewCertPool()
ok := rootCAs.AppendCertsFromPEM(pemCerts)
if !ok {
return fmt.Errorf("failed to parse root CA certificate(s)")
}
}
client := http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: argv.Insecure, RootCAs: rootCAs},
}}
// Explicitly handle redirects.
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
nRedirect := 0
for {
resp, err := client.Post(url, "application/json", strings.NewReader(data))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
return fmt.Errorf("unauthorized")
}
// Check for redirect.
if resp.StatusCode == http.StatusMovedPermanently {
nRedirect++
if nRedirect > maxRedirect {
return fmt.Errorf("maximum leader redirect limit exceeded")
}
url = resp.Header["Location"][0]
continue
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return json.Unmarshal(body, ret)
}
}
// cliJSON fetches JSON from a URL, and displays it at the CLI.
func cliJSON(ctx *cli.Context, cmd, line, url string, argv *argT) error {
// Recursive JSON printer.
var pprint func(indent int, m map[string]interface{})
pprint = func(indent int, m map[string]interface{}) {
indentation := " "
for k, v := range m {
if v == nil {
continue
}
switch v.(type) {
case map[string]interface{}:
for i := 0; i < indent; i++ {
fmt.Print(indentation)
}
fmt.Printf("%s:\n", k)
pprint(indent+1, v.(map[string]interface{}))
default:
for i := 0; i < indent; i++ {
fmt.Print(indentation)
}
fmt.Printf("%s: %v\n", k, v)
}
}
}
client := http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: argv.Insecure},
}}
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
return fmt.Errorf("unauthorized")
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
ret := make(map[string]interface{})
if err := json.Unmarshal(body, &ret); err != nil {
return err
}
// Specific key requested?
parts := strings.Split(line, " ")
if len(parts) >= 2 {
ret = map[string]interface{}{parts[1]: ret[parts[1]]}
}
pprint(0, ret)
return nil
}