/
connection.go
228 lines (177 loc) · 5.5 KB
/
connection.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// Package connection implements the most basic parts of an IRC connection
package connection
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"sync"
"time"
"awesome-dragon.science/go/irc/isupport"
"awesome-dragon.science/go/irc/numerics"
"github.com/ergochat/irc-go/ircmsg"
"github.com/op/go-logging"
)
var log = logging.MustGetLogger("irc") //nolint:gochecknoglobals // Its the logger.
// Config contains all the configuration options used by Server
type Config struct {
Host string // Hostname of the target server
Port string // Server port
TLS bool // Use TLS
InsecureSkipVerifyTLS bool // Skip verifying TLS Certificates
TLSCertPath string
TLSKeyPath string
RawLog bool // Log raw messages
}
// Connection implements the barebones required to make a connection to an IRC server.
//
// It expects that you do EVERYTHING yourself. It simply is a nice frontend for the socket.
type Connection struct {
config *Config
conn net.Conn
connectionCtx context.Context // nolint:containedctx // Used to hold onto tne entire connection
cancelConnCtx context.CancelFunc
lineChan chan *ircmsg.Message // Incoming lines
writeMutex sync.Mutex // Protects the write socket
ISupport *isupport.ISupport
}
// NewConnection creates a new Server instance ready for use
func NewConnection(config *Config) *Connection {
return &Connection{
config: config,
lineChan: make(chan *ircmsg.Message),
ISupport: isupport.New(),
}
}
// NewSimpleServer is a nice wrapper that creates a ServerConfig for you
func NewSimpleServer(host, port string, useTLS bool) *Connection {
return NewConnection(&Config{Host: host, Port: port, TLS: useTLS, RawLog: true})
}
// Connect connects the Server instance to IRC. It does NOT block.
func (s *Connection) Connect(ctx context.Context) error {
connContext, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
conn, err := s.openConn(connContext)
if err != nil {
return fmt.Errorf("could not open connection: %w", err)
}
s.conn = conn
mainCtx, mainCancel := context.WithCancel(ctx)
s.connectionCtx = mainCtx
s.cancelConnCtx = mainCancel
// These are being used as signals
readCtx, readCancel := context.WithCancel(mainCtx)
_ = readCancel
go s.readLoop(readCtx)
return nil
}
func (s *Connection) openConn(ctx context.Context) (net.Conn, error) {
var (
conn net.Conn
err error
hostPort = net.JoinHostPort(s.config.Host, s.config.Port)
)
log.Debugf("Opening connection to %q...", hostPort)
if s.config.TLS {
dialer := &tls.Dialer{}
dialer.Config = &tls.Config{InsecureSkipVerify: s.config.InsecureSkipVerifyTLS} //nolint:gosec // Its intentional
if s.config.TLSCertPath != "" && s.config.TLSKeyPath != "" {
res, err := tls.LoadX509KeyPair(s.config.TLSCertPath, s.config.TLSKeyPath)
if err != nil {
return nil, fmt.Errorf("could not load keypair: %w", err)
}
dialer.Config.Certificates = append(dialer.Config.Certificates, res)
}
conn, err = dialer.DialContext(ctx, "tcp", hostPort)
} else {
conn, err = (&net.Dialer{}).DialContext(ctx, "tcp", net.JoinHostPort(s.config.Host, s.config.Port))
}
if err != nil {
return nil, fmt.Errorf("could not dial: %w", err)
}
return conn, nil
}
func (s *Connection) readLoop(ctx context.Context) {
reader := bufio.NewReader(s.conn)
outer:
for {
select {
case <-ctx.Done():
break outer
default:
}
data, err := reader.ReadString('\n')
if err != nil {
if errors.Is(err, io.EOF) {
break
}
log.Warningf("Unexpected error from conn.Read: %s", err)
break
}
msg, err := ircmsg.ParseLine(data)
if err != nil {
log.Warningf("got an invalid IRC Line: %q -> %s", data, err)
continue
}
if s.config.RawLog {
log.Infof("[>>] %s", data)
}
s.onLine(&msg)
}
close(s.lineChan)
s.cancelConnCtx()
}
func (s *Connection) onLine(msg *ircmsg.Message) {
switch msg.Command {
case numerics.RPL_ISUPPORT:
s.ISupport.Parse(msg)
case numerics.RPL_MYINFO:
}
s.lineChan <- msg
}
func (s *Connection) Write(b []byte) (int, error) {
s.writeMutex.Lock()
defer s.writeMutex.Unlock()
if s.config.RawLog {
log.Infof("[<<] %s", b)
}
n, err := s.conn.Write(b)
if err != nil {
return n, fmt.Errorf("Connection.Write: %w", err)
}
return n, nil
}
// WriteLine constructs an ircmsg.Message and sends it to the server
func (s *Connection) WriteLine(command string, args ...string) error {
msg := ircmsg.MakeMessage(nil, "", command, args...)
bytes, err := msg.LineBytes()
if err != nil {
return fmt.Errorf("could not create IRC line: %w", err)
}
_, err = s.Write(bytes)
if err != nil {
return fmt.Errorf("WriteLine: Could not send line: %w", err)
}
return nil
}
// WriteString implements io.StringWriter
func (s *Connection) WriteString(m string) (int, error) { return s.Write([]byte(m)) } //nolint:gocritic // ... No
// LineChan returns a read only channel that will have messages from the server
// sent to it
func (s *Connection) LineChan() <-chan *ircmsg.Message { return s.lineChan }
// Done returns a channel that is closed when the connection is closed.
func (s *Connection) Done() <-chan struct{} { return s.connectionCtx.Done() }
// Stop stops the connection to IRC
func (s *Connection) Stop(msg string) {
if err := s.WriteLine("QUIT", msg); err != nil {
log.Infof("Failed to write quit while exiting: %s", err)
}
select {
case <-time.After(time.Second * 2):
s.cancelConnCtx()
case <-s.connectionCtx.Done():
}
}