Merge pull request #62 from pidario/feat/dcc-support
start of DCC implementation
This commit is contained in:
commit
63afd839be
@ -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/')]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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{}),
|
||||||
|
@ -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
223
pkg/irc/dcc.go
Normal 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)
|
||||||
|
}
|
@ -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{}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 |",
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user