rdio-scanner/server/command.go
2022-09-09 11:52:20 -04:00

471 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (C) 2019-2022 Chrystian Huot <chrystian.huot@saubeo.solutions>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
)
const (
COMMAND_ARG = "cmd"
COMMAND_ARG_CODE = "+code"
COMMAND_ARG_EXPIRATION = "+expiration"
COMMAND_ARG_IDENT = "+ident"
COMMAND_ARG_IN = "+in"
COMMAND_ARG_LIMIT = "+limit"
COMMAND_ARG_OUT = "+out"
COMMAND_ARG_PASSWORD = "+password"
COMMAND_ARG_SYSTEMS = "+systems"
COMMAND_ARG_TOKEN = "+token"
COMMAND_ARG_URL = "+url"
COMMAND_ADMIN_PASSWORD = "admin-password"
COMMAND_CONFIG_GET = "config-get"
COMMAND_CONFIG_SET = "config-set"
COMMAND_HELP = "help"
COMMAND_LOGIN = "login"
COMMAND_LOGOUT = "logout"
COMMAND_USER_ADD = "user-add"
COMMAND_USER_REMOVE = "user-remove"
COMMAND_DEF_PASSWORD = "rdio-scanner"
COMMAND_DEF_URL = "http://localhost:3000/"
)
type Command struct {
app string
code string
command string
expiration string
ident string
in string
limit string
out string
password string
systems string
token string
tokenFile string
url string
}
func NewCommand(baseDir string) *Command {
app, _ := os.Executable()
pass := os.Getenv("RDIO_ADMIN_PASSWORD")
if pass == "" {
pass = COMMAND_DEF_PASSWORD
}
return &Command{
app: filepath.Base(app),
command: COMMAND_HELP,
password: pass,
tokenFile: baseDir + filepath.Base(app) + ".token",
url: COMMAND_DEF_URL,
}
}
func (command *Command) Do(action string) {
var err error
i := 1
readVal := func() string {
i++
if i < len(os.Args) {
return os.Args[i]
}
return ""
}
for i < len(os.Args) {
switch os.Args[i] {
case COMMAND_ARG_CODE:
command.code = readVal()
case COMMAND_ARG_EXPIRATION:
command.expiration = readVal()
case COMMAND_ARG_IDENT:
command.ident = readVal()
case COMMAND_ARG_IN:
command.in = readVal()
case COMMAND_ARG_LIMIT:
command.limit = readVal()
case COMMAND_ARG_OUT:
command.out = readVal()
if !strings.HasSuffix(strings.ToLower(command.out), ".json") {
command.out = command.out + ".json"
}
case COMMAND_ARG_PASSWORD:
command.password = readVal()
case COMMAND_ARG_SYSTEMS:
command.systems = readVal()
case COMMAND_ARG_TOKEN:
command.tokenFile = readVal()
case COMMAND_ARG_URL:
command.url = readVal()
if err != nil || !regexp.MustCompile(`^https?://`).Match([]byte(command.url)) {
command.exitWithError(errors.New("invalid URL"))
}
}
i++
}
if _, err := os.Stat(command.tokenFile); !os.IsNotExist(err) {
if b, err := os.ReadFile(command.tokenFile); err == nil {
command.token = string(b)
} else {
command.exitWithError(err)
}
}
switch action {
case COMMAND_CONFIG_GET:
command.configGet()
case COMMAND_CONFIG_SET:
command.configSet()
case COMMAND_LOGIN:
command.login()
case COMMAND_LOGOUT:
command.logout()
case COMMAND_ADMIN_PASSWORD:
command.adminPassword()
case COMMAND_USER_ADD:
command.userAdd()
case COMMAND_USER_REMOVE:
command.userRemove()
default:
command.printUsage()
}
os.Exit(0)
}
func (command *Command) printUsage() {
var prompt string
switch runtime.GOOS {
case "windows":
prompt = "C:\\>"
default:
prompt = "$ ./"
}
fmt.Printf("\nAvailable Commands:\n\n")
fmt.Printf(" %-11s Change administrator password.\n\n", COMMAND_ADMIN_PASSWORD)
fmt.Printf(" %-11s %s%s -%s %s %s <password>\n\n", "", prompt, command.app, COMMAND_ARG, COMMAND_ADMIN_PASSWORD, COMMAND_ARG_PASSWORD)
fmt.Printf(" %-11s Retrieve server's configuration.\n\n", COMMAND_CONFIG_GET)
fmt.Printf(" %-11s %s%s -%s %s %s <file.json>\n\n", "", prompt, command.app, COMMAND_ARG, COMMAND_CONFIG_GET, COMMAND_ARG_OUT)
fmt.Printf(" %-11s Set server's configuration.\n\n", COMMAND_CONFIG_SET)
fmt.Printf(" %-11s %s%s -%s %s %s <file.json>\n\n", "", prompt, command.app, COMMAND_ARG, COMMAND_CONFIG_SET, COMMAND_ARG_IN)
fmt.Printf(" %-11s Login to server.\n\n", COMMAND_LOGIN)
if runtime.GOOS != "windows" {
fmt.Printf(" %-11s $ RDIO_ADMIN_PASSWORD=<password> ./%s -%s %s\n", "", command.app, COMMAND_ARG, COMMAND_LOGIN)
}
fmt.Printf(" %-11s %s%s -%s %s %s <password>\n\n", "", prompt, command.app, COMMAND_ARG, COMMAND_LOGIN, COMMAND_ARG_PASSWORD)
fmt.Printf(" %-11s Logout from server.\n\n", COMMAND_LOGOUT)
fmt.Printf(" %-11s %s%s -%s %s\n\n", "", prompt, command.app, COMMAND_ARG, COMMAND_LOGOUT)
fmt.Printf(" %-11s Add a user access.\n\n", COMMAND_USER_ADD)
fmt.Printf(" %-11s %s%s -%s %s %s <ident> %s <code>\n\n", "", prompt, command.app, COMMAND_ARG, COMMAND_USER_ADD, COMMAND_ARG_IDENT, COMMAND_ARG_CODE)
fmt.Printf(" %-11s Optional:\n\n", "")
fmt.Printf(" %-11s %-11s <RFC3339 format> User access expiration date.\n", "", COMMAND_ARG_EXPIRATION)
fmt.Printf(" %-11s %-11s <limit> Concurrent user access limit.\n", "", COMMAND_ARG_LIMIT)
fmt.Printf(" %-11s %-11s <sysid1[,sysid2,...]> Specific system access.\n\n", "", COMMAND_ARG_SYSTEMS)
fmt.Printf(" %-11s Remove a user access.\n\n", COMMAND_USER_REMOVE)
fmt.Printf(" %-11s %s%s -%s %s %s <ident>\n\n", "", prompt, command.app, COMMAND_ARG, COMMAND_USER_REMOVE, COMMAND_ARG_IDENT)
fmt.Printf("Global Options:\n\n")
fmt.Printf(" %-11s Session token keystore. Default is `.%s.token`.\n", COMMAND_ARG_TOKEN, command.app)
fmt.Printf(" %-11s Server remote address. Default is `%s`.\n\n", COMMAND_ARG_URL, COMMAND_DEF_URL)
}
func (command *Command) adminPassword() {
if command.password == "" {
command.exitWithError(fmt.Sprintf("Missing %s <password> arguments.", COMMAND_ARG_PASSWORD))
}
if body, err := command.writeBody(map[string]any{"newPassword": command.password}); err == nil {
if res, err := command.submit(http.MethodPost, "/api/admin/password", body, true); err == nil {
if res.StatusCode == http.StatusOK {
fmt.Println("New admin password applied")
} else {
command.exitWithError(errors.New(res.Status))
}
}
} else {
command.exitWithError(err)
}
}
func (command *Command) configGet() {
if command.out == "" {
command.exitWithError(fmt.Sprintf("Missing %s <file.json> arguments.", COMMAND_ARG_OUT))
}
if res, err := command.submit(http.MethodGet, "/api/admin/config", nil, true); err == nil {
if res.StatusCode == http.StatusOK {
if data, err := command.readBody(res.Body); err == nil {
switch v := data.(type) {
case map[string]any:
switch v := v["config"].(type) {
case map[string]any:
if f, err := os.Create(command.out); err == nil {
j := json.NewEncoder(f)
j.SetIndent("", " ")
j.Encode(v)
fmt.Printf("Server's configuration saved to %s.\n", command.out)
} else {
command.exitWithError(err)
}
}
default:
command.exitWithError(errors.New("invalid response"))
}
} else {
command.exitWithError(err)
}
} else {
command.exitWithError(errors.New(res.Status))
}
}
}
func (command *Command) configSet() {
if command.in == "" {
command.exitWithError(fmt.Sprintf("Missing %s <file.json> arguments.", COMMAND_ARG_IN))
}
if f, err := os.Open(command.in); err == nil {
var d any
json.NewDecoder(f).Decode(&d)
switch v := d.(type) {
case map[string]any:
if body, err := command.writeBody(v); err == nil {
if res, err := command.submit(http.MethodPut, "/api/admin/config", body, true); err == nil {
if res.StatusCode == http.StatusOK {
fmt.Println("Server's configuration applied.")
} else {
command.exitWithError(errors.New(res.Status))
}
}
} else {
command.exitWithError(err)
}
default:
command.exitWithError("Invalid Data")
}
} else {
command.exitWithError(err)
}
}
func (command *Command) login() {
if body, err := command.writeBody(map[string]any{"password": command.password}); err == nil {
if res, err := command.submit(http.MethodPost, "/api/admin/login", body, false); err == nil {
if res.StatusCode == http.StatusOK {
if data, err := command.readBody(res.Body); err == nil {
switch v := data.(type) {
case map[string]any:
switch v := v["token"].(type) {
case string:
command.token = v
if err := os.WriteFile(command.tokenFile, []byte(command.token), 0600); err != nil {
command.exitWithError(err)
}
fmt.Println("Logged in.")
default:
command.exitWithError(errors.New("no token in response"))
}
default:
command.exitWithError(errors.New("invalid response"))
}
} else {
command.exitWithError(err)
}
} else {
command.exitWithError(res.Status)
}
} else {
command.exitWithError(err)
}
} else {
command.exitWithError(err)
}
}
func (command *Command) logout() {
if res, err := command.submit(http.MethodPost, "/api/admin/logout", nil, true); err == nil {
switch res.StatusCode {
case http.StatusOK, http.StatusUnauthorized:
command.token = ""
if _, err := os.Stat(command.tokenFile); !os.IsNotExist(err) {
if err := os.Remove(command.tokenFile); err != nil {
command.exitWithError(err)
}
}
fmt.Println("Logged out.")
default:
command.exitWithError(res.Status)
}
} else {
command.exitWithError(err)
}
}
func (command *Command) userAdd() {
if command.ident == "" {
command.exitWithError(fmt.Sprintf("Missing %s <ident> arguments.", COMMAND_ARG_IDENT))
}
if command.code == "" {
command.exitWithError(fmt.Sprintf("Missing %s <code> arguments.", COMMAND_ARG_CODE))
}
u := map[string]any{
"code": command.code,
"ident": command.ident,
"systems": "*",
}
if command.expiration != "" {
if t, err := time.Parse(time.RFC3339, command.expiration); err == nil {
u["expiration"] = t.UTC().Format(time.RFC3339)
} else {
command.exitWithError(fmt.Sprintf("Invalid date format for %s", COMMAND_ARG_EXPIRATION))
}
}
if command.limit != "" {
if i, err := strconv.Atoi(command.limit); err == nil {
u["limit"] = i
} else {
command.exitWithError(fmt.Sprintf("Invalid number for %s", COMMAND_ARG_LIMIT))
}
}
if command.systems != "" {
s := []int{}
for _, v := range strings.Split(command.systems, ",") {
if i, err := strconv.Atoi(v); err == nil {
s = append(s, i)
} else {
command.exitWithError(fmt.Sprintf("The value '%s' is invalid for %s", v, COMMAND_ARG_SYSTEMS))
}
}
if len(s) > 0 {
systems := []map[string]any{}
for _, i := range s {
systems = append(systems, map[string]any{"id": i, "talkgroups": "*"})
}
u["systems"] = systems
} else {
command.exitWithError(fmt.Sprintf("Invalid system ids list for %s", COMMAND_ARG_SYSTEMS))
}
}
if body, err := command.writeBody(u); err == nil {
if res, err := command.submit(http.MethodPost, "/api/admin/user-add", body, true); err == nil {
if res.StatusCode == http.StatusOK {
fmt.Printf("User %s added.\n", command.ident)
} else {
command.exitWithError(errors.New(res.Status))
}
}
} else {
command.exitWithError(err)
}
}
func (command *Command) userRemove() {
if command.ident == "" {
command.exitWithError(fmt.Sprintf("Missing %s <ident> arguments.", COMMAND_ARG_IDENT))
}
if body, err := command.writeBody(map[string]any{"ident": command.ident}); err == nil {
if res, err := command.submit(http.MethodPost, "/api/admin/user-remove", body, true); err == nil {
if res.StatusCode == http.StatusOK {
fmt.Printf("User %s removed.\n", command.ident)
} else {
command.exitWithError(errors.New(res.Status))
}
}
} else {
command.exitWithError(err)
}
}
func (c *Command) readBody(body io.ReadCloser) (data any, err error) {
err = json.NewDecoder(body).Decode(&data)
return data, err
}
func (c *Command) writeBody(data any) (body io.Reader, err error) {
var b []byte
if b, err = json.Marshal(data); err == nil {
body = bytes.NewReader(b)
}
return body, err
}
func (c *Command) submit(method string, url string, body io.Reader, auth bool) (res *http.Response, err error) {
var req *http.Request
u := strings.TrimSuffix(c.url, "/") + url
if req, err = http.NewRequest(method, u, body); err == nil {
if auth {
if c.token != "" {
req.Header.Add("Authorization", c.token)
} else {
c.exitWithError("Not logged in.")
}
}
if body != nil {
req.Header.Add("Content-Type", "application/json")
}
res, err = http.DefaultClient.Do(req)
}
return res, err
}
func (c *Command) exitWithError(err any) {
fmt.Printf("%v\n", err)
os.Exit(1)
}