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
更多关于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头部也能成功下载文件。

