spongy

A Unixy IRC client
git clone https://git.woozle.org/neale/spongy.git

spongy / spongyd
Neale Pickett  ·  2016-05-18

network.go

  1package main
  2
  3import (
  4	"bufio"
  5	"crypto/tls"
  6	"fmt"
  7	"io"
  8	"log"
  9	"net"
 10	"os"
 11	"os/user"
 12	"os/exec"
 13	"path"
 14	"strings"
 15	"time"
 16)
 17
 18// This gets called every time the data's needed.
 19// That makes it so you can change stuff while running.
 20
 21func ReadLines(fn string) ([]string, error) {
 22	lines := make([]string, 0)
 23
 24	f, err := os.Open(fn)
 25	if err != nil {
 26		return lines, err
 27	}
 28	defer f.Close()
 29
 30	scanner := bufio.NewScanner(f)
 31	for scanner.Scan() {
 32		line := strings.TrimSpace(scanner.Text())
 33		switch {
 34		case line == "":
 35		default:
 36			lines = append(lines, line)
 37		}
 38	}
 39
 40	return lines, nil
 41}
 42
 43type Network struct {
 44	running bool
 45
 46	Nick string
 47	
 48	basePath string
 49	serverIndex int
 50
 51	conn io.ReadWriteCloser
 52
 53	logf *Logfile
 54	
 55	inq  chan string
 56	outq chan string
 57}
 58
 59func NewNetwork(basePath string) *Network {
 60	nw := Network{
 61		running: true,
 62		basePath: basePath,
 63	}
 64	nw.logf = NewLogfile(nw.basePath, int(maxlogsize))
 65	
 66	return &nw
 67}
 68
 69func (nw *Network) Close() {
 70	nw.running = false
 71	if nw.conn != nil {
 72		nw.conn.Close()
 73	}
 74	nw.logf.Close()
 75}
 76
 77func (nw *Network) watchOutqDirectory() {
 78	outqDirname := path.Join(nw.basePath, "outq")
 79
 80	dir, err := os.Open(outqDirname)
 81	if err != nil {
 82		log.Fatal(err)
 83	}
 84	defer dir.Close()
 85	
 86	// XXX: Do this with fsnotify
 87	for nw.running {
 88		entities, _ := dir.Readdirnames(0)
 89		for _, fn := range entities {
 90			pathname := path.Join(outqDirname, fn)
 91			nw.HandleInfile(pathname)
 92		}
 93		_, _ = dir.Seek(0, 0)
 94		time.Sleep(500 * time.Millisecond)
 95	}
 96}
 97
 98func (nw *Network) HandleInfile(fn string) {
 99	f, err := os.Open(fn)
100	if err != nil {
101		return
102	}
103	defer f.Close()
104	
105	// Do this after Open attempt.
106	// If Open fails, the file will stick around.
107	// Hopefully this is helpful for debugging.
108	os.Remove(fn)
109
110	inf := bufio.NewScanner(f)
111	for inf.Scan() {
112		txt := inf.Text()
113		nw.outq <- txt
114	}
115}
116
117func (nw *Network) serverWriteLoop() {
118	for v := range nw.outq {
119		debug("» %s", v)
120		nw.logf.Log(v)
121		fmt.Fprintln(nw.conn, v)
122		time.Sleep(500 * time.Millisecond)
123	}
124}
125
126func (nw *Network) NextNick() {
127	nicks, err := ReadLines(path.Join(nw.basePath, "nick"))
128	if err != nil {
129		log.Print(err)
130		return
131	}
132	
133	// Make up some alternates if they weren't provided
134	if len(nicks) == 1 {
135		nicks = append(nicks, nicks[0] + "_")
136		nicks = append(nicks, nicks[0] + "__")
137		nicks = append(nicks, nicks[0] + "___")
138	}
139	
140	nextidx := 0
141	for idx, n := range nicks {
142		if n == nw.Nick {
143			nextidx = idx + 1
144		}
145	}
146	
147	nw.Nick = nicks[nextidx % len(nicks)]
148	nw.outq <- "NICK " + nw.Nick
149}
150
151func (nw *Network) JoinChannels() {
152	chans, err := ReadLines(path.Join(nw.basePath, "channels"))
153	if err != nil {
154		log.Print(err)
155		return
156	}
157	
158	for _, ch := range chans {
159		debug("Joining %s", ch)
160		nw.outq <- "JOIN " + ch
161	}
162}
163
164func (nw *Network) messageDispatchLoop() {
165	for line := range nw.inq {
166		nw.logf.Log(line)
167
168		m, err := NewMessage(line)
169		if err != nil {
170			log.Print(err)
171			continue
172		}
173		
174		switch m.Command {
175		case "PING":
176			nw.outq <- "PONG :" + m.Text
177			continue
178		case "001":
179			nw.JoinChannels()
180		case "433":
181			nw.NextNick()
182		case "PRIVMSG":
183			if m.Text == "\001VERSION\001" {
184				//nw.outq <- "NOTICE " + m.Sender + " :\001VERSION Spongy v8294.003.1R6pl58₄SEσ\001"
185				nw.outq <- "NOTICE " + m.Sender + " :\001 VERSION begin 644 version.txt\001"
186				nw.outq <- "NOTICE " + m.Sender + " :\001 VERSION F4W!O;F=Y('9E<G-I;VX@.#(Y-\"XP,#,N,5(V<&PU..*\"A%-%SX,`\001"
187				nw.outq <- "NOTICE " + m.Sender + " :\001 VERSION `\001"
188				nw.outq <- "NOTICE " + m.Sender + " :\001 VERSION end\001"
189			}
190		}
191
192		handlerPath := path.Join(nw.basePath, "handler")
193		cmd := exec.Command(handlerPath, m.Args...)
194		cmd.Env = os.Environ()
195		cmd.Env = append(cmd.Env, "command=" + m.Command)
196		cmd.Env = append(cmd.Env, "fullsender=" + m.FullSender)
197		cmd.Env = append(cmd.Env, "sender=" + m.Sender)
198		cmd.Env = append(cmd.Env, "forum=" + m.Forum)
199		cmd.Env = append(cmd.Env, "text=" + m.Text)
200		cmd.Env = append(cmd.Env, "raw=" + line)
201		cmd.Stderr = os.Stderr
202		out, err := cmd.Output()
203		if err != nil {
204			log.Print(err)
205			continue
206		}
207
208		if len(out) > 0 {
209			outlines := strings.Split(string(out), "\n")
210			for _, line := range outlines {
211				if len(line) > 0 {
212					nw.outq <- line
213				}
214			}
215		}
216	}
217}
218
219func (nw *Network) ConnectToNextServer() bool {
220	servers, err := ReadLines(path.Join(nw.basePath, "server"))
221	if err != nil {
222		log.Printf("Couldn't find any servers to connect to in %s", nw.basePath)
223		return false
224	}
225	
226	if nw.serverIndex > len(servers) {
227		nw.serverIndex = 0
228	}
229	server := servers[nw.serverIndex]
230
231	debug("Connecting to %s", server)
232	switch (server[0]) {
233	case '|':
234		parts := strings.Split(server[1:], " ")
235		nw.conn, err = StartStdioProcess(parts[0], parts[1:])
236	case '^':
237		nw.conn, err = net.Dial("tcp", server[1:])
238	default:
239		log.Print("Not validating server certificate!")
240		config := &tls.Config{
241			InsecureSkipVerify: true,
242		}
243		nw.conn, err = tls.Dial("tcp", server, config)
244	}
245	
246	if err != nil {
247		log.Print(err)
248		return false
249	}
250	debug("Connected")
251	
252	return true
253}
254
255func (nw *Network) login() {
256	var name string
257	var username string
258
259	usernames, err := ReadLines(path.Join(nw.basePath, "username"))
260	if err == nil {
261		username = usernames[0]
262	} else {
263		username = "sponge"
264	}
265
266	passwd, err := ReadLines(path.Join(nw.basePath, "passwd"))
267	if err == nil {
268		nw.outq <- "PASS " + passwd[0]
269	}
270
271
272	names, err := ReadLines(path.Join(nw.basePath, "name"))
273	if err == nil {
274		name = names[0]
275	}
276	
277	if name == "" {
278		me, err := user.Current()
279		if err == nil {
280			name = me.Name
281		}
282	}
283	
284	if name == "" {
285		// Rogue used "Rodney" if you didn't give it a name.
286		// This one works for the ladies, too.
287		name = "Ronnie"
288	}
289
290	nw.outq <- "USER " + username + " g g :" + name
291	nw.NextNick()
292}
293
294func (nw *Network) keepaliveLoop() {
295	for nw.running {
296		time.Sleep(1 * time.Minute)
297		nw.outq <- "PING :keepalive"
298	}
299}
300
301
302func (nw *Network) Connect() {
303	for nw.running {
304		if ! nw.ConnectToNextServer() {
305			time.Sleep(8 * time.Second)
306			continue
307		}
308		
309		nw.inq = make(chan string, 20)
310		nw.outq = make(chan string, 20)
311
312		go nw.serverWriteLoop()
313		go nw.messageDispatchLoop()
314		go nw.watchOutqDirectory()
315		go nw.keepaliveLoop()
316
317		nw.login()
318		
319		scanner := bufio.NewScanner(nw.conn)
320		for scanner.Scan() {
321			nw.inq <- scanner.Text()
322		}
323
324		close(nw.inq)
325		close(nw.outq)
326	}
327}
328