// Copyright 2020 - 2021 Martin Dosch. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE file. package main import ( "bytes" "encoding/xml" "fmt" "log" "net/http" "os" "path/filepath" "regexp" "strconv" "github.com/beevik/etree" // BSD-2-clause "github.com/gabriel-vasile/mimetype" // MIT License) "github.com/mattn/go-xmpp" // BSD-3-Clause ) func httpUpload(client *xmpp.Client, jserver string, filePath string) string { var iqDiscoItemsXML IQDiscoItemsType var iqDiscoInfoXML IQDiscoInfoType var iqHttpUploadSlotXML IQHttpUploadSlot var uploadComponent string var maxFileSize int64 // Get file size fileInfo, err := os.Stat(filePath) if err != nil { log.Fatal(err) } fileSize := fileInfo.Size() // Read file buffer, err := readFile(filePath) if err != nil { log.Fatal(err) } // Get mime type mimeType := mimetype.Detect(buffer.Bytes()).String() var mimeTypeEscaped bytes.Buffer xml.Escape(&mimeTypeEscaped, []byte(mimeType)) // Get file name fileName := filepath.Base(filePath) // Just use alphanumerical characters for now to work around // https://github.com/mattn/go-xmpp/issues/132 reg, err := regexp.Compile(`[^a-zA-Z0-9\+\-\_\.]+`) if err != nil { log.Fatal(err) } fileNameEscaped := reg.ReplaceAllString(fileName, "_") // Query server for disco#items iqContent, err := sendIQ(client, jserver, "get", "") if err != nil { log.Fatal(err) } err = xml.Unmarshal(iqContent.Query, &iqDiscoItemsXML) if err != nil { log.Fatal(err) } // Check the services reported by disco#items for the http upload service for _, r := range iqDiscoItemsXML.Item { iqDiscoInfo, err := sendIQ(client, r.Jid, "get", "") if err != nil { log.Fatal(err) } if iqDiscoInfo.Type != "result" { continue } err = xml.Unmarshal(iqDiscoInfo.Query, &iqDiscoInfoXML) if err != nil { log.Fatal(err) } if iqDiscoInfoXML.Identity.Type == "file" && iqDiscoInfoXML.Identity.Category == "store" { uploadComponent = r.Jid } } if uploadComponent == "" { log.Fatal("No http upload component found.") } for _, r := range iqDiscoInfoXML.X { for i, t := range r.Field { if t.Var == "max-file-size" && r.Field[i-1].Value == nsHttpUpload { maxFileSize, err = strconv.ParseInt(t.Value, 10, 64) if err != nil { log.Fatal("Error while checking server maximum http upload file size.") } } } } // Check if the file size doesn't exceed the maximum file size of the http upload // component if a maximum file size is reported, if not just continue and hope for // the best. if maxFileSize != 0 { if fileSize > maxFileSize { log.Fatal("File size " + strconv.FormatInt(fileSize/1024/1024, 10) + " MB is larger than the maximum file size allowed (" + strconv.FormatInt(maxFileSize/1024/1024, 10) + " MB).") } } request := etree.NewDocument() requestReq := request.CreateElement("request") requestReq.CreateAttr("xmlns", nsHttpUpload) requestReq.CreateAttr("filename", fileNameEscaped) requestReq.CreateAttr("size", fmt.Sprint(fileSize)) requestReq.CreateAttr("content-type", mimeType) r, err := request.WriteToString() if err != nil { log.Fatal(err) } // Request http upload slot uploadSlot, err := sendIQ(client, uploadComponent, "get", r) if err != nil { log.Fatal(err) } if uploadSlot.Type != "result" { log.Fatal("Error while requesting upload slot.") } err = xml.Unmarshal(uploadSlot.Query, &iqHttpUploadSlotXML) if err != nil { log.Fatal(err) } // Upload file httpClient := &http.Client{} req, err := http.NewRequest(http.MethodPut, iqHttpUploadSlotXML.Put.URL, buffer) if err != nil { log.Fatal(err) } req.Header.Set("Content-Type", mimeTypeEscaped.String()) for _, h := range iqHttpUploadSlotXML.Put.Headers { switch h.Name { case "Authorization", "Cookie", "Expires": req.Header.Set(h.Name, h.Value) } } resp, err := httpClient.Do(req) if err != nil { log.Fatal(err) } // Test for http status code "200 OK" or "201 Created" if resp.StatusCode != 200 && resp.StatusCode != 201 { log.Fatal("Http upload failed.") } // Return http link return iqHttpUploadSlotXML.Get.URL }