diff --git a/go.mod b/go.mod index fe4a170..614ccf4 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,21 @@ require ( github.com/OneOfOne/xxhash v1.2.8 github.com/gobwas/glob v0.2.3 github.com/golang-jwt/jwt/v4 v4.4.2 + github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 + github.com/gosimple/slug v1.13.1 github.com/igm/sockjs-go/v3 v3.0.2 github.com/orcaman/concurrent-map v1.0.0 golang.org/x/crypto v0.1.0 + golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 golang.org/x/net v0.1.0 golang.org/x/time v0.1.0 gopkg.in/ini.v1 v1.67.0 ) -go 1.13 +require ( + github.com/gosimple/unidecode v1.0.1 // indirect + golang.org/x/text v0.4.0 // indirect +) + +go 1.18 diff --git a/go.sum b/go.sum index f37dadd..3b1c96d 100644 --- a/go.sum +++ b/go.sum @@ -6,9 +6,15 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q= +github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/igm/sockjs-go/v3 v3.0.2 h1:2m0k53w0DBiGozeQUIEPR6snZFmpFpYvVsGnfLPNXbE= github.com/igm/sockjs-go/v3 v3.0.2/go.mod h1:UqchsOjeagIBFHvd+RZpLaVRbCwGilEC08EDHsD1jYE= github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY= @@ -18,40 +24,16 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 h1:5oN1Pz/eDhCpbMbLstvIPa0b/BEQo6g6nwV3pLjfM6w= +golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/main.go b/main.go index efd812b..f0cb581 100644 --- a/main.go +++ b/main.go @@ -64,7 +64,7 @@ func runGateway(configFile string, function string) { pluginsQuit := &sync.WaitGroup{} loadPlugins(gateway, pluginsQuit) - + gateway.StartXDCC() gateway.Start() pluginsQuit.Wait() diff --git a/pkg/webircgateway/config.go b/pkg/webircgateway/config.go index 019d955..ae15908 100644 --- a/pkg/webircgateway/config.go +++ b/pkg/webircgateway/config.go @@ -330,6 +330,19 @@ func (c *Config) Load() error { } c.ReverseProxies = append(c.ReverseProxies, *validRange) } + } + + if strings.Index(section.Name(), "XDCC") == 0 { + + + Configs.DomainName = section.Key("DomainName").MustString("") + Configs.TLS = section.Key("TLS").MustBool(false) + Configs.Port = section.Key("Port").MustString("3000") + Configs.LetsEncryptCacheDir = section.Key("LetsEncryptCacheDir").MustString("") + Configs.CertFile = section.Key("CertFile").MustString("") + Configs.KeyFile = section.Key("KeyFile").MustString("") + + } } diff --git a/pkg/webircgateway/gateway.go b/pkg/webircgateway/gateway.go index 47169ef..2209965 100644 --- a/pkg/webircgateway/gateway.go +++ b/pkg/webircgateway/gateway.go @@ -78,6 +78,58 @@ func (s *Gateway) Start() { proxy.Start(fmt.Sprintf("%s:%d", s.Config.Proxy.LocalAddr, s.Config.Proxy.Port)) } } +func (s *Gateway)StartXDCC() { + + + + + if Configs.TLS && Configs.LetsEncryptCacheDir == "" { + if Configs.CertFile == "" || Configs.KeyFile == "" { + s.Log(3, "'cert' and 'key' options must be set for TLS servers") + return + } + + tlsCert := s.Config.ResolvePath(Configs.CertFile) + tlsKey := s.Config.ResolvePath(Configs.KeyFile) + + s.Log(2, "XDCC: Listening with TLS on %s", Configs.Port) + keyPair, keyPairErr := tls.LoadX509KeyPair(tlsCert, tlsKey) + if keyPairErr != nil { + s.Log(3, "XDCC: Failed to listen with TLS, certificate error: %s", keyPairErr.Error()) + return + } + Configs.server.server.Addr = Configs.Port; + Configs.server.server.TLSConfig = &tls.Config{ + Certificates: []tls.Certificate{keyPair}, + } + + + + } else if Configs.TLS && Configs.LetsEncryptCacheDir != "" { + s.Log(2, "Listening with letsencrypt TLS on %s", Configs.Port) + leManager := s.Acme.Get(Configs.LetsEncryptCacheDir) + Configs.server.server.Addr = Configs.Port; + Configs.server.server.TLSConfig = &tls.Config{ + GetCertificate: leManager.GetCertificate, + } + + } + + + + + + HookRegister("irc.line", DCCSend) + HookRegister("gateway.closing", DCCClose) + HookRegister("client.state", ClientClose) + + + Configs.server.InitDispatch() + s.Log(2,"XDCC: Initializing request routes...\n") + + go Configs.server.Start() //Launch server; unblocks goroutine. + +} func (s *Gateway) Close() { hook := HookGatewayClosing{} diff --git a/pkg/webircgateway/xdcc.go b/pkg/webircgateway/xdcc.go new file mode 100644 index 0000000..c1b70cf --- /dev/null +++ b/pkg/webircgateway/xdcc.go @@ -0,0 +1,288 @@ +package webircgateway + +import ( + "context" + "log" + "os" + "github.com/gorilla/mux" + "golang.org/x/exp/maps" + "github.com/gosimple/slug" + "encoding/binary" + "fmt" + "io" + "net" + "net/http" + "strconv" + "strings" + "regexp" + "bytes" + + "github.com/kiwiirc/webircgateway/pkg/irc" + +) + +func remove[T comparable](l []T, item T) []T{ + for i, other := range l{ + if other == item{ + return append(l[:i],l[i+1:]...) + } + } + return l +} + +// Server muxer, dynamic map of handlers, and listen port. +type Server struct { + Dispatcher *mux.Router + fileNames map[string]ParsedParts + clientsMap map[string][]string + Port string + server http.Server +} +type XDCCConfig struct { + Port string + DomainName string + LetsEncryptCacheDir string + CertFile string +KeyFile string +server Server +TLS bool +} +var Configs = XDCCConfig{ +Port :"3000", +DomainName : func(n string, _ error) string { return n }(os.Hostname()), +LetsEncryptCacheDir : "", +CertFile: "", +KeyFile: "", +server: Server{Port: "3000", Dispatcher: mux.NewRouter(), fileNames: make(map[string]ParsedParts),clientsMap: make(map[string][]string), server: http.Server{ + Addr: "3000", + +}} , +TLS: false, +} + + +func int2ip(nn uint32) net.IP { + ip := make(net.IP, 4) + binary.BigEndian.PutUint32(ip, nn) + return ip +} + +type ParsedParts struct { + ip net.IP + file string + port int + length uint64 + receiverNick string + senderNick string + serverHostname string + +} + +func parseSendParams(text string) *ParsedParts { + re := regexp.MustCompile(`(?:[^\s"]+|"[^"]*")+`) + replace := regexp.MustCompile(`^"(.+)"$`) + + parts := re.FindAllString(text, -1) + + ipInt, _ := strconv.ParseUint(parts[3], 10, 32) + portInt, _ := strconv.ParseInt(parts[4], 10, 0) + lengthInt, _ := strconv.ParseUint(parts[5], 10, 64) + partsStruct := &ParsedParts{ + file: replace.ReplaceAllString(parts[2], "$1"), + ip: int2ip(uint32(ipInt)), + port: int(portInt), + length: lengthInt, + } + + return partsStruct + +} + + +type WriteCounter struct { + Total uint64 + connection *net.Conn + expectedLength uint64 + writer *io.PipeWriter +} + +func (wc *WriteCounter) Write(p []byte) (int, error) { + n := len(p) + wc.Total += uint64(n) + buf := bytes.NewBuffer(make([]byte,8)) + + if wc.expectedLength > 0xffffffff { + binary.Write((*wc.connection), binary.BigEndian, buf.Bytes()) + + }else{ + binary.Write((*wc.connection), binary.BigEndian, buf.Bytes()[4:8]) + + } + if wc.expectedLength == wc.Total{ + (*wc.writer).Close() + } + return n, nil +} + + +func serveFile(parts ParsedParts, w http.ResponseWriter, r *http.Request) (work bool) { + + ipPort := fmt.Sprintf("%s:%d", parts.ip.String(), parts.port) + //println(strings.Trim(m.GetParamU(1,""),"\x01")) + //println(parts.ip.String()) + // println(parts.port) + if parts.ip == nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 - You tried")) + return false + } + conn, err := net.Dial("tcp", ipPort) + + if err != nil { + w.WriteHeader(http.StatusBadGateway) + w.Write([]byte(err.Error())) + return false + } + + pr, pw := io.Pipe() + counter := &WriteCounter{ + connection :&conn, + Total: 0, + expectedLength: parts.length, + writer: pw, + } + + + contentDisposition := fmt.Sprintf("attachment; filename=%s", parts.file) + w.Header().Set("Content-Disposition", contentDisposition) + w.Header().Set("Content-Type", "application/octet-stream") + intLength := int(parts.length) + if uint64(intLength) != parts.length { + panic("overflows!") + } + w.Header().Set("Content-Length", strconv.Itoa(intLength) /*r.Header.Get("Content-Length")*/) + + go io.Copy(pw, io.TeeReader( conn,w)) + io.Copy(counter, pr) + + defer conn.Close() + + + return true + + +} +func DCCSend(hook *HookIrcLine) { + + if hook.Halt || hook.ToServer { + return + } + client := hook.Client + + data := hook.Line + + if data == "" { + return + } + + data = ensureUtf8(data, client.Encoding) + if data == "" { + return + } + m, parseErr := irc.ParseLine(data) + if parseErr != nil { + return + } + + pLen := len(m.Params) + + + if pLen > 0 && m.Command == "PRIVMSG" && strings.HasPrefix(strings.Trim(m.GetParamU(1, ""), "\x01"), "DCC SEND") { //can be moved to plugin goto hook.dispatch("irc.line") + + parts := parseSendParams(strings.Trim(m.GetParamU(1, ""), "\x01")) + parts.receiverNick = client.IrcState.Nick + parts.senderNick = m.Prefix.Nick + parts.serverHostname = client.UpstreamConfig.Hostname + lastIndex := strings.LastIndex(parts.file,".") + parts.file = strings.ToLower(slug.Make(parts.receiverNick + strings.ReplaceAll(parts.serverHostname, ".", "_") + parts.senderNick + parts.file[0:lastIndex]) + parts.file[lastIndex:len(parts.file)]) //long URLs may not work + hook.Message.Command = "NOTICE" + hook.Message.Params[1] = fmt.Sprintf("http://%s:3000/%s",Configs.DomainName, parts.file) + + + + Configs.server.AddFile(parts.file, *parts) + log.Printf(parts.file) + + client.SendClientSignal("data", hook.Message.ToLine()) + } + +} + +func DCCClose(hook *HookGatewayClosing) { + + Configs.server.server.Shutdown(context.Background()) + +} +func ClientClose(hook *HookClientState){ + if !hook.Connected{ + oldKeys := maps.Keys(Configs.server.clientsMap) + + for i := range oldKeys { + if strings.HasPrefix(oldKeys[i],hook.Client.IrcState.Nick + strings.ReplaceAll(hook.Client.UpstreamConfig.Hostname, ".", "_")) { + delete(Configs.server.clientsMap,oldKeys[i] ) + } + } + + + } + +} + + + + +func (s *Server) Start() { + + http.ListenAndServe(":"+s.Port, s.Dispatcher) +} + +// InitDispatch routes. +func (s *Server) InitDispatch() { + d := s.Dispatcher + + + + d.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) { + //Lookup handler in map and call it, proxying this writer and request + vars := mux.Vars(r) + name := vars["name"] + + // s.ProxyCall(w, r, name) + + parts := s.fileNames[name] + + //call serveFile here + serveFile(parts, w, r) //removed go keyword this could mean servFile can only happen once + + //destroy route + s.Destroy(parts) + + }).Methods("GET") +} + +func (s *Server) Destroy(parts ParsedParts) { + delete(s.fileNames, parts.file) + s.clientsMap[parts.receiverNick+ strings.ReplaceAll(parts.serverHostname, ".", "_")+parts.senderNick] = remove(s.clientsMap[parts.receiverNick+ strings.ReplaceAll(parts.serverHostname, ".", "_")+parts.senderNick],parts.file) +} + + + +func (s *Server) AddFile( /*w http.ResponseWriter, r *http.Request,*/ fName string, parts ParsedParts) { // add only 1 function instead + + //store the parts and the hook + s.fileNames[fName] = parts // Add the handler to our map + + Configs.server.clientsMap[parts.receiverNick + strings.ReplaceAll(parts.serverHostname, ".", "_") + parts.senderNick] = append(Configs.server.clientsMap[parts.receiverNick + strings.ReplaceAll(parts.serverHostname, ".", "_")+ parts.senderNick],fName) + + +}