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

huangapple go评论72阅读模式
英文:

How to quote strings for use in HTTP header fields?

问题

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

我正在编写一个Go的net/http处理程序,并且我想设置Content-Disposition头字段来指定浏览器在保存文件时应该使用的文件名。根据MDN的说明,语法如下:

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

其中"filename.jpg"是一个HTTP的"quoted-string"。然而,在net/http文档中我没有看到任何关于"quote"的提及,只有关于HTML和URL转义的提及。

quoted-string和URL转义是相同的,或者至少是兼容的吗?我可以直接使用url.QueryEscapeurl.PathEscape吗?如果可以,我应该使用哪一个,或者它们都适用于这个目的?HTTP的quoted-string看起来类似于URL转义,但我没有立即找到任何关于它们是否兼容或是否存在需要担心的边界情况的信息。

另外,是否有更高级的包可以代替,可以处理包含此类参数的HTTP头字段值的构造细节?

英文:

TL;DR: Given an arbitrary filename as a Go string value, what's the best way to create a Content-Disposition header field that specifies that filename?

I'm writing a Go net/http handler, and I want to set the Content-Disposition header field to specify a filename that the browser should use when saving the file. According to MDN, the syntax is:

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

and "filename.jpg" in an HTTP "quoted-string". However, I don't see any mention of "quote" in the net/http docs. Only mentions of HTML and URL escaping.

Is quoted-string the same as or at least compatible with URL escaping? Can I just use url.QueryEscape or url.PathEscape for this? If so, which one should I use, or are they both safe for this purpose? HTTP quoted-string looks similar to URL escaping, but I can't immediately find anything saying whether they're compatible, or if there are edge cases to worry about.

Alternatively, is there a higher-level package I should be using instead that can handle the details of constructing HTTP header field values that contain parameters like this?

答案1

得分: 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 )

其中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 {
    // 处理文件名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包源代码中复制以下代码:

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

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

标准库的代码与Steven Penny的答案中的代码类似,但标准库的代码只在每次调用escapeQuotes时分配和构建替换器一次。

英文:

HTTP quoted-string is defined in 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    = &quot;\&quot; ( HTAB / SP / VCHAR / obs-text )

where VCHAR is any visible ASCII character.

The following function quotes per the 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(&#39;&quot;&#39;)
	for i := 0; i &lt; len(s); i++ {
		b := s[i]
		if (b &lt; &#39; &#39; &amp;&amp; b != &#39;\t&#39;) || b == 0x7f {
			return &quot;&quot;, fmt.Errorf(&quot;invalid byte %0x&quot;, b)
		}
		if b == &#39;\\&#39; || b == &#39;&quot;&#39; {
			result.WriteByte(&#39;\\&#39;)
		}
		result.WriteByte(b)
	}
	result.WriteByte(&#39;&quot;&#39;)
	return result.String(), nil
}

Use the function like this:

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

It may be convenient to fix invalid bytes instead of reporting an error. It's probably a good idea to clean up invalid UTF8 as well. Here's a quote function that does that:

// 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(&#39;&quot;&#39;)
	for _, r := range s {
		if (r &lt; &#39; &#39; &amp;&amp; r != &#39;\t&#39;) || r == 0x7f || r == 0xfffd {
			r = &#39;_&#39;
		}
		if r == &#39;\\&#39; || r == &#39;&quot;&#39; {
			result.WriteByte(&#39;\\&#39;)
		}
		result.WriteRune(r)
	}
	result.WriteByte(&#39;&quot;&#39;)
	return result.String()
}

If you know that the filename does not contain invalid bytes, then copy the following code from the mime/multipart package source:

var quoteEscaper = strings.NewReplacer(&quot;\\&quot;, &quot;\\\\&quot;, `&quot;`, &quot;\\\&quot;&quot;)

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

The standard library code is similar to the code in Steven Penny's answer, but the standard library code allocates and builds the replacer once instead of on each invocation of escapeQuotes.

答案2

得分: 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())
}

结果:

--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
英文:

One way is using the multipart package [1]:

package main

import (
   &quot;mime/multipart&quot;
   &quot;strings&quot;
)

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

Result:

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

or you can use this function, based on the Go source code [2]:

package escape
import &quot;strings&quot;

func escapeQuotes(s string) string {
   return strings.NewReplacer(`\`, `\\`, `&quot;`, `\&quot;`).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

答案3

得分: 0

我认为这只是意味着你应该在文件名周围使用普通的引号。假设你想修复文件名,那么你可以按照MDN的建议设置头部。

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

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"`)

你需要确保文件名不包含任何可能被操作系统解释为破坏文件路径的字符。例如,包含/或\可能会导致下载出现问题,或者文件名太长。

假设文件名不是最终用户定义的,那么可能会没问题。如果使用用户自由文本,则可能需要以某种方式进行限制和验证。

英文:

I think this just means that you should have normal quotes around the filename. Assuming you want to fix the filename then you can just set the header as per the MDN suggestion.

Without escaping the quotes might be possible, but only if filename does not contain spaces:

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

The quotes around the filename allow spaces:

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

Using multiline quote (`) instead

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

You will want to make sure that the filename does not contain any characters which could be interpreted by the OS in some that would corrupt the path of the file. e.g. Having the / or \ may cause some issues with the download, or having a filename that is too long.

Assuming the filename is not end-user defined, you'll probably be ok. If using user free text, then you might want to restrict and validate in some way.

答案4

得分: -3

这是什么问题,只需转义并像其他标头值一样添加即可,像这样:

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

What is the problem with that, just escaping and adding like other header values like this?

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

huangapple
  • 本文由 发表于 2021年6月28日 03:39:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/68154687.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定