如何引用用于 HTTP 标头字段的字符串?

Mat*_*sky 5 go http-headers

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

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

Content-Disposition: attachment; filename="filename.jpg"
Run Code Online (Sandbox Code Playgroud)

"filename.jpg"在 HTTP“引用字符串”中。但是,我在net/http 文档中没有看到任何提及“引用”的内容。仅提及 HTML 和 URL 转义。

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

或者,是否有我应该使用的更高级别的包来处理构建包含此类参数的 HTTP 标头字段值的细节?

Cer*_*món 7

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 )
 
Run Code Online (Sandbox Code Playgroud)

其中 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
}
Run Code Online (Sandbox Code Playgroud)

像这样使用函数:

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

修复无效字节而不是报告错误可能会很方便。清理无效的 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()
}
Run Code Online (Sandbox Code Playgroud)

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

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

func escapeQuotes(s string) string {
    return quoteEscaper.Replace(s)
}
Run Code Online (Sandbox Code Playgroud)

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


小智 5

一种方法是使用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())
}
Run Code Online (Sandbox Code Playgroud)

结果:

--81200ce57413eafde86bb95b1ba47121862043451ba5e55cda9af9573277
Content-Disposition: form-data; name="attachment"; filename="filename.jpg"
Content-Type: application/octet-stream
Run Code Online (Sandbox Code Playgroud)

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

package escape
import "strings"

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