master
go 169 lines 3.99 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package socket
4
5 import (
6 "bufio"
7 "context"
8 "crypto/tls"
9 "errors"
10 "fmt"
11 "net"
12 "time"
13 )
14
15 // Processor is a callback function passed to the Socket.Command method.
16 // It processes each response line received from the server.
17 type Processor func([]byte) (bool, error)
18
19 // Client defines an interface for socket clients, abstracting the underlying implementation.
20 // Implementations should provide connections for various socket types such as TCP, UDP, or Unix domain sockets.
21 type Client interface {
22 Connect() error
23 Disconnect() error
24 Command(command string, process Processor) error
25 }
26
27 // ConnectAndRead establishes a connection using the given configuration,
28 // executes the provided processor function on the incoming response lines,
29 // and ensures the connection is properly closed after use.
30 func ConnectAndRead(cfg Config, process Processor) error {
31 sock := New(cfg)
32
33 if err := sock.Connect(); err != nil {
34 return err
35 }
36
37 defer func() { _ = sock.Disconnect() }()
38
39 return sock.read(process)
40 }
41
42 // New creates and returns a new Socket instance configured with the provided settings.
43 // The socket supports multiple types (TCP, UDP, UNIX), addresses (IPv4, IPv6, domain names),
44 // and optional TLS encryption. Connections are reused where possible.
45 func New(cfg Config) *Socket {
46 return &Socket{Config: cfg}
47 }
48
49 // Socket is a concrete implementation of the Client interface, managing a network connection
50 // based on the specified configuration (address, type, timeout, and optional TLS settings).
51 type Socket struct {
52 Config
53 conn net.Conn
54 }
55
56 // Config encapsulates the settings required to establish a network connection.
57 type Config struct {
58 Address string
59 Timeout time.Duration
60 TLSConf *tls.Config
61 MaxReadLines int64
62 }
63
64 // Connect establishes a connection to the specified address using the configuration details.
65 func (s *Socket) Connect() error {
66 conn, err := s.dial()
67 if err != nil {
68 return fmt.Errorf("socket.Connect: %w", err)
69 }
70
71 s.conn = conn
72
73 return nil
74 }
75
76 // Disconnect terminates the active connection if one exists.
77 func (s *Socket) Disconnect() error {
78 if s.conn == nil {
79 return nil
80 }
81 err := s.conn.Close()
82 s.conn = nil
83 return err
84 }
85
86 // Command sends a command string to the connected server and processes its response line by line
87 // using the provided Processor function. This method respects the timeout configuration
88 // for write and read operations. If a timeout or processing error occurs, it stops and returns the error.
89 func (s *Socket) Command(command string, process Processor) error {
90 if s.conn == nil {
91 return errors.New("cannot send command on nil connection")
92 }
93
94 if err := s.write(command); err != nil {
95 return err
96 }
97
98 return s.read(process)
99 }
100
101 func (s *Socket) write(command string) error {
102 if s.conn == nil {
103 return errors.New("write: nil connection")
104 }
105
106 if err := s.conn.SetWriteDeadline(s.deadline()); err != nil {
107 return err
108 }
109
110 _, err := s.conn.Write([]byte(command))
111
112 return err
113 }
114
115 func (s *Socket) read(process Processor) error {
116 if process == nil {
117 return errors.New("read: process func is nil")
118 }
119 if s.conn == nil {
120 return errors.New("read: nil connection")
121 }
122
123 if err := s.conn.SetReadDeadline(s.deadline()); err != nil {
124 return err
125 }
126
127 sc := bufio.NewScanner(s.conn)
128
129 var n int64
130 limit := s.MaxReadLines
131
132 for sc.Scan() {
133 more, err := process(sc.Bytes())
134 if err != nil {
135 return err
136 }
137 if n++; limit > 0 && n > limit {
138 return fmt.Errorf("read line limit exceeded (%d", limit)
139 }
140 if !more {
141 break
142 }
143 }
144
145 return sc.Err()
146 }
147
148 func (s *Socket) dial() (net.Conn, error) {
149 network, address := parseAddress(s.Address)
150
151 var d net.Dialer
152 d.Timeout = s.timeout()
153
154 if s.TLSConf != nil {
155 return tls.DialWithDialer(&d, network, address, s.TLSConf)
156 }
157 return d.DialContext(context.Background(), network, address)
158 }
159
160 func (s *Socket) deadline() time.Time {
161 return time.Now().Add(s.timeout())
162 }
163
164 func (s *Socket) timeout() time.Duration {
165 if s.Timeout == 0 {
166 return time.Second
167 }
168 return s.Timeout
169 }