如何在 HTTP header 字段中引用字符串?

How to quote strings for use in HTTP header fields?

TL;DR:给定一个任意文件名作为 Go string 值,创建指定该文件名的 Content-Disposition header 字段的最佳方法是什么?

我正在编写 Go net/http 处理程序,我想设置 Content-Disposition header 字段以指定浏览器在保存文件时应使用的文件名。根据MDN,语法为:

Content-Disposition: attachment; filename="filename.jpg"

"filename.jpg" 在 HTTP“quoted-string”中。但是,我在 net/http docs 中没有看到任何提及“quote”的地方。仅提及 HTML 和 URL 转义。

quoted-string 是否与 URL 转义相同或至少兼容?我可以为此使用 url.QueryEscape or url.PathEscape 吗?如果是这样,我应该使用哪一个,或者它们都安全吗? HTTP quoted-string 看起来类似于 URL 转义,但我无法立即找到任何说明它们是否兼容,或者是否有需要担心的边缘情况。

或者,是否有我应该使用的 higher-level 包,它可以处理构建包含此类参数的 HTTP header 字段值的细节?

这有什么问题,只是像这样转义和添加其他 header 值?

w.Header().Add("Content-Disposition", "attachment; filename=\"flename.txt\"")

我认为这只是意味着您应该在文件名周围加上正常的引号。假设你想修复文件名,那么你可以根据 MDN 建议设置 header。

不转义引号是可能的,但前提是文件名不包含空格:

w.Header().Set("Content-Disposition", "attachment; filename=testsomething.txt")

文件名两边的引号允许有空格:

w.Header().Set("Content-Disposition", "attachment; filename=\"test  something.txt\"")

改用多行引号 (`)

w.Header().Set("Content-Disposition", `attachment; filename="test something.txt"`)

您需要确保文件名不包含任何可以被 OS 解释的字符,这些字符会破坏文件的路径。例如使用 / 或 \ 可能会导致下载出现一些问题,或者文件名太长。

假设文件名未 end-user 定义,您可能没问题。如果使用用户自由文本,那么您可能希望以某种方式进行限制和验证。

一种方法是使用 multipart 包 [1]:

package main

import (
   "mime/multipart"
   "strings"
)

func main() {
   b := new(strings.Builder)
   m := multipart.NewWriter(b)
   defer m.Close()
   m.CreateFormFile("attachment", "filename.jpg")
   print(b.String())
}

结果:

--81200ce57413eafde86bb95b1ba47121862043451ba5e55cda9af9573277
Content-Disposition: form-data; name="attachment"; filename="filename.jpg"
Content-Type: application/octet-stream

或者你可以使用这个函数,基于Go源码[2]:

package escape
import "strings"

func escapeQuotes(s string) string {
   return strings.NewReplacer(`\`, `\`, `"`, `\"`).Replace(s)
}
  1. https://golang.org/pkg/mime/multipart
  2. https://github.com/golang/go/blob/go1.16.5/src/mime/multipart/writer.go#L132-L136

HTTP 引号字符串在 RFC 7230:

中定义
 quoted-string  = DQUOTE *( qdtext / quoted-pair ) DQUOTE
 qdtext         = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
 obs-text       = %x80-FF
 quoted-pair    = "\" ( HTAB / SP / VCHAR / obs-text )
 

其中 VCHAR 是任何可见的 ASCII 字符。

根据 RFC 引用以下函数:

// quotedString returns s quoted per quoted-string in RFC 7230.
func quotedString(s string) (string, error) {
    var result strings.Builder
    result.Grow(len(s) + 2) // optimize for case where no \ are added.

    result.WriteByte('"')
    for i := 0; i < len(s); i++ {
        b := s[i]
        if (b < ' ' && b != '\t') || b == 0x7f {
            return "", fmt.Errorf("invalid byte %0x", b)
        }
        if b == '\' || b == '"' {
            result.WriteByte('\')
        }
        result.WriteByte(b)
    }
    result.WriteByte('"')
    return result.String(), nil
}

像这样使用函数:

qf, err := quotedString(f)
if err != nil {
    // handle invalid byte in filename f
}
header.Set("Content-Disposition", "attachment; filename=" + qf)

修复无效字节可能比报告错误更方便。清除无效的 UTF8 可能也是个好主意。这是一个执行此操作的引用函数:

// cleanQuotedString returns s quoted per quoted-string in RFC 7230 with invalid
// bytes and invalid UTF8 replaced with _.
func cleanQuotedString(s string) string {
    var result strings.Builder
    result.Grow(len(s) + 2) // optimize for case where no \ are added.

    result.WriteByte('"')
    for _, r := range s {
        if (r < ' ' && r != '\t') || r == 0x7f || r == 0xfffd {
            r = '_'
        }
        if r == '\' || r == '"' {
            result.WriteByte('\')
        }
        result.WriteRune(r)
    }
    result.WriteByte('"')
    return result.String()
}

如果您知道文件名不包含无效字节,则从mime/multipart package source复制以下代码:

var quoteEscaper = strings.NewReplacer("\", "\\", `"`, "\\"")

func escapeQuotes(s string) string {
    return quoteEscaper.Replace(s)
}

标准库代码类似于 中的代码,但标准库代码分配和构建替换器一次,而不是每次调用 escapeQuotes.