source: code/trunk/service.go@ 270

Last change on this file since 270 was 270, checked in by delthas, 5 years ago

Add support for the irc+insecure address scheme

Some servers do not support TLS, or have invalid, expired or self-signed
TLS certificates. While the right fix would be toi contact each server
owner to add support for valid TLS, supporting plaintext upstream
connections is sometimes necessary.

This adds support for the irc+insecure address scheme, which connects to
a network in plain-text over TCP.

File size: 7.2 KB
Line 
1package soju
2
3import (
4 "flag"
5 "fmt"
6 "io/ioutil"
7 "strings"
8
9 "github.com/google/shlex"
10 "golang.org/x/crypto/bcrypt"
11 "gopkg.in/irc.v3"
12)
13
14const serviceNick = "BouncerServ"
15
16var servicePrefix = &irc.Prefix{
17 Name: serviceNick,
18 User: serviceNick,
19 Host: serviceNick,
20}
21
22type serviceCommandSet map[string]*serviceCommand
23
24type serviceCommand struct {
25 usage string
26 desc string
27 handle func(dc *downstreamConn, params []string) error
28 children serviceCommandSet
29}
30
31func sendServiceNOTICE(dc *downstreamConn, text string) {
32 dc.SendMessage(&irc.Message{
33 Prefix: servicePrefix,
34 Command: "NOTICE",
35 Params: []string{dc.nick, text},
36 })
37}
38
39func sendServicePRIVMSG(dc *downstreamConn, text string) {
40 dc.SendMessage(&irc.Message{
41 Prefix: servicePrefix,
42 Command: "PRIVMSG",
43 Params: []string{dc.nick, text},
44 })
45}
46
47func handleServicePRIVMSG(dc *downstreamConn, text string) {
48 words, err := shlex.Split(text)
49 if err != nil {
50 sendServicePRIVMSG(dc, fmt.Sprintf("error: failed to parse command: %v", err))
51 return
52 }
53
54 cmd, params, err := serviceCommands.Get(words)
55 if err != nil {
56 sendServicePRIVMSG(dc, fmt.Sprintf(`error: %v (type "help" for a list of commands)`, err))
57 return
58 }
59
60 if err := cmd.handle(dc, params); err != nil {
61 sendServicePRIVMSG(dc, fmt.Sprintf("error: %v", err))
62 }
63}
64
65func (cmds serviceCommandSet) Get(params []string) (*serviceCommand, []string, error) {
66 if len(params) == 0 {
67 return nil, nil, fmt.Errorf("no command specified")
68 }
69
70 name := params[0]
71 params = params[1:]
72
73 cmd, ok := cmds[name]
74 if !ok {
75 for k := range cmds {
76 if !strings.HasPrefix(k, name) {
77 continue
78 }
79 if cmd != nil {
80 return nil, params, fmt.Errorf("command %q is ambiguous", name)
81 }
82 cmd = cmds[k]
83 }
84 }
85 if cmd == nil {
86 return nil, params, fmt.Errorf("command %q not found", name)
87 }
88
89 if len(params) == 0 || len(cmd.children) == 0 {
90 return cmd, params, nil
91 }
92 return cmd.children.Get(params)
93}
94
95var serviceCommands serviceCommandSet
96
97func init() {
98 serviceCommands = serviceCommandSet{
99 "help": {
100 usage: "[command]",
101 desc: "print help message",
102 handle: handleServiceHelp,
103 },
104 "network": {
105 children: serviceCommandSet{
106 "create": {
107 usage: "-addr <addr> [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [[-connect-command command] ...]",
108 desc: "add a new network",
109 handle: handleServiceCreateNetwork,
110 },
111 "status": {
112 desc: "show a list of saved networks and their current status",
113 handle: handleServiceNetworkStatus,
114 },
115 "delete": {
116 usage: "<name>",
117 desc: "delete a network",
118 handle: handleServiceNetworkDelete,
119 },
120 },
121 },
122 "change-password": {
123 usage: "<new password>",
124 desc: "change your password",
125 handle: handlePasswordChange,
126 },
127 }
128}
129
130func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, l *[]string) {
131 for name, cmd := range cmds {
132 words := append(prefix, name)
133 if len(cmd.children) == 0 {
134 s := strings.Join(words, " ")
135 *l = append(*l, s)
136 } else {
137 appendServiceCommandSetHelp(cmd.children, words, l)
138 }
139 }
140}
141
142func handleServiceHelp(dc *downstreamConn, params []string) error {
143 if len(params) > 0 {
144 cmd, rest, err := serviceCommands.Get(params)
145 if err != nil {
146 return err
147 }
148 words := params[:len(params)-len(rest)]
149
150 if len(cmd.children) > 0 {
151 var l []string
152 appendServiceCommandSetHelp(cmd.children, words, &l)
153 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
154 } else {
155 text := strings.Join(words, " ")
156 if cmd.usage != "" {
157 text += " " + cmd.usage
158 }
159 text += ": " + cmd.desc
160
161 sendServicePRIVMSG(dc, text)
162 }
163 } else {
164 var l []string
165 appendServiceCommandSetHelp(serviceCommands, nil, &l)
166 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
167 }
168 return nil
169}
170
171func newFlagSet() *flag.FlagSet {
172 fs := flag.NewFlagSet("", flag.ContinueOnError)
173 fs.SetOutput(ioutil.Discard)
174 return fs
175}
176
177type stringSliceVar []string
178
179func (v *stringSliceVar) String() string {
180 return fmt.Sprint([]string(*v))
181}
182
183func (v *stringSliceVar) Set(s string) error {
184 *v = append(*v, s)
185 return nil
186}
187
188func handleServiceCreateNetwork(dc *downstreamConn, params []string) error {
189 fs := newFlagSet()
190 addr := fs.String("addr", "", "")
191 name := fs.String("name", "", "")
192 username := fs.String("username", "", "")
193 pass := fs.String("pass", "", "")
194 realname := fs.String("realname", "", "")
195 nick := fs.String("nick", "", "")
196 var connectCommands stringSliceVar
197 fs.Var(&connectCommands, "connect-command", "")
198
199 if err := fs.Parse(params); err != nil {
200 return err
201 }
202 if *addr == "" {
203 return fmt.Errorf("flag -addr is required")
204 }
205
206 if addrParts := strings.SplitN(*addr, "://", 2); len(addrParts) == 2 {
207 scheme := addrParts[0]
208 switch scheme {
209 case "ircs", "irc+insecure":
210 default:
211 return fmt.Errorf("unknown scheme %q (supported schemes: ircs, irc+insecure)", scheme)
212 }
213 }
214
215 for _, command := range connectCommands {
216 _, err := irc.ParseMessage(command)
217 if err != nil {
218 return fmt.Errorf("flag -connect-command must be a valid raw irc command string: %q: %v", command, err)
219 }
220 }
221
222 if *nick == "" {
223 *nick = dc.nick
224 }
225
226 var err error
227 network, err := dc.user.createNetwork(&Network{
228 Addr: *addr,
229 Name: *name,
230 Username: *username,
231 Pass: *pass,
232 Realname: *realname,
233 Nick: *nick,
234 ConnectCommands: connectCommands,
235 })
236 if err != nil {
237 return fmt.Errorf("could not create network: %v", err)
238 }
239
240 sendServicePRIVMSG(dc, fmt.Sprintf("created network %q", network.GetName()))
241 return nil
242}
243
244func handleServiceNetworkStatus(dc *downstreamConn, params []string) error {
245 dc.user.forEachNetwork(func(net *network) {
246 var statuses []string
247 var details string
248 if uc := net.upstream(); uc != nil {
249 statuses = append(statuses, "connected as "+uc.nick)
250 details = fmt.Sprintf("%v channels", len(uc.channels))
251 } else {
252 statuses = append(statuses, "disconnected")
253 if net.lastError != nil {
254 details = net.lastError.Error()
255 }
256 }
257
258 if net == dc.network {
259 statuses = append(statuses, "current")
260 }
261
262 name := net.GetName()
263 if name != net.Addr {
264 name = fmt.Sprintf("%v (%v)", name, net.Addr)
265 }
266
267 s := fmt.Sprintf("%v [%v]", name, strings.Join(statuses, ", "))
268 if details != "" {
269 s += ": " + details
270 }
271 sendServicePRIVMSG(dc, s)
272 })
273 return nil
274}
275
276func handleServiceNetworkDelete(dc *downstreamConn, params []string) error {
277 if len(params) != 1 {
278 return fmt.Errorf("expected exactly one argument")
279 }
280
281 net := dc.user.getNetwork(params[0])
282 if net == nil {
283 return fmt.Errorf("unknown network %q", params[0])
284 }
285
286 if err := dc.user.deleteNetwork(net.ID); err != nil {
287 return err
288 }
289
290 sendServicePRIVMSG(dc, fmt.Sprintf("deleted network %q", net.GetName()))
291 return nil
292}
293
294func handlePasswordChange(dc *downstreamConn, params []string) error {
295 if len(params) != 1 {
296 return fmt.Errorf("expected exactly one argument")
297 }
298
299 hashed, err := bcrypt.GenerateFromPassword([]byte(params[0]), bcrypt.DefaultCost)
300 if err != nil {
301 return fmt.Errorf("failed to hash password: %v", err)
302 }
303 if err := dc.user.updatePassword(string(hashed)); err != nil {
304 return err
305 }
306
307 sendServicePRIVMSG(dc, "password updated")
308 return nil
309}
Note: See TracBrowser for help on using the repository browser.