Merge pull request #62 from pidario/feat/dcc-support

start of DCC implementation
This commit is contained in:
Ken-Håvard Lieng 2020-05-19 10:44:07 +02:00 committed by GitHub
commit 63afd839be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 358 additions and 1 deletions

View File

@ -10,4 +10,8 @@ precacheAndRoute(self.__WB_MANIFEST, {
}); });
const handler = createHandlerBoundToURL('/'); const handler = createHandlerBoundToURL('/');
registerRoute(new NavigationRoute(handler)); registerRoute(
new NavigationRoute(handler, {
denylist: [new RegExp('/downloads/')]
})
);

View File

@ -5,6 +5,8 @@ port = 80
hexIP = false hexIP = false
verify_certificates = true verify_certificates = true
autoget = false
# Defaults for the client connect form # Defaults for the client connect form
[defaults] [defaults]
name = "freenode" name = "freenode"

View File

@ -14,6 +14,7 @@ type Config struct {
Dev bool Dev bool
HexIP bool HexIP bool
VerifyCertificates bool `mapstructure:"verify_certificates"` VerifyCertificates bool `mapstructure:"verify_certificates"`
Autoget bool
Headers map[string]string Headers map[string]string
Defaults Defaults Defaults Defaults
HTTPS HTTPS HTTPS HTTPS

View File

@ -21,8 +21,12 @@ type Client struct {
Realname string Realname string
HandleNickInUse func(string) string HandleNickInUse func(string) string
DownloadFolder string
Autoget bool
Messages chan *Message Messages chan *Message
ConnectionChanged chan ConnectionState ConnectionChanged chan ConnectionState
Progress chan DownloadProgress
Features *Features Features *Features
nick string nick string
channels []string channels []string
@ -50,6 +54,7 @@ func NewClient(nick, username string) *Client {
Realname: nick, Realname: nick,
Messages: make(chan *Message, 32), Messages: make(chan *Message, 32),
ConnectionChanged: make(chan ConnectionState, 16), ConnectionChanged: make(chan ConnectionState, 16),
Progress: make(chan DownloadProgress, 16),
out: make(chan string, 32), out: make(chan string, 32),
quit: make(chan struct{}), quit: make(chan struct{}),
reconnect: make(chan struct{}), reconnect: make(chan struct{}),

View File

@ -223,6 +223,11 @@ func (c *Client) recv() {
c.setNick(msg.LastParam()) c.setNick(msg.LastParam())
} }
case Privmsg:
if ctcp := msg.ToCTCP(); ctcp != nil {
c.handleCTCP(ctcp)
}
case ReplyWelcome: case ReplyWelcome:
c.setNick(msg.Params[0]) c.setNick(msg.Params[0])
c.setRegistered(true) c.setRegistered(true)
@ -239,8 +244,20 @@ func (c *Client) recv() {
if c.HandleNickInUse != nil { if c.HandleNickInUse != nil {
go c.writeNick(c.HandleNickInUse(msg.Params[1])) go c.writeNick(c.HandleNickInUse(msg.Params[1]))
} }
} }
c.Messages <- msg 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)
}
}
}
}

223
pkg/irc/dcc.go Normal file
View File

@ -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)
}

View File

@ -19,6 +19,34 @@ func (m *Message) LastParam() string {
return "" 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 { func ParseMessage(line string) *Message {
msg := Message{} msg := Message{}

View File

@ -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) state.setIRC(server.Host, i)
i.Connect(address) i.Connect(address)
go newIRCHandler(i, state).run() go newIRCHandler(i, state).run()

View File

@ -63,6 +63,19 @@ func (i *ircHandler) run() {
} else if state.Connected { } else if state.Connected {
i.log("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]) 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 { func parseMode(mode string) *Mode {
m := Mode{} m := Mode{}
add := false add := false

View File

@ -3,6 +3,7 @@ package server
import ( import (
"log" "log"
"net/http" "net/http"
"strconv"
"strings" "strings"
"sync" "sync"
@ -180,6 +181,33 @@ func (d *Dispatch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
d.upgradeWS(w, r, state) 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 { } else {
d.serveFiles(w, r) d.serveFiles(w, r)
} }

View File

@ -63,6 +63,12 @@ func (h *wsHandler) dispatchRequest(req WSRequest) {
func (h *wsHandler) init(r *http.Request) { func (h *wsHandler) init(r *http.Request) {
h.state.setWS(h.addr.String(), h.ws) h.state.setWS(h.addr.String(), h.ws)
h.state.user.SetLastIP(addrToIPBytes(h.addr)) 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, "|", log.Println(h.addr, "[State] User ID:", h.state.user.ID, "|",
h.state.numIRC(), "IRC connections |", h.state.numIRC(), "IRC connections |",

View File

@ -52,6 +52,14 @@ func (d directory) Key(username string) string {
return filepath.Join(d.User(username), "key.pem") 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 { func (d directory) Config() string {
return filepath.Join(d.ConfigRoot(), "config.toml") return filepath.Join(d.ConfigRoot(), "config.toml")
} }

View File

@ -37,6 +37,11 @@ func NewUser(store Store) (*User, error) {
return nil, err return nil, err
} }
err = os.Mkdir(Path.Downloads(user.Username), 0700)
if err != nil {
return nil, err
}
return user, nil return user, nil
} }