487 lines
12 KiB
Go
487 lines
12 KiB
Go
// # CLI Access
|
|
//
|
|
// The CLI Access section implements the core transport and session management for
|
|
// CommuniGate Pro. It handles the low-level communication protocol, ensuring
|
|
// reliable and secure data exchange between the Go application and the CommuniGate Pro CLI.
|
|
//
|
|
// Key capabilities include:
|
|
// - Session Lifecycle: automatic connection establishment, authentication,
|
|
// and graceful termination via [Cli.Close].
|
|
// - Persistence: transparent re-connection logic if the session exceeds
|
|
// the server's idle timeout (approx. 5 minutes).
|
|
// - Security: support for standard [Plain] login, MD5-based [APOP], and [WebUser]
|
|
// authentication, with optional TLS (STARTTLS) encryption.
|
|
// - Efficiency: mandatory use of CommuniGate Pro CLI "INLINE" mode, which, combined with
|
|
// the library's custom parser, ensures high-performance data processing.
|
|
package cgpcli
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/md5"
|
|
"crypto/tls"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// Secure defines the authentication method used for the CommuniGate Pro CLI session.
|
|
type Secure int
|
|
|
|
const (
|
|
// Plain represents standard USER/PASS authentication.
|
|
Plain Secure = iota
|
|
|
|
// APOP represents Authenticated Post Office Protocol (MD5-based) security.
|
|
APOP
|
|
|
|
// WebUser represents authentication for web-specific users.
|
|
WebUser
|
|
)
|
|
|
|
var (
|
|
cmdSTLS = []byte("STLS\n")
|
|
cmdQUIT = []byte("QUIT\n")
|
|
cmdINLINE = []byte("INLINE\n")
|
|
)
|
|
|
|
const (
|
|
cgpLineSize = 129 * 1024
|
|
cgpTimeout = 5*time.Minute - 5*time.Second
|
|
cmdTimeout = 1 * time.Minute
|
|
connTimeout = 7 * time.Second
|
|
)
|
|
|
|
// CliCode represents a numeric response status from the CommuniGate Pro server.
|
|
type CliCode int
|
|
|
|
const (
|
|
CodeOk CliCode = 200
|
|
CodeOkInline = 201
|
|
CodePassword = 300 // Server expects a password
|
|
CodeGenErr = 500
|
|
CodeGenErr1 = 501
|
|
CodeGenErr12 = 512
|
|
CodeGenErr25 = 525
|
|
CodeGenErr99 = 599
|
|
CodeStrange = 10000
|
|
)
|
|
|
|
const defaultPort = "106"
|
|
|
|
// Cli represents a CommuniGate Pro CLI client session.
|
|
// It handles connection persistence, automatic reconnection, and command marshaling.
|
|
// Use New() to initialize a session.
|
|
type Cli struct {
|
|
host string
|
|
login string
|
|
password string
|
|
secure Secure
|
|
tls bool
|
|
debug bool
|
|
conn net.Conn
|
|
reader *bufio.Reader
|
|
writer *bufio.Writer
|
|
buf bytes.Buffer
|
|
isConnected bool
|
|
lastAccess time.Time
|
|
respCode CliCode
|
|
respData []byte
|
|
}
|
|
|
|
// Close gracefully terminates the CommuniGate Pro CLI session.
|
|
//
|
|
// Returns:
|
|
// - error: an error if the command fails.
|
|
func (cli *Cli) Close() (err error) {
|
|
if cli.conn == nil {
|
|
return nil
|
|
}
|
|
|
|
cli.writer.Write(cmdQUIT)
|
|
if err = cli.writer.Flush(); err != nil {
|
|
cli.close()
|
|
return
|
|
}
|
|
|
|
_ = cli.parseResponse()
|
|
|
|
cli.close()
|
|
return
|
|
}
|
|
|
|
// GetRespCode returns the numeric status code from the last server response.
|
|
//
|
|
// Returns:
|
|
// - CliCode: a numeric status code (e.g., 200 for OK, 201 for OK INLINE).
|
|
func (cli *Cli) GetRespCode() CliCode {
|
|
return cli.respCode
|
|
}
|
|
|
|
// GetRespData returns the raw string data (payload) from the last server response.
|
|
//
|
|
// Returns:
|
|
// - string: the data portion of the CLI response, following the status code.
|
|
func (cli *Cli) GetRespData() string {
|
|
return string(cli.respData)
|
|
}
|
|
|
|
// IsSuccess returns true if the last response code indicates success (200 or 201).
|
|
func (cli *Cli) IsSuccess() bool {
|
|
return (cli.respCode == CodeOk || cli.respCode == CodeOkInline)
|
|
}
|
|
|
|
// Logout gracefully terminates the CommuniGate Pro CLI session.
|
|
// It is an alias for [Cli.Close].
|
|
//
|
|
// Returns:
|
|
// - error: an error if the command fails.
|
|
func (cli *Cli) Logout() error {
|
|
return cli.Close()
|
|
}
|
|
|
|
// New creates and initializes a new CommuniGate Pro CLI session.
|
|
// It establishes a connection, performs authentication, and sets the session
|
|
// to INLINE mode for efficient data exchange.
|
|
//
|
|
// Parameters:
|
|
// - host: the server hostname or IP address.
|
|
// - login: the administrative or user login name.
|
|
// - password: the password for authentication.
|
|
// - secure: the security mode to use (Plain, APOP, or WebUser).
|
|
// - tls: if true, attempts to upgrade the connection via STARTTLS.
|
|
//
|
|
// Returns:
|
|
// - *Cli: a pointer to the initialized session.
|
|
// - error: an error if the command fails.
|
|
func New(host, login, password string, secure Secure, tls bool) (*Cli, error) {
|
|
|
|
cli := new(Cli)
|
|
|
|
cli.host = net.JoinHostPort(host, defaultPort)
|
|
cli.login = login
|
|
cli.password = password
|
|
cli.secure = secure
|
|
cli.tls = tls
|
|
|
|
err := cli.connect()
|
|
|
|
return cli, err
|
|
}
|
|
|
|
// Query sends a command to CommuniGate Pro CLI and unmarshals the response into a Go type.
|
|
// It automatically handles connection health and session state.
|
|
//
|
|
// Parameters:
|
|
// - cmd: the CLI command string (e.g., "GETACCOUNTSETTINGS").
|
|
// - args: variadic arguments that will be marshaled into the CLI command format.
|
|
//
|
|
// Returns:
|
|
// - any: the unmarshaled result from the server.
|
|
// - error: an error if the command fails.
|
|
func (cli *Cli) Query(cmd string, args ...any) (res any, err error) {
|
|
if err = cli.Send(cmd, args...); err != nil {
|
|
return
|
|
}
|
|
if err = cli.parseResponse(); err != nil {
|
|
return
|
|
}
|
|
if !cli.IsSuccess() {
|
|
err = fmt.Errorf("CGP Error %d: %s", cli.respCode, string(cli.respData))
|
|
return
|
|
}
|
|
|
|
err = Unmarshal(cli.respData, &res)
|
|
return
|
|
}
|
|
|
|
// QueryNV (No Value) sends a command where only the success status code is needed.
|
|
// It is useful for operations like updates where the server doesn't return data.
|
|
//
|
|
// Parameters:
|
|
// - cmd: the CLI command string.
|
|
// - args: variadic arguments for the command.
|
|
//
|
|
// Returns:
|
|
// - error: an error if the command fails.
|
|
func (cli *Cli) QueryNV(cmd string, args ...any) (err error) {
|
|
if err = cli.Send(cmd, args...); err != nil {
|
|
return
|
|
}
|
|
if err = cli.parseResponse(); err != nil {
|
|
return
|
|
}
|
|
if !cli.IsSuccess() {
|
|
err = fmt.Errorf("CGP Error %d: %s", cli.respCode, string(cli.respData))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Send marshals and sends a raw command to the CommuniGate Pro CLI.
|
|
// It handles automatic reconnection if the session has timed out.
|
|
//
|
|
// Parameters:
|
|
// - cmd: the CLI command string.
|
|
// - args: variadic arguments to be appended to the command.
|
|
//
|
|
// Returns:
|
|
// - error: an error if the command fails.
|
|
func (cli *Cli) Send(cmd string, args ...any) (err error) {
|
|
if time.Since(cli.lastAccess) > cgpTimeout || cli.conn == nil {
|
|
cli.close()
|
|
if err = cli.connect(); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
cli.lastAccess = time.Now()
|
|
cli.conn.SetWriteDeadline(time.Now().Add(cmdTimeout))
|
|
cli.buf.Reset()
|
|
|
|
cli.buf.WriteString(cmd)
|
|
|
|
for _, arg := range args {
|
|
cli.buf.WriteByte(' ')
|
|
if err = marshal(&cli.buf, arg, ""); err != nil {
|
|
return
|
|
}
|
|
}
|
|
cli.buf.WriteByte('\n')
|
|
|
|
if cli.debug {
|
|
os.Stderr.Write([]byte(">>> SEND: "))
|
|
os.Stderr.Write(cli.buf.Bytes())
|
|
}
|
|
|
|
_, err = cli.writer.Write(cli.buf.Bytes())
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return cli.writer.Flush()
|
|
}
|
|
|
|
// SetDebug enables or disables raw protocol logging to os.Stderr.
|
|
//
|
|
// Parameters:
|
|
// - debug: if true, all sent (>>>) and received (<<<) raw data will
|
|
// be printed to the standard error stream.
|
|
func (cli *Cli) SetDebug(debug bool) {
|
|
cli.debug = debug
|
|
}
|
|
|
|
func (cli *Cli) close() {
|
|
if cli.conn != nil {
|
|
cli.conn.Close()
|
|
cli.conn = nil
|
|
}
|
|
}
|
|
|
|
func (cli *Cli) connect() (err error) {
|
|
|
|
// 1. Устанавливаем базовое TCP соединение
|
|
cli.conn, err = net.DialTimeout("tcp", cli.host, connTimeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
cli.reader = bufio.NewReaderSize(cli.conn, cgpLineSize)
|
|
cli.writer = bufio.NewWriter(cli.conn)
|
|
cli.buf.Grow(cgpLineSize)
|
|
|
|
err = cli.parseResponse()
|
|
if err != nil {
|
|
err = fmt.Errorf("can't read CGPro Server prompt: %w", err)
|
|
cli.close()
|
|
return
|
|
}
|
|
|
|
cli.conn.SetWriteDeadline(time.Now().Add(cmdTimeout))
|
|
|
|
// 2. Обработка STARTTLS (STLS)
|
|
if cli.tls {
|
|
cli.writer.Write(cmdSTLS)
|
|
if err = cli.writer.Flush(); err != nil {
|
|
cli.close()
|
|
return
|
|
}
|
|
err = cli.parseResponse()
|
|
if err != nil || cli.respCode != CodeOk {
|
|
if err == nil {
|
|
err = fmt.Errorf("STLS failed: %d %s", cli.respCode, string(cli.respData))
|
|
}
|
|
cli.close()
|
|
return
|
|
}
|
|
|
|
tlsconfig := &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
}
|
|
tlsConn := tls.Client(cli.conn, tlsconfig)
|
|
if err = tlsConn.Handshake(); err != nil {
|
|
cli.close()
|
|
return
|
|
}
|
|
|
|
cli.conn = net.Conn(tlsConn)
|
|
cli.reader.Reset(cli.conn)
|
|
cli.writer.Reset(cli.conn)
|
|
}
|
|
|
|
// 3. Аутентификация
|
|
switch cli.secure {
|
|
case Plain:
|
|
cli.writer.WriteString("USER ")
|
|
cli.writer.WriteString(cli.login)
|
|
cli.writer.WriteByte('\n')
|
|
if err = cli.writer.Flush(); err != nil {
|
|
cli.close()
|
|
return
|
|
}
|
|
err = cli.parseResponse()
|
|
if err != nil || cli.respCode != CodePassword {
|
|
if err == nil {
|
|
err = fmt.Errorf("USER command failed: %d %s", cli.respCode, string(cli.respData))
|
|
}
|
|
cli.close()
|
|
return
|
|
}
|
|
|
|
cli.writer.WriteString("PASS ")
|
|
cli.writer.WriteString(cli.password)
|
|
cli.writer.WriteByte('\n')
|
|
if err = cli.writer.Flush(); err != nil {
|
|
cli.close()
|
|
return
|
|
}
|
|
err = cli.parseResponse()
|
|
if err != nil {
|
|
cli.close()
|
|
return
|
|
}
|
|
|
|
case APOP:
|
|
idStart := bytes.LastIndexByte(cli.respData, '<')
|
|
if idStart == -1 {
|
|
cli.close()
|
|
err = fmt.Errorf("APOP ID not found in server prompt")
|
|
return
|
|
}
|
|
id := cli.respData[idStart:]
|
|
hash := md5.Sum(append(id, cli.password...))
|
|
digest := hex.EncodeToString(hash[:])
|
|
|
|
cli.writer.WriteString("APOP ")
|
|
cli.writer.WriteString(cli.login)
|
|
cli.writer.WriteByte(' ')
|
|
cli.writer.WriteString(digest)
|
|
cli.writer.WriteByte('\n')
|
|
if err = cli.writer.Flush(); err != nil {
|
|
cli.close()
|
|
return
|
|
}
|
|
err = cli.parseResponse()
|
|
if err != nil {
|
|
cli.close()
|
|
return
|
|
}
|
|
|
|
case WebUser:
|
|
cli.writer.WriteString("AUTH WEBUSER ")
|
|
cli.writer.WriteString(cli.login)
|
|
cli.writer.WriteByte(' ')
|
|
cli.writer.WriteString(cli.password)
|
|
cli.writer.WriteByte('\n')
|
|
if err = cli.writer.Flush(); err != nil {
|
|
cli.close()
|
|
return
|
|
}
|
|
err = cli.parseResponse()
|
|
if err != nil {
|
|
cli.close()
|
|
return
|
|
}
|
|
|
|
default:
|
|
cli.close()
|
|
err = fmt.Errorf("unknown secure type")
|
|
return
|
|
}
|
|
|
|
// Проверка результата входа
|
|
if cli.respCode != CodeOk {
|
|
err = fmt.Errorf("login failed: %d %s", cli.respCode, string(cli.respData))
|
|
cli.close()
|
|
return
|
|
}
|
|
|
|
// 4. Переход в режим INLINE
|
|
cli.writer.Write(cmdINLINE)
|
|
if err = cli.writer.Flush(); err != nil {
|
|
cli.close()
|
|
return
|
|
}
|
|
err = cli.parseResponse()
|
|
if err != nil || cli.respCode != CodeOk {
|
|
if err == nil {
|
|
err = fmt.Errorf("failed to set INLINE mode: %d %s", cli.respCode, string(cli.respData))
|
|
}
|
|
cli.close()
|
|
return
|
|
}
|
|
|
|
cli.isConnected = true
|
|
cli.lastAccess = time.Now()
|
|
return
|
|
}
|
|
|
|
func parseCode(b []byte) CliCode {
|
|
code := 0
|
|
for _, c := range b {
|
|
if c < '0' || c > '9' {
|
|
break
|
|
}
|
|
code = code*10 + int(c-'0')
|
|
}
|
|
return CliCode(code)
|
|
}
|
|
|
|
func (cli *Cli) parseResponse() (err error) {
|
|
cli.conn.SetReadDeadline(time.Now().Add(cmdTimeout))
|
|
|
|
line, err := cli.reader.ReadSlice('\n')
|
|
if err != nil {
|
|
return fmt.Errorf("can't read line: %s", err)
|
|
}
|
|
|
|
line = bytes.TrimRight(line, "\r\n")
|
|
|
|
if cli.debug {
|
|
os.Stderr.Write([]byte("<<< RECV: "))
|
|
os.Stderr.Write(line)
|
|
os.Stderr.Write([]byte("\n"))
|
|
}
|
|
|
|
idx := bytes.IndexByte(line, ' ')
|
|
if idx == -1 {
|
|
cli.respCode = parseCode(line)
|
|
cli.respData = nil
|
|
return nil
|
|
}
|
|
|
|
code := parseCode(line)
|
|
if code < CodeOk || code > CodeGenErr99 {
|
|
cli.respCode = CodeStrange
|
|
cli.respData = line
|
|
return fmt.Errorf("STRANGE CODE: %d : %s", code, line)
|
|
}
|
|
|
|
cli.respCode = CliCode(code)
|
|
cli.respData = line[idx+1:]
|
|
cli.lastAccess = time.Now()
|
|
|
|
return nil
|
|
}
|