Golang使用net/textproto从故障服务器下载文件的问题

Golang使用net/textproto从故障服务器下载文件的问题 你好,

我有一个项目,需要从物联网设备导入/下载文件。

听起来很简单,但我无法下载所有我想要的文件。

首先,我做了什么?

我使用了 net/http 包并简单地下载内容。我已经从示例中移除了所有不必要的代码。所以直接来看:

import (
	"io"
	"net/http"
	neturl "net/url"
)

func readFile() ([]byte, error)  {
	// 构建 URL,实际上它们是作为参数传入函数的
	var url string
	var filename string
	filename = "filename"
	url = "http://192.168.0.15/?download=" + neturl.QueryEscape(filename)

	// 打开连接
	res, err := http.Get(url)
	if err != nil {
		// 可以记录错误,返回错误
		return []byte(""), err
	}
	// 记得关闭文件读取器
	defer func(Body io.ReadCloser) {
		err := Body.Close()
		if err != nil {
			// 可以记录错误
		}
	}(res.Body)
	// 读取/下载文件内容
	content, err := io.ReadAll(res.Body)
	if err != nil {
		// 可以记录错误,返回错误
		return []byte(""), err
	}
	return content, nil
}

所以这实际上工作得很好……或者说曾经是。

更多信息

  • 如果我使用 go1.18.10 构建项目,一切正常。
  • 如果我使用 go 1.20.5 构建项目,有时会出错
  • 原因:物联网设备的网络服务器

物联网设备有时会创建包含非 UTF-8 字符的损坏文件名。 设备上的网络服务器将这些名称放入 HTTP 响应头中,这导致了无效的 Content-Disposition 头。

像 Chromium 或 Firefox 这样的网络浏览器会忽略这些无效的头部,并正常下载文件。

错误及其来源

错误信息示例:

Get "http://192.168.0.15/download?download=A_123_%7D%7F%FE%EF%EF%FE%E0%FE_0_xyz.txt": net/http: HTTP/1.x transport connection broken: malformed MIME header line: Content-Disposition: attachment; filename=A_123_}������_0_xyz.txt

正如你在请求 URL 中看到的,文件名包含无效字符。不幸的是,我无法更改物联网设备上的软件来修复这些格式错误的头部。

在客户端,我找到的信息是错误来自 net/textproto 包,该包在 go1.20 中进行了更新。

所以目前我正在寻找一种方法,即使这个头部是损坏的,也能下载这些文件。也许有人能给我指明正确的方向。

感谢你的帮助!


更多关于Golang使用net/textproto从故障服务器下载文件的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang使用net/textproto从故障服务器下载文件的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


根据你的描述,问题确实出现在Go 1.20中net/textproto包的更新,它加强了对HTTP头部格式的验证。当物联网设备返回包含非UTF-8字符的损坏文件名时,会触发malformed MIME header line错误。

解决方案是使用更底层的HTTP客户端实现,绕过net/textproto的严格验证。以下是几种可行的方案:

方案1:使用自定义Transport绕过头部验证

import (
    "bufio"
    "bytes"
    "io"
    "net"
    "net/http"
    neturl "net/url"
    "strings"
    "time"
)

type lenientReader struct {
    *bufio.Reader
}

func (r *lenientReader) ReadLine() (string, error) {
    line, err := r.Reader.ReadString('\n')
    if err != nil {
        return "", err
    }
    // 移除回车换行
    line = strings.TrimSuffix(line, "\r\n")
    line = strings.TrimSuffix(line, "\n")
    
    // 处理损坏的Content-Disposition头部
    if strings.HasPrefix(line, "Content-Disposition:") {
        // 找到第一个冒号
        idx := strings.Index(line, ":")
        if idx == -1 {
            return line, nil
        }
        
        // 分离键和值
        key := line[:idx]
        value := line[idx+1:]
        
        // 清理值中的非法字符
        var cleaned bytes.Buffer
        for _, b := range []byte(value) {
            if b >= 32 && b <= 126 || b == '\t' {
                cleaned.WriteByte(b)
            } else {
                cleaned.WriteByte(' ')
            }
        }
        
        return key + ":" + cleaned.String(), nil
    }
    
    return line, nil
}

func readFileWithCustomTransport() ([]byte, error) {
    filename := "filename"
    url := "http://192.168.0.15/?download=" + neturl.QueryEscape(filename)
    
    // 创建自定义Transport
    transport := &http.Transport{
        DialContext: (&net.Dialer{
            Timeout:   30 * time.Second,
            KeepAlive: 30 * time.Second,
        }).DialContext,
        // 使用自定义的RoundTripper
    }
    
    client := &http.Client{
        Transport: transport,
        Timeout:   30 * time.Second,
    }
    
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return nil, err
    }
    
    resp, err := client.Do(req)
    if err != nil {
        // 如果是头部解析错误,尝试原始TCP连接
        if strings.Contains(err.Error(), "malformed MIME header") {
            return readFileRawTCP(url)
        }
        return nil, err
    }
    defer resp.Body.Close()
    
    return io.ReadAll(resp.Body)
}

方案2:使用原始TCP连接手动处理HTTP

