diff --git a/client/js/sw.js b/client/js/sw.js index cc70f733..d628dbdb 100644 --- a/client/js/sw.js +++ b/client/js/sw.js @@ -10,4 +10,8 @@ precacheAndRoute(self.__WB_MANIFEST, { }); const handler = createHandlerBoundToURL('/'); -registerRoute(new NavigationRoute(handler)); +registerRoute( + new NavigationRoute(handler, { + denylist: [new RegExp('/downloads/')] + }) +); diff --git a/config.default.toml b/config.default.toml index 4b44ae4c..cfe5b29b 100644 --- a/config.default.toml +++ b/config.default.toml @@ -5,6 +5,8 @@ port = 80 hexIP = false verify_certificates = true +autoget = false + # Defaults for the client connect form [defaults] name = "freenode" diff --git a/config/config.go b/config/config.go index 83288b90..7bc67f99 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,7 @@ type Config struct { Dev bool HexIP bool VerifyCertificates bool `mapstructure:"verify_certificates"` + Autoget bool Headers map[string]string Defaults Defaults HTTPS HTTPS diff --git a/pkg/irc/client.go b/pkg/irc/client.go index f6839f2e..83dd40b0 100644 --- a/pkg/irc/client.go +++ b/pkg/irc/client.go @@ -21,8 +21,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 +54,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{}), diff --git a/pkg/irc/conn.go b/pkg/irc/conn.go index 935a55f4..fc8b698a 100644 --- a/pkg/irc/conn.go +++ b/pkg/irc/conn.go @@ -223,6 +223,11 @@ func (c *Client) recv() { c.setNick(msg.LastParam()) } + case Privmsg: + if ctcp := msg.ToCTCP(); ctcp != nil { + c.handleCTCP(ctcp) + } + case ReplyWelcome: c.setNick(msg.Params[0]) c.setRegistered(true) @@ -239,8 +244,20 @@ func (c *Client) recv() { if c.HandleNickInUse != nil { go c.writeNick(c.HandleNickInUse(msg.Params[1])) } + } c.Messages <- msg } } + +func (c *Client) handleCTCP(ctcp *CTCP) { + switch ctcp.Command { + case "DCC": + if strings.HasPrefix(ctcp.Params, "SEND") { + if dccSend := ParseDCCSend(ctcp); dccSend != nil { + go c.Download(dccSend) + } + } + } +} diff --git a/pkg/irc/dcc.go b/pkg/irc/dcc.go new file mode 100644 index 00000000..bc8f7032 --- /dev/null +++ b/pkg/irc/dcc.go @@ -0,0 +1,223 @@ +package irc + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "io" + "math" + "net" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "time" +) + +type DCCSend struct { + File string `json:"file"` + IP string `json:"ip"` + Port string `json:"port"` + Length uint64 `json:"length"` +} + +func ParseDCCSend(ctcp *CTCP) *DCCSend { + params := strings.Split(ctcp.Params, " ") + + if len(params) > 4 { + ip, err := strconv.Atoi(params[2]) + if err != nil { + return nil + } + + length, err := strconv.ParseUint(params[4], 10, 64) + if err != nil { + return nil + } + + filename := path.Base(params[1]) + if filename == "/" || filename == "." { + filename = "" + } + + return &DCCSend{ + File: filename, + IP: intToIP(ip), + Port: params[3], + Length: length, + } + } + + return nil +} + +func (c *Client) Download(pack *DCCSend) { + if !c.Autoget { + // TODO: ask user if he/she wants to download the file + return + } + + c.Progress <- DownloadProgress{ + File: pack.File, + } + + file, err := os.OpenFile(filepath.Join(c.DownloadFolder, pack.File), os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + c.downloadFailed(pack, err) + return + } + defer file.Close() + + conn, err := net.Dial("tcp", net.JoinHostPort(pack.IP, pack.Port)) + if err != nil { + c.downloadFailed(pack, err) + return + } + defer conn.Close() + + totalBytes := uint64(0) + accBytes := uint64(0) + averageSpeed := float64(0) + buf := make([]byte, 4*1024) + start := time.Now() + prevUpdate := start + + for { + n, err := conn.Read(buf) + if err != nil { + if err != io.EOF { + c.downloadFailed(pack, err) + return + } + if n == 0 { + break + } + } + + if _, err := file.Write(buf[:n]); err != nil { + c.downloadFailed(pack, err) + return + } + + accBytes += uint64(n) + totalBytes += uint64(n) + + conn.Write(uint64Bytes(totalBytes)) + + if dt := time.Since(prevUpdate); dt >= time.Second { + prevUpdate = time.Now() + + speed := float64(accBytes) / dt.Seconds() + if averageSpeed == 0 { + averageSpeed = speed + } else { + averageSpeed = 0.2*speed + 0.8*averageSpeed + } + accBytes = 0 + + bytesRemaining := float64(pack.Length - totalBytes) + percentage := 100 * (float64(totalBytes) / float64(pack.Length)) + + c.Progress <- DownloadProgress{ + Speed: humanReadableByteCount(averageSpeed, true), + PercCompletion: percentage, + BytesRemaining: humanReadableByteCount(bytesRemaining, false), + BytesCompleted: humanReadableByteCount(float64(totalBytes), false), + SecondsElapsed: secondsSince(start), + SecondsToGo: bytesRemaining / averageSpeed, + File: pack.File, + } + } + } + + c.Progress <- DownloadProgress{ + PercCompletion: 100, + BytesCompleted: humanReadableByteCount(float64(totalBytes), false), + SecondsElapsed: secondsSince(start), + File: pack.File, + } +} + +func (c *Client) downloadFailed(pack *DCCSend, err error) { + c.Progress <- DownloadProgress{ + PercCompletion: -1, + File: pack.File, + Error: err, + } +} + +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"` + Speed string `json:"speed"` + SecondsElapsed int64 `json:"elapsed"` + SecondsToGo float64 `json:"eta"` +} + +func (p DownloadProgress) ToJSON() string { + progress, err := json.Marshal(p) + if err != nil { + return "" + } + return string(progress) +} + +func intToIP(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 uint64Bytes(i uint64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, i) + return b +} + +func secondsSince(t time.Time) int64 { + return int64(math.Round(time.Since(t).Seconds())) +} + +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) +} diff --git a/pkg/irc/message.go b/pkg/irc/message.go index cc64d55e..6327fec4 100644 --- a/pkg/irc/message.go +++ b/pkg/irc/message.go @@ -19,6 +19,34 @@ func (m *Message) LastParam() string { return "" } +type CTCP struct { + Command string + Params string +} + +func (m *Message) ToCTCP() *CTCP { + lp := m.LastParam() + + if len(lp) > 1 && lp[0] == 0x01 { + parts := strings.SplitN(strings.Trim(lp, "\x01"), " ", 2) + ctcp := CTCP{} + + if parts[0] != "" { + ctcp.Command = parts[0] + } else { + return nil + } + + if len(parts) == 2 { + ctcp.Params = parts[1] + } + + return &ctcp + } + + return nil +} + func ParseMessage(line string) *Message { msg := Message{} diff --git a/server/irc.go b/server/irc.go index 8e67c96a..23336a64 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 = storage.Path.Downloads(state.user.Username) + 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..1875f5cb 100644 --- a/server/irc_handler.go +++ b/server/irc_handler.go @@ -63,6 +63,19 @@ func (i *ircHandler) run() { } else if state.Connected { i.log("Connected") } + + case progress := <-i.client.Progress: + if progress.Error != nil { + i.sendDCCInfo("%s: Download failed (%s)", true, progress.File, progress.Error) + } else if progress.PercCompletion == 100 { + i.sendDCCInfo("Download finished, get it here: %s://%s/downloads/%s/%s", true, + i.state.String("scheme"), i.state.String("host"), i.state.user.Username, progress.File) + } else if progress.PercCompletion == 0 { + i.sendDCCInfo("%s: Starting download", true, progress.File) + } else { + i.sendDCCInfo("%s: %.1f%%, %s, %s remaining, %.1fs left", false, progress.File, + progress.PercCompletion, progress.Speed, progress.BytesRemaining, progress.SecondsToGo) + } } } } @@ -418,6 +431,20 @@ func (i *ircHandler) log(v ...interface{}) { log.Println("[IRC]", i.state.user.ID, i.client.Host, s[:len(s)-1]) } +func (i *ircHandler) sendDCCInfo(message string, log bool, a ...interface{}) { + msg := Message{ + Server: i.client.Host, + From: "@dcc", + Content: fmt.Sprintf(message, a...), + } + i.state.sendJSON("pm", msg) + + if log { + i.state.user.AddOpenDM(msg.Server, msg.From) + i.state.user.LogMessage(betterguid.New(), msg.Server, msg.From, msg.From, msg.Content) + } +} + func parseMode(mode string) *Mode { m := Mode{} add := false diff --git a/server/server.go b/server/server.go index 75c1bff2..4aec34b7 100644 --- a/server/server.go +++ b/server/server.go @@ -3,6 +3,7 @@ package server import ( "log" "net/http" + "strconv" "strings" "sync" @@ -180,6 +181,33 @@ func (d *Dispatch) ServeHTTP(w http.ResponseWriter, r *http.Request) { } d.upgradeWS(w, r, state) + } else if strings.HasPrefix(r.URL.Path, "/downloads") { + state := d.handleAuth(w, r, false, false) + if state == nil { + log.Println("[Auth] No state") + fail(w, http.StatusInternalServerError) + return + } + + params := strings.Split(strings.Trim(r.URL.Path, "/"), "/") + + if len(params) == 3 { + userID, err := strconv.ParseUint(params[1], 10, 64) + if err != nil { + fail(w, http.StatusBadRequest) + } + + if userID != state.user.ID { + fail(w, http.StatusUnauthorized) + } + + filename := params[2] + + w.Header().Set("Content-Disposition", "attachment; filename="+filename) + http.ServeFile(w, r, storage.Path.DownloadedFile(state.user.Username, filename)) + } else { + fail(w, http.StatusNotFound) + } } else { d.serveFiles(w, r) } diff --git a/server/websocket_handler.go b/server/websocket_handler.go index f6207e77..0ce34557 100644 --- a/server/websocket_handler.go +++ b/server/websocket_handler.go @@ -63,6 +63,12 @@ func (h *wsHandler) dispatchRequest(req WSRequest) { func (h *wsHandler) init(r *http.Request) { h.state.setWS(h.addr.String(), h.ws) h.state.user.SetLastIP(addrToIPBytes(h.addr)) + if r.TLS != nil { + h.state.Set("scheme", "https") + } else { + h.state.Set("scheme", "http") + } + h.state.Set("host", r.Host) log.Println(h.addr, "[State] User ID:", h.state.user.ID, "|", h.state.numIRC(), "IRC connections |", diff --git a/storage/directory.go b/storage/directory.go index 0aced016..4b99ac83 100644 --- a/storage/directory.go +++ b/storage/directory.go @@ -52,6 +52,14 @@ func (d directory) Key(username string) string { return filepath.Join(d.User(username), "key.pem") } +func (d directory) Downloads(username string) string { + return filepath.Join(d.User(username), "downloads") +} + +func (d directory) DownloadedFile(username string, file string) string { + return filepath.Join(d.Downloads(username), file) +} + func (d directory) Config() string { return filepath.Join(d.ConfigRoot(), "config.toml") } diff --git a/storage/user.go b/storage/user.go index 66386862..cf86f379 100644 --- a/storage/user.go +++ b/storage/user.go @@ -37,6 +37,11 @@ func NewUser(store Store) (*User, error) { return nil, err } + err = os.Mkdir(Path.Downloads(user.Username), 0700) + if err != nil { + return nil, err + } + return user, nil }