// Copyright 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" "net/http" "net/url" "os" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/beevik/etree" // BSD-2-clause "github.com/gabriel-vasile/mimetype" // MIT License "github.com/xmppo/go-xmpp" // BSD-3-Clause ) func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath string, timeout time.Duration, ) (string, error) { var uploadComponent string var maxFileSize int64 var iqDiscoItemsXMLQuery, iqDiscoInfoXMLQuery *etree.Element // Get file size fileInfo, err := os.Stat(filePath) if err != nil { return "", err } fileSize := fileInfo.Size() // Read file buffer, err := readFile(filePath) if err != nil { return "", 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 and some special characters for now // to work around https://github.com/xmppo/go-xmpp/issues/132 reg := regexp.MustCompile(`[^a-zA-Z0-9\+\-\_\.]+`) fileNameEscaped := reg.ReplaceAllString(fileName, "_") // Query server for disco#items iqContent, err := sendIQ(client, iqc, jserver, "get", "") if err != nil { return "", err } iqDiscoItemsXML := etree.NewDocument() err = iqDiscoItemsXML.ReadFromBytes(iqContent.Query) if err != nil { return "", err } iqDiscoItemsXMLQuery = iqDiscoItemsXML.SelectElement("query") if iqDiscoItemsXMLQuery == nil { return "", fmt.Errorf("http-upload: no query element in disco items reply") } iqDiscoItemsXMLItems := iqDiscoItemsXMLQuery.SelectElements("item") // Check the services reported by disco#items for the http upload service for _, r := range iqDiscoItemsXMLItems { jid := r.SelectAttr("jid") iqDiscoInfoReqXML := etree.NewDocument() iqDiscoInfoReqXML.WriteSettings.AttrSingleQuote = true iqDiscoInfoReqXMLQuery := iqDiscoInfoReqXML.CreateElement("query") iqDiscoInfoReqXMLQuery.CreateAttr("xmlns", nsDiscoInfo) iqdi, err := iqDiscoInfoReqXML.WriteToString() if err != nil { return "", err } iqDiscoInfo, err := sendIQ(client, iqc, jid.Value, "get", iqdi) if err != nil { return "", err } if iqDiscoInfo.Type != strResult { continue } iqDiscoInfoXML := etree.NewDocument() err = iqDiscoInfoXML.ReadFromBytes(iqDiscoInfo.Query) if err != nil { return "", err } iqDiscoInfoXMLQuery = iqDiscoInfoXML.SelectElement("query") if iqDiscoInfoXMLQuery == nil { continue } iqDiscoInfoXMLIdentity := iqDiscoInfoXMLQuery.SelectElement("identity") if iqDiscoInfoXMLIdentity == nil { continue } iqDiscoInfoXMLType := iqDiscoInfoXMLIdentity.SelectAttr("type") if iqDiscoInfoXMLType == nil { continue } iqDiscoInfoXMLCategory := iqDiscoInfoXMLIdentity.SelectAttr("category") if iqDiscoInfoXMLCategory == nil { continue } if iqDiscoInfoXMLType.Value == "file" && iqDiscoInfoXMLCategory.Value == "store" { uploadComponent = jid.Value break } } if uploadComponent == "" { return "", fmt.Errorf("http-upload: no http upload component found.") } iqDiscoInfoXMLX := iqDiscoInfoXMLQuery.SelectElements("x") for _, r := range iqDiscoInfoXMLX { field := r.SelectElements("field") for i, t := range field { varAttr := t.SelectAttr("var") if varAttr == nil { continue } curFieldVal := t.SelectElement("value") if curFieldVal == nil { continue } if varAttr.Value == "max-file-size" { var prevFieldVal *etree.Element if i > 0 { prevFieldVal = field[i-1].SelectElement("value") if prevFieldVal == nil { continue } } if prevFieldVal.Text() == nsHTTPUpload { maxFileSize, err = strconv.ParseInt(curFieldVal.Text(), 10, 64) if err != nil { return "", fmt.Errorf("http-upload: 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 { return "", fmt.Errorf("http-upload: file size %s MiB is larger than the maximum file size allowed (%s MiB).", strconv.FormatInt(fileSize/1024/1024, 10), strconv.FormatInt(maxFileSize/1024/1024, 10)) } } request := etree.NewDocument() request.WriteSettings.AttrSingleQuote = true 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 { return "", err } // Request http upload slot uploadSlot, err := sendIQ(client, iqc, uploadComponent, "get", r) if err != nil { return "", err } if uploadSlot.Type != strResult { return "", fmt.Errorf("http-upload: error while requesting upload slot.") } iqHTTPUploadSlotXML := etree.NewDocument() err = iqHTTPUploadSlotXML.ReadFromBytes(uploadSlot.Query) if err != nil { return "", err } iqHTTPUploadSlotXMLSlot := iqHTTPUploadSlotXML.SelectElement("slot") if iqHTTPUploadSlotXMLSlot == nil { return "", fmt.Errorf("http-upload: no slot element") } iqHTTPUploadSlotXMLPut := iqHTTPUploadSlotXMLSlot.SelectElement("put") if iqHTTPUploadSlotXMLPut == nil { return "", fmt.Errorf("http-upload: no put element") } iqHTTPUploadSlotXMLPutURL := iqHTTPUploadSlotXMLPut.SelectAttr("url") if iqHTTPUploadSlotXMLPutURL == nil { return "", fmt.Errorf("http-upload: no url attribute") } if !strings.HasPrefix(iqHTTPUploadSlotXMLPutURL.Value, "https://") { return "", fmt.Errorf("http-upload: upload slot does not provide https") } // Upload file httpTransport := &http.Transport{ IdleConnTimeout: timeout, TLSHandshakeTimeout: timeout, } proxyEnv := os.Getenv("HTTP_PROXY") if proxyEnv != "" { proxyURL, err := url.Parse(proxyEnv) if err != nil { return "", err } httpTransport.Proxy = http.ProxyURL(proxyURL) } httpClient := &http.Client{Transport: httpTransport} req, err := http.NewRequest(http.MethodPut, iqHTTPUploadSlotXMLPutURL.Value, buffer) if err != nil { return "", err } req.Header.Set("Content-Type", mimeTypeEscaped.String()) iqHTTPUploadSlotXMLPutHeaders := iqHTTPUploadSlotXMLPut.SelectElements("header") for _, h := range iqHTTPUploadSlotXMLPutHeaders { name := h.SelectAttr("name") if name == nil { continue } switch name.Value { case "Authorization", "Cookie", "Expires": req.Header.Set(name.Value, h.Text()) } } resp, err := httpClient.Do(req) if err != nil { return "", err } // Test for http status code "200 OK" or "201 Created" if resp.StatusCode != 200 && resp.StatusCode != 201 { return "", fmt.Errorf("http-upload: upload failed.") } // Return http link iqHTTPUploadSlotXMLGet := iqHTTPUploadSlotXMLSlot.SelectElement("get") if iqHTTPUploadSlotXMLGet == nil { return "", fmt.Errorf("http-upload: no get element") } iqHTTPUploadSlotXMLGetURL := iqHTTPUploadSlotXMLGet.SelectAttr("url") if iqHTTPUploadSlotXMLGetURL == nil { return "", fmt.Errorf("http-upload: no url attribute") } err = resp.Body.Close() if err != nil { fmt.Println("http-upload: error while closing http request body:", err) } return iqHTTPUploadSlotXMLGetURL.Value, nil }