mirror of
https://salsa.debian.org/mdosch/go-sendxmpp
synced 2024-11-10 19:10:30 +00:00
258 lines
7.5 KiB
Go
258 lines
7.5 KiB
Go
// 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"
|
|
"errors"
|
|
"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",
|
|
"<query xmlns='http://jabber.org/protocol/disco#items'/>")
|
|
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 "", errors.New("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 "", errors.New("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 "", errors.New("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 "", errors.New("http-upload: file size " + strconv.FormatInt(fileSize/1024/1024, 10) +
|
|
" MiB is larger than the maximum file size allowed (" +
|
|
strconv.FormatInt(maxFileSize/1024/1024, 10) + " MiB).")
|
|
}
|
|
}
|
|
|
|
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 "", errors.New("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 "", errors.New("http-upload: no slot element")
|
|
}
|
|
iqHTTPUploadSlotXMLPut := iqHTTPUploadSlotXMLSlot.SelectElement("put")
|
|
if iqHTTPUploadSlotXMLPut == nil {
|
|
return "", errors.New("http-upload: no put element")
|
|
}
|
|
iqHTTPUploadSlotXMLPutURL := iqHTTPUploadSlotXMLPut.SelectAttr("url")
|
|
if iqHTTPUploadSlotXMLPutURL == nil {
|
|
return "", errors.New("http-upload: no url attribute")
|
|
}
|
|
if !strings.HasPrefix(iqHTTPUploadSlotXMLPutURL.Value, "https://") {
|
|
return "", errors.New("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 "", errors.New("http-upload: upload failed.")
|
|
}
|
|
|
|
// Return http link
|
|
iqHTTPUploadSlotXMLGet := iqHTTPUploadSlotXMLSlot.SelectElement("get")
|
|
if iqHTTPUploadSlotXMLGet == nil {
|
|
return "", errors.New("http-upload: no get element")
|
|
}
|
|
iqHTTPUploadSlotXMLGetURL := iqHTTPUploadSlotXMLGet.SelectAttr("url")
|
|
if iqHTTPUploadSlotXMLGetURL == nil {
|
|
return "", errors.New("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
|
|
}
|