diff --git a/go-sendxmpp.go b/go-sendxmpp.go index c7a4f2a..cd8689a 100644 --- a/go-sendxmpp.go +++ b/go-sendxmpp.go @@ -167,6 +167,7 @@ func main() { // Define command line flags. flagHelp := getopt.BoolLong("help", 0, "Show help.") + flagHttpUpload := getopt.StringLong("http-upload", 0, "", "Send a file via http-upload.") flagDebug := getopt.BoolLong("debug", 'd', "Show debugging info.") flagServer := getopt.StringLong("jserver", 'j', "", "XMPP server address.") flagUser := getopt.StringLong("username", 'u', "", "Username for XMPP account.") @@ -274,6 +275,11 @@ func main() { log.Fatal(err) } + if *flagHttpUpload != "" { + message = httpUpload(client, strings.Split(server, ":")[0], + *flagHttpUpload) + } + // Skip reading message if '-i' or '--interactive' is set to work with e.g. 'tail -f'. if !*flagInteractive { if message == "" { diff --git a/httpupload.go b/httpupload.go new file mode 100644 index 0000000..3728ca7 --- /dev/null +++ b/httpupload.go @@ -0,0 +1,208 @@ +// Copyright 2020 Martin Dosch. +// Use of this source code is governed by the BSD-2-clause +// license that can be found in the LICENSE file. + +// TODO: +// * Check filesize limit before trying to send +// * Check IQ replies for type='error' +// * Send link OOB + +package main + +import ( + "bytes" + "crypto/rand" + "encoding/xml" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "strconv" + + "github.com/mattn/go-xmpp" // BSD-3-Clause" +) + +func getID() string { + b := make([]byte, 12) + _, err := rand.Read(b) + if err != nil { + log.Fatal(err) + } + id := fmt.Sprintf("%x-%x-%x", b[0:4], b[4:8], b[8:]) + return id +} + +func httpUpload(client *xmpp.Client, jserver string, filePath string) string { + + // Created with https://github.com/miku/zek + type IQDiscoItemsType struct { + XMLName xml.Name `xml:"query"` + Text string `xml:",chardata"` + Xmlns string `xml:"xmlns,attr"` + Item []struct { + Text string `xml:",chardata"` + Jid string `xml:"jid,attr"` + } `xml:"item"` + } + + // Created with https://github.com/miku/zek + type IQDiscoInfoType struct { + XMLName xml.Name `xml:"query"` + Text string `xml:",chardata"` + Xmlns string `xml:"xmlns,attr"` + Identity struct { + Text string `xml:",chardata"` + Type string `xml:"type,attr"` + Name string `xml:"name,attr"` + Category string `xml:"category,attr"` + } `xml:"identity"` + Feature []struct { + Text string `xml:",chardata"` + Var string `xml:"var,attr"` + } `xml:"feature"` + X []struct { + Text string `xml:",chardata"` + Type string `xml:"type,attr"` + Xmlns string `xml:"xmlns,attr"` + Field []struct { + Text string `xml:",chardata"` + Type string `xml:"type,attr"` + Var string `xml:"var,attr"` + Value string `xml:"value"` + } `xml:"field"` + } `xml:"x"` + } + + // Created with https://github.com/miku/zek + type IQHttpUploadSlot struct { + XMLName xml.Name `xml:"slot"` + Text string `xml:",chardata"` + Xmlns string `xml:"xmlns,attr"` + Get struct { + Text string `xml:",chardata"` + URL string `xml:"url,attr"` + } `xml:"get"` + Put struct { + Text string `xml:",chardata"` + URL string `xml:"url,attr"` + } `xml:"put"` + } + + var iqDiscoItemsXML IQDiscoItemsType + var iqDiscoInfoXML IQDiscoInfoType + var uploadComponent string + var iqHttpUploadSlotXML IQHttpUploadSlot + + // Get file size + fileInfo, err := os.Stat(filePath) + if err != nil { + log.Fatal(err) + } + fileSize := fileInfo.Size() + + // Open File + f, err := os.Open(filePath) + if err != nil { + log.Fatal(err) + } + defer f.Close() + + // Read file + buffer := make([]byte, fileSize) + _, err = f.Read(buffer) + if err != nil { + log.Fatal(err) + } + + // Get mime type + mimeType := http.DetectContentType(buffer) + + // Get file name + fileName := filepath.Base(filePath) + + // IQDiscoItemsType server for disco#items + id := getID() + c := make(chan xmpp.IQ) + go getIQ(client, id, c) + _, err = client.RawInformation(client.JID(), jserver, id, + "get", "") + if err != nil { + log.Fatal(err) + } + iqContent := <-c + close(c) + xml.Unmarshal(iqContent.Query, &iqDiscoItemsXML) + + // Check the services reported by disco#items for the http upload service + for _, r := range iqDiscoItemsXML.Item { + id = getID() + c := make(chan xmpp.IQ) + go getIQ(client, id, c) + _, err = client.RawInformation(client.JID(), r.Jid, id, "get", + "") + if err != nil { + log.Fatal(err) + } + iqDiscoInfo := <-c + close(c) + xml.Unmarshal(iqDiscoInfo.Query, &iqDiscoInfoXML) + + if iqDiscoInfoXML.Identity.Type == "file" && iqDiscoInfoXML.Identity.Category == "store" { + uploadComponent = r.Jid + } + + } + + if uploadComponent == "" { + log.Fatal("No http upload component found.") + } + + // Request http upload slot + id = getID() + c = make(chan xmpp.IQ) + go getIQ(client, id, c) + _, err = client.RawInformation(client.JID(), uploadComponent, id, "get", + "") + if err != nil { + log.Fatal(err) + } + uploadSlot := <-c + close(c) + xml.Unmarshal(uploadSlot.Query, &iqHttpUploadSlotXML) + + // Upload file + httpClient := &http.Client{} + req, err := http.NewRequest(http.MethodPut, iqHttpUploadSlotXML.Put.URL, bytes.NewBuffer(buffer)) + if err != nil { + log.Fatal(err) + } + req.Header.Set("Content-Type", mimeType) + resp, err := httpClient.Do(req) + if err != nil { + log.Fatal(err) + } + fmt.Println(resp.StatusCode) + + // Return http link + return iqHttpUploadSlotXML.Get.URL +} + +func getIQ(client *xmpp.Client, id string, c chan xmpp.IQ) { + for { + msg, err := client.Recv() + if err != nil { + log.Fatal(err) + } + + switch v := msg.(type) { + case xmpp.IQ: + if v.ID == id { + c <- v + return + } + } + } +}