diff --git a/config.default.toml b/config.default.toml index 4b44ae4c..20e3f620 100644 --- a/config.default.toml +++ b/config.default.toml @@ -5,6 +5,9 @@ port = 80 hexIP = false verify_certificates = true +download_folder = "" +autoget = false + # Defaults for the client connect form [defaults] name = "freenode" diff --git a/config/config.go b/config/config.go index 83288b90..72515fa3 100644 --- a/config/config.go +++ b/config/config.go @@ -13,7 +13,9 @@ type Config struct { Port string Dev bool HexIP bool - VerifyCertificates bool `mapstructure:"verify_certificates"` + VerifyCertificates bool `mapstructure:"verify_certificates"` + DownloadFolder string `mapstructure:"download_folder"` + Autoget bool Headers map[string]string Defaults Defaults HTTPS HTTPS diff --git a/pkg/irc/client.go b/pkg/irc/client.go index f6839f2e..e3d2ed38 100644 --- a/pkg/irc/client.go +++ b/pkg/irc/client.go @@ -3,7 +3,13 @@ package irc import ( "bufio" "crypto/tls" + "encoding/binary" + "fmt" + "io" + "math" "net" + "os" + "path/filepath" "strings" "sync" "time" @@ -21,8 +27,12 @@ type Client struct { Realname string HandleNickInUse func(string) string + DownloadFolder string + Autoget bool + Messages chan *Message ConnectionChanged chan ConnectionState + Progress chan DownloadProgress Features *Features nick string channels []string @@ -50,6 +60,7 @@ func NewClient(nick, username string) *Client { Realname: nick, Messages: make(chan *Message, 32), ConnectionChanged: make(chan ConnectionState, 16), + Progress: make(chan DownloadProgress, 16), out: make(chan string, 32), quit: make(chan struct{}), reconnect: make(chan struct{}), @@ -207,3 +218,149 @@ func (c *Client) flushChannels() { } c.lock.Unlock() } + +func byteRead(totalBytes uint64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, totalBytes) + return b +} + +func round2(source float64) float64 { + return math.Round(100*source) / 100 +} + +const ( + _ = 1.0 << (10 * iota) + kibibyte + mebibyte + gibibyte +) + +func humanReadableByteCount(b float64, speed bool) string { + unit := "" + value := b + + switch { + case b >= gibibyte: + unit = "GiB" + value = value / gibibyte + case b >= mebibyte: + unit = "MiB" + value = value / mebibyte + case b >= kibibyte: + unit = "KiB" + value = value / kibibyte + case b > 1 || b == 0: + unit = "bytes" + case b == 1: + unit = "byte" + } + + if speed { + unit = unit + "/s" + } + + stringValue := strings.TrimSuffix( + fmt.Sprintf("%.2f", value), ".00", + ) + + return fmt.Sprintf("%s %s", stringValue, unit) +} + +func (c *Client) Download(pack *CTCP) { + if !c.Autoget { + // TODO: ask user if he/she wants to download the file + return + } + c.Progress <- DownloadProgress{ + PercCompletion: 0, + File: pack.File, + } + file, err := os.OpenFile(filepath.Join(c.DownloadFolder, pack.File), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + c.Progress <- DownloadProgress{ + PercCompletion: -1, + File: pack.File, + Error: err, + } + return + } + defer file.Close() + + con, err := net.Dial("tcp", fmt.Sprintf("%s:%d", pack.IP, pack.Port)) + + if err != nil { + c.Progress <- DownloadProgress{ + PercCompletion: -1, + File: pack.File, + Error: err, + } + return + } + + defer con.Close() + + var avgSpeed float64 + var prevTime int64 = -1 + secondsElapsed := int64(0) + totalBytes := uint64(0) + buf := make([]byte, 0, 4*1024) + start := time.Now().UnixNano() + for { + n, err := con.Read(buf[:cap(buf)]) + buf = buf[:n] + if n == 0 { + if err == nil { + continue + } + if err == io.EOF { + break + } + } + + if _, err := file.Write(buf); err != nil { + c.Progress <- DownloadProgress{ + PercCompletion: -1, + File: pack.File, + Error: err, + } + return + } + + cycleBytes := uint64(len(buf)) + totalBytes += cycleBytes + percentage := round2(100 * float64(totalBytes) / float64(pack.Length)) + + now := time.Now().UnixNano() + secondsElapsed = (now - start) / 1e9 + avgSpeed = round2(float64(totalBytes) / (float64(secondsElapsed))) + speed := 0.0 + + if prevTime < 0 { + speed = avgSpeed + } else { + speed = round2(1e9 * float64(cycleBytes) / (float64(now - prevTime))) + } + secondsToGo := (float64(pack.Length) - float64(totalBytes)) / speed + prevTime = now + con.Write(byteRead(totalBytes)) + c.Progress <- DownloadProgress{ + InstSpeed: humanReadableByteCount(speed, true), + AvgSpeed: humanReadableByteCount(avgSpeed, true), + PercCompletion: percentage, + BytesRemaining: humanReadableByteCount(float64(pack.Length-totalBytes), false), + BytesCompleted: humanReadableByteCount(float64(totalBytes), false), + SecondsElapsed: secondsElapsed, + SecondsToGo: secondsToGo, + File: pack.File, + } + } + con.Write(byteRead(totalBytes)) + c.Progress <- DownloadProgress{ + AvgSpeed: humanReadableByteCount(avgSpeed, true), + PercCompletion: 100, + BytesCompleted: humanReadableByteCount(float64(totalBytes), false), + SecondsElapsed: secondsElapsed, + File: pack.File, + } +} diff --git a/pkg/irc/conn.go b/pkg/irc/conn.go index 935a55f4..ae819a2b 100644 --- a/pkg/irc/conn.go +++ b/pkg/irc/conn.go @@ -239,6 +239,11 @@ func (c *Client) recv() { if c.HandleNickInUse != nil { go c.writeNick(c.HandleNickInUse(msg.Params[1])) } + + } + + if ctcp := msg.ToCTCP(); ctcp != nil { + go c.Download(ctcp) } c.Messages <- msg diff --git a/pkg/irc/message.go b/pkg/irc/message.go index cc64d55e..b40fecd7 100644 --- a/pkg/irc/message.go +++ b/pkg/irc/message.go @@ -1,7 +1,12 @@ package irc import ( + "encoding/json" + "fmt" + "path" + "strconv" "strings" + "unicode" ) type Message struct { @@ -12,6 +17,26 @@ type Message struct { Params []string } +type DownloadProgress struct { + File string `json:"file"` + Error error `json:"error"` + BytesCompleted string `json:"bytes_completed"` + BytesRemaining string `json:"bytes_remaining"` + PercCompletion float64 `json:"perc_completion"` + AvgSpeed string `json:"avg_speed"` + InstSpeed string `json:"speed"` + SecondsElapsed int64 `json:"elapsed"` + SecondsToGo float64 `json:"eta"` +} + +// CTCP is used to parse a message into a CTCP message +type CTCP struct { + File string `json:"file"` + IP string `json:"ip"` + Port uint16 `json:"port"` + Length uint64 `json:"length"` +} + func (m *Message) LastParam() string { if len(m.Params) > 0 { return m.Params[len(m.Params)-1] @@ -19,6 +44,40 @@ func (m *Message) LastParam() string { return "" } +// ToCTCP tries to parse the message parameters into a CTCP message +func (m *Message) ToCTCP() *CTCP { + params := strings.Join(m.Params, " ") + if strings.Contains(params, "DCC SEND") { + // to be extra sure that there are non-printable characters + params = strings.TrimFunc(params, func(r rune) bool { + return !unicode.IsPrint(r) + }) + parts := strings.Split(params, " ") + ip, err := strconv.Atoi(parts[4]) + port, err := strconv.Atoi(parts[5]) + length, err := strconv.Atoi(parts[6]) + + if err != nil { + return nil + } + + ip3 := uint32ToIP(ip) + + filename := path.Base(parts[3]) + if filename == "/" || filename == "." { + filename = "" + } + + return &CTCP{ + File: filename, + IP: ip3, + Port: uint16(port), + Length: uint64(length), + } + } + return nil +} + func ParseMessage(line string) *Message { msg := Message{} @@ -112,3 +171,19 @@ var unescapeTagReplacer = strings.NewReplacer( func unescapeTag(s string) string { return unescapeTagReplacer.Replace(s) } + +func uint32ToIP(n int) string { + var byte1 = n & 255 + var byte2 = ((n >> 8) & 255) + var byte3 = ((n >> 16) & 255) + var byte4 = ((n >> 24) & 255) + return fmt.Sprintf("%d.%d.%d.%d", byte4, byte3, byte2, byte1) +} + +func (p DownloadProgress) ToJSON() string { + progress, err := json.Marshal(p) + if err != nil { + return "" + } + return string(progress) +} diff --git a/server/irc.go b/server/irc.go index 8e67c96a..f708e9d6 100644 --- a/server/irc.go +++ b/server/irc.go @@ -70,6 +70,9 @@ func connectIRC(server *storage.Server, state *State, srcIP []byte) *irc.Client } } + i.DownloadFolder = cfg.DownloadFolder + i.Autoget = cfg.Autoget + state.setIRC(server.Host, i) i.Connect(address) go newIRCHandler(i, state).run() diff --git a/server/irc_handler.go b/server/irc_handler.go index 9a8c1ed0..27b2aeb0 100644 --- a/server/irc_handler.go +++ b/server/irc_handler.go @@ -63,6 +63,8 @@ func (i *ircHandler) run() { } else if state.Connected { i.log("Connected") } + case progress := <-i.client.Progress: + i.state.sendJSON("progress", progress.ToJSON()) } } }