import (
    "bufio"
    "bytes"
    "fmt"
    "io"
    "net"
    neturl "net/url"
    "strconv"
    "strings"
    "time"
)

func readFileRawTCP(url string) ([]byte, error) {
    parsedURL, err := neturl.Parse(url)
    if err != nil {
        return nil, err
    }
    
    // 建立TCP连接
    conn, err := net.DialTimeout("tcp", parsedURL.Host, 30*time.Second)
    if err != nil {
        return nil, err
    }
    defer conn.Close()
    
    // 设置读取超时
    conn.SetReadDeadline(time.Now().Add(30 * time.Second))
    
    // 发送HTTP请求
    path := parsedURL.Path
    if parsedURL.RawQuery != "" {
        path = path + "?" + parsedURL.RawQuery
    }
    
    request := fmt.Sprintf("GET %s HTTP/1.1\r\n", path)
    request += fmt.Sprintf("Host: %s\r\n", parsedURL.Host)
    request += "Connection: close\r\n"
    request += "\r\n"
    
    _, err = conn.Write([]byte(request))
    if err != nil {
        return nil, err
    }
    
    // 读取响应
    reader := bufio.NewReader(conn)
    
    // 读取状态行
    _, err = reader.ReadString('\n')
    if err != nil {
        return nil, err
    }
    
    // 读取头部,跳过验证
    contentLength := -1
    transferEncoding := ""
    
    for {
        line, err := reader.ReadString('\n')
        if err != nil {
            return nil, err
        }
        
        line = strings.TrimSpace(line)
        if line == "" {
            break // 头部结束
        }
        
        // 宽松解析头部
        if idx := strings.Index(line, ":"); idx > 0 {
            key := strings.TrimSpace(line[:idx])
            value := strings.TrimSpace(line[idx+1:])
            
            switch strings.ToLower(key) {
            case "content-length":
                if n, err := strconv.Atoi(value); err == nil {
                    contentLength = n
                }
            case "transfer-encoding":
                transferEncoding = strings.ToLower(value)
            }
        }
    }
    
    // 读取响应体
    var body []byte
    if transferEncoding == "chunked" {
        // 处理分块传输
        for {
            // 读取块大小
            chunkSizeLine, err := reader.ReadString('\n')
            if err != nil {
                return nil, err
            }
            
            chunkSizeStr := strings.TrimSpace(chunkSizeLine)
            chunkSize, err := strconv.ParseInt(chunkSizeStr, 16, 64)
            if err != nil {
                return nil, err
            }
            
            if chunkSize == 0 {
                // 读取最后的CRLF
                reader.ReadString('\n')
                break
            }
            
            // 读取块数据
            chunk := make([]byte, chunkSize)
            _, err = io.ReadFull(reader, chunk)
            if err != nil {
                return nil, err
            }
            
            body = append(body, chunk...)
            
            // 读取块后的CRLF
            reader.ReadString('\n')
        }
    } else if contentLength > 0 {
        // 根据Content-Length读取
        body = make([]byte, contentLength)
        _, err = io.ReadFull(reader, body)
        if err != nil {
            return nil, err
        }
    } else {
        // 读取直到连接关闭
        var buf bytes.Buffer
        _, err := io.Copy(&buf, reader)
        if err != nil && err != io.EOF {
            return nil, err
        }
        body = buf.Bytes()
    }
    
    return body, nil
}

方案3:修改net/textproto的Reader(需要Go 1.20+)

import (
    "bufio"
    "net/textproto"
    "strings"
)

type lenientMIMEHeader struct {
    textproto.Reader
}

func (r *lenientMIMEHeader) ReadMIMEHeader() (textproto.MIMEHeader, error) {
    m := make(textproto.MIMEHeader)
    
    for {
        kv, err := r.ReadLine()
        if err != nil {
            return m, err
        }
        
        if kv == "" {
            return m, nil
        }
        
        // 宽松解析头部行
        i := strings.Index(kv, ":")
        if i < 0 {
            continue // 跳过无效行
        }
        
        key := textproto.CanonicalMIMEHeaderKey(kv[:i])
        value := strings.TrimSpace(kv[i+1:])
        
        // 清理值中的非法字符
        var cleaned strings.Builder
        for _, b := range []byte(value) {
            if b >= 32 && b <= 126 || b == '\t' {
                cleaned.WriteByte(b)
            }
        }
        
        m[key] = append(m[key], cleaned.String())
    }
}

// 使用示例
func useLenientReader() {
    // 这需要你能够修改http包的内部实现
    // 或者创建自定义的HTTP客户端
}

推荐方案

建议使用方案2的原始TCP连接方法,因为它完全绕过了net/textproto的验证,对损坏的头部有最好的兼容性。你可以这样使用:

func main() {
    content, err := readFileRawTCP("http://192.168.0.15/?download=A_123_%7D%7F%FE%EF%EF%FE%E0%FE_0_xyz.txt")
    if err != nil {
        // 处理错误
    }
    // 使用content
}

这种方法虽然需要手动处理HTTP协议细节,但能确保即使面对损坏的HTTP头部也能成功下载文件。

回到顶部