mirror of
https://github.com/golang/go
synced 2024-11-25 07:47:56 -07:00
http: sniffing algorithm.
This follows draft-ietf-websec-mime-sniff-03 in its intent, though not its algorithmic specification. R=rsc CC=golang-dev https://golang.org/cl/4746042
This commit is contained in:
parent
7911965fe9
commit
78c89d21bc
@ -359,10 +359,7 @@ func (w *response) sniff() {
|
|||||||
w.needSniff = false
|
w.needSniff = false
|
||||||
|
|
||||||
data := w.conn.body
|
data := w.conn.body
|
||||||
ctype := detectContentType(data)
|
fmt.Fprintf(w.conn.buf, "Content-Type: %s\r\n", DetectContentType(data))
|
||||||
if ctype != "" {
|
|
||||||
fmt.Fprintf(w.conn.buf, "Content-Type: %s\r\n", ctype)
|
|
||||||
}
|
|
||||||
io.WriteString(w.conn.buf, "\r\n")
|
io.WriteString(w.conn.buf, "\r\n")
|
||||||
|
|
||||||
if w.chunking && len(data) > 0 {
|
if w.chunking && len(data) > 0 {
|
||||||
|
@ -4,15 +4,173 @@
|
|||||||
|
|
||||||
package http
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
// Content-type sniffing algorithm.
|
// Content-type sniffing algorithm.
|
||||||
// http://tools.ietf.org/html/draft-ietf-websec-mime-sniff-03
|
// References in this file refer to this draft specification:
|
||||||
|
// http://tools.ietf.org/html/draft-ietf-websec-mime-sniff-03
|
||||||
|
|
||||||
// The algorithm prefers to use sniffLen bytes to make its decision.
|
// The algorithm prefers to use sniffLen bytes to make its decision.
|
||||||
const sniffLen = 1024
|
const sniffLen = 512
|
||||||
|
|
||||||
// detectContentType returns the sniffed Content-Type string
|
// DetectContentType returns the sniffed Content-Type string
|
||||||
// for the given data.
|
// for the given data. This function always returns a valid MIME type.
|
||||||
func detectContentType(data []byte) string {
|
func DetectContentType(data []byte) string {
|
||||||
// TODO(dsymonds,rsc): Implement algorithm from draft.
|
if len(data) > sniffLen {
|
||||||
|
data = data[:sniffLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index of the first non-whitespace byte in data.
|
||||||
|
firstNonWS := 0
|
||||||
|
for ; firstNonWS < len(data) && isWS(data[firstNonWS]); firstNonWS++ {
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sig := range sniffSignatures {
|
||||||
|
if ct := sig.match(data, firstNonWS); ct != "" {
|
||||||
|
return ct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "application/octet-stream" // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWS(b byte) bool {
|
||||||
|
return bytes.IndexByte([]byte("\t\n\x0C\n "), b) != -1
|
||||||
|
}
|
||||||
|
|
||||||
|
type sniffSig interface {
|
||||||
|
// match returns the MIME type of the data, or "" if unknown.
|
||||||
|
match(data []byte, firstNonWS int) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data matching the table in section 6.
|
||||||
|
var sniffSignatures = []sniffSig{
|
||||||
|
htmlSig([]byte("<!DOCTYPE HTML")),
|
||||||
|
htmlSig([]byte("<HTML")),
|
||||||
|
htmlSig([]byte("<HEAD")),
|
||||||
|
htmlSig([]byte("<SCRIPT")),
|
||||||
|
htmlSig([]byte("<IFRAME")),
|
||||||
|
htmlSig([]byte("<H1")),
|
||||||
|
htmlSig([]byte("<DIV")),
|
||||||
|
htmlSig([]byte("<FONT")),
|
||||||
|
htmlSig([]byte("<TABLE")),
|
||||||
|
htmlSig([]byte("<A")),
|
||||||
|
htmlSig([]byte("<STYLE")),
|
||||||
|
htmlSig([]byte("<TITLE")),
|
||||||
|
htmlSig([]byte("<B")),
|
||||||
|
htmlSig([]byte("<BODY")),
|
||||||
|
htmlSig([]byte("<BR")),
|
||||||
|
htmlSig([]byte("<P")),
|
||||||
|
htmlSig([]byte("<!--")),
|
||||||
|
|
||||||
|
&maskedSig{mask: []byte("\xFF\xFF\xFF\xFF\xFF"), pat: []byte("<?xml"), skipWS: true, ct: "text/xml; charset=utf-8"},
|
||||||
|
|
||||||
|
&exactSig{[]byte("%PDF-"), "application/pdf"},
|
||||||
|
&exactSig{[]byte("%!PS-Adobe-"), "application/postscript"},
|
||||||
|
|
||||||
|
// UTF BOMs.
|
||||||
|
&maskedSig{mask: []byte("\xFF\xFF\x00\x00"), pat: []byte("\xFE\xFF\x00\x00"), ct: "text/plain; charset=utf-16be"},
|
||||||
|
&maskedSig{mask: []byte("\xFF\xFF\x00\x00"), pat: []byte("\xFF\xFE\x00\x00"), ct: "text/plain; charset=utf-16le"},
|
||||||
|
&maskedSig{mask: []byte("\xFF\xFF\xFF\x00"), pat: []byte("\xEF\xBB\xBF\x00"), ct: "text/plain; charset=utf-8"},
|
||||||
|
|
||||||
|
&exactSig{[]byte("GIF87a"), "image/gif"},
|
||||||
|
&exactSig{[]byte("GIF89a"), "image/gif"},
|
||||||
|
&exactSig{[]byte("\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"), "image/png"},
|
||||||
|
&exactSig{[]byte("\xFF\xD8\xFF"), "image/jpeg"},
|
||||||
|
&exactSig{[]byte("BM"), "image/bmp"},
|
||||||
|
&maskedSig{
|
||||||
|
mask: []byte("\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF"),
|
||||||
|
pat: []byte("RIFF\x00\x00\x00\x00WEBPVP"),
|
||||||
|
ct: "image/webp",
|
||||||
|
},
|
||||||
|
&exactSig{[]byte("\x00\x00\x01\x00"), "image/vnd.microsoft.icon"},
|
||||||
|
&exactSig{[]byte("\x4F\x67\x67\x53\x00"), "application/ogg"},
|
||||||
|
&maskedSig{
|
||||||
|
mask: []byte("\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF"),
|
||||||
|
pat: []byte("RIFF\x00\x00\x00\x00WAVE"),
|
||||||
|
ct: "audio/wave",
|
||||||
|
},
|
||||||
|
&exactSig{[]byte("\x1A\x45\xDF\xA3"), "video/webm"},
|
||||||
|
&exactSig{[]byte("\x52\x61\x72\x20\x1A\x07\x00"), "application/x-rar-compressed"},
|
||||||
|
&exactSig{[]byte("\x50\x4B\x03\x04"), "application/zip"},
|
||||||
|
&exactSig{[]byte("\x1F\x8B\x08"), "application/x-gzip"},
|
||||||
|
|
||||||
|
// TODO(dsymonds): MP4.
|
||||||
|
|
||||||
|
textSig(0), // should be last
|
||||||
|
}
|
||||||
|
|
||||||
|
type exactSig struct {
|
||||||
|
sig []byte
|
||||||
|
ct string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *exactSig) match(data []byte, firstNonWS int) string {
|
||||||
|
if bytes.HasPrefix(data, e.sig) {
|
||||||
|
return e.ct
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type maskedSig struct {
|
||||||
|
mask, pat []byte
|
||||||
|
skipWS bool
|
||||||
|
ct string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *maskedSig) match(data []byte, firstNonWS int) string {
|
||||||
|
if m.skipWS {
|
||||||
|
data = data[firstNonWS:]
|
||||||
|
}
|
||||||
|
if len(data) < len(m.mask) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for i, mask := range m.mask {
|
||||||
|
db := data[i] & mask
|
||||||
|
if db != m.pat[i] {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m.ct
|
||||||
|
}
|
||||||
|
|
||||||
|
type htmlSig []byte
|
||||||
|
|
||||||
|
func (h htmlSig) match(data []byte, firstNonWS int) string {
|
||||||
|
data = data[firstNonWS:]
|
||||||
|
if len(data) < len(h)+1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for i, b := range h {
|
||||||
|
db := data[i]
|
||||||
|
if 'A' <= b && b <= 'Z' {
|
||||||
|
db &= 0xDF
|
||||||
|
}
|
||||||
|
if b != db {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Next byte must be space or right angle bracket.
|
||||||
|
if db := data[len(h)]; db != ' ' && db != '>' {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return "text/html; charset=utf-8"
|
return "text/html; charset=utf-8"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type textSig int
|
||||||
|
|
||||||
|
func (textSig) match(data []byte, firstNonWS int) string {
|
||||||
|
// c.f. section 5, step 4.
|
||||||
|
for _, b := range data[firstNonWS:] {
|
||||||
|
switch {
|
||||||
|
case 0x00 <= b && b <= 0x08,
|
||||||
|
b == 0x0B,
|
||||||
|
0x0E <= b && b <= 0x1A,
|
||||||
|
0x1C <= b && b <= 0x1F:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "text/plain; charset=utf-8"
|
||||||
|
}
|
||||||
|
40
src/pkg/http/sniff_test.go
Normal file
40
src/pkg/http/sniff_test.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sniffTests = []struct {
|
||||||
|
desc string
|
||||||
|
data []byte
|
||||||
|
exp string
|
||||||
|
}{
|
||||||
|
// Some nonsense.
|
||||||
|
{"Empty", []byte{}, "text/plain; charset=utf-8"},
|
||||||
|
{"Binary", []byte{1, 2, 3}, "application/octet-stream"},
|
||||||
|
|
||||||
|
{"HTML document #1", []byte(`<HtMl><bOdY>blah blah blah</body></html>`), "text/html; charset=utf-8"},
|
||||||
|
{"HTML document #2", []byte(`<HTML></HTML>`), "text/html; charset=utf-8"},
|
||||||
|
{"HTML document #3 (leading whitespace)", []byte(` <!DOCTYPE HTML>...`), "text/html; charset=utf-8"},
|
||||||
|
|
||||||
|
{"Plain text", []byte(`This is not HTML. It has ☃ though.`), "text/plain; charset=utf-8"},
|
||||||
|
|
||||||
|
{"XML", []byte("\n<?xml!"), "text/xml; charset=utf-8"},
|
||||||
|
|
||||||
|
// Image types.
|
||||||
|
{"GIF 87a", []byte(`GIF87a`), "image/gif"},
|
||||||
|
{"GIF 89a", []byte(`GIF89a...`), "image/gif"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSniffing(t *testing.T) {
|
||||||
|
for _, st := range sniffTests {
|
||||||
|
got := DetectContentType(st.data)
|
||||||
|
if got != st.exp {
|
||||||
|
t.Errorf("%v: sniffed as %v, want %v", st.desc, got, st.exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user