forked from Silverfish/proton-bridge
We build too many walls and not enough bridges
This commit is contained in:
364
pkg/mime/mediaType.go
Normal file
364
pkg/mime/mediaType.go
Normal file
@ -0,0 +1,364 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package pmmime
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// changeEncodingAndKeepLastParamDefinition is necessary to modify behaviour
|
||||
// provided by the golang standard libraries.
|
||||
func changeEncodingAndKeepLastParamDefinition(v string) (out string, err error) {
|
||||
log := logrus.WithField("pkg", "pm-mime")
|
||||
|
||||
out = v // By default don't do anything with that.
|
||||
keepOrig := true
|
||||
|
||||
i := strings.Index(v, ";")
|
||||
if i == -1 {
|
||||
i = len(v)
|
||||
}
|
||||
mediatype := strings.TrimSpace(strings.ToLower(v[0:i]))
|
||||
|
||||
params := map[string]string{}
|
||||
var continuation map[string]map[string]string
|
||||
|
||||
v = v[i:]
|
||||
for len(v) > 0 {
|
||||
v = strings.TrimLeftFunc(v, unicode.IsSpace)
|
||||
if len(v) == 0 {
|
||||
break
|
||||
}
|
||||
key, value, rest := consumeMediaParam(v)
|
||||
if key == "" {
|
||||
break
|
||||
}
|
||||
|
||||
pmap := params
|
||||
if idx := strings.Index(key, "*"); idx != -1 {
|
||||
baseName := key[:idx]
|
||||
if continuation == nil {
|
||||
continuation = make(map[string]map[string]string)
|
||||
}
|
||||
var ok bool
|
||||
if pmap, ok = continuation[baseName]; !ok {
|
||||
continuation[baseName] = make(map[string]string)
|
||||
pmap = continuation[baseName]
|
||||
}
|
||||
if isFirstContinuation(key) {
|
||||
charset, _, err := get2231Charset(value)
|
||||
if err != nil {
|
||||
log.Errorln("Filter params:", err)
|
||||
continue
|
||||
}
|
||||
if charset != "utf-8" && charset != "us-ascii" {
|
||||
keepOrig = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, exists := pmap[key]; exists {
|
||||
keepOrig = false
|
||||
}
|
||||
pmap[key] = value
|
||||
v = rest
|
||||
}
|
||||
|
||||
if keepOrig {
|
||||
return
|
||||
}
|
||||
|
||||
if continuation != nil {
|
||||
for paramKey, contMap := range continuation {
|
||||
value, err := mergeContinuations(paramKey, contMap)
|
||||
if err == nil {
|
||||
params[paramKey+"*"] = value
|
||||
continue
|
||||
}
|
||||
|
||||
// Fallback.
|
||||
log.Errorln("Merge param", paramKey, ":", err)
|
||||
for ck, cv := range contMap {
|
||||
params[ck] = cv
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge ;
|
||||
out = mediatype
|
||||
for k, v := range params {
|
||||
out += ";"
|
||||
out += k
|
||||
out += "="
|
||||
out += v
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func isFirstContinuation(key string) bool {
|
||||
if idx := strings.Index(key, "*"); idx != -1 {
|
||||
return key[idx:] == "*" || key[idx:] == "*0*"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// get2231Charset partially from mime/mediatype.go:211 function `decode2231Enc`.
|
||||
func get2231Charset(v string) (charset, value string, err error) {
|
||||
sv := strings.SplitN(v, "'", 3)
|
||||
if len(sv) != 3 {
|
||||
err = errors.New("incorrect RFC2231 charset format")
|
||||
return
|
||||
}
|
||||
charset = strings.ToLower(sv[0])
|
||||
value = sv[2]
|
||||
return
|
||||
}
|
||||
|
||||
func mergeContinuations(paramKey string, contMap map[string]string) (string, error) {
|
||||
var err error
|
||||
var charset, value string
|
||||
|
||||
// Single value.
|
||||
if contValue, ok := contMap[paramKey+"*"]; ok {
|
||||
if charset, value, err = get2231Charset(contValue); err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
for n := 0; ; n++ {
|
||||
contKey := fmt.Sprintf("%s*%d", paramKey, n)
|
||||
contValue, isLast := contMap[contKey]
|
||||
if !isLast {
|
||||
var ok bool
|
||||
contValue, ok = contMap[contKey+"*"]
|
||||
if !ok {
|
||||
return "", errors.New("not valid RFC2231 continuation")
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
if charset, value, err = get2231Charset(contValue); err != nil || charset == "" {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
value += contValue
|
||||
}
|
||||
if isLast {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return convertHexToUTF(charset, value)
|
||||
}
|
||||
|
||||
// convertHexToUTF converts hex values string with charset to UTF8 in RFC2231 format.
|
||||
func convertHexToUTF(charset, value string) (string, error) {
|
||||
raw, err := percentHexUnescape(value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
utf8, err := DecodeCharset(raw, map[string]string{"charset": charset})
|
||||
return "utf-8''" + percentHexEscape(utf8), err
|
||||
}
|
||||
|
||||
// consumeMediaParam copy paste mime/mediatype.go:297.
|
||||
func consumeMediaParam(v string) (param, value, rest string) {
|
||||
rest = strings.TrimLeftFunc(v, unicode.IsSpace)
|
||||
if !strings.HasPrefix(rest, ";") {
|
||||
return "", "", v
|
||||
}
|
||||
|
||||
rest = rest[1:] // Consume semicolon.
|
||||
rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
|
||||
param, rest = consumeToken(rest)
|
||||
param = strings.ToLower(param)
|
||||
if param == "" {
|
||||
return "", "", v
|
||||
}
|
||||
|
||||
rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
|
||||
if !strings.HasPrefix(rest, "=") {
|
||||
return "", "", v
|
||||
}
|
||||
rest = rest[1:] // Consume equals sign.
|
||||
rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
|
||||
value, rest2 := consumeValue(rest)
|
||||
if value == "" && rest2 == rest {
|
||||
return "", "", v
|
||||
}
|
||||
rest = rest2
|
||||
return param, value, rest
|
||||
}
|
||||
|
||||
// consumeToken copy paste mime/mediatype.go:238.
|
||||
// consumeToken consumes a token from the beginning of the provided string,
|
||||
// per RFC 2045 section 5.1 (referenced from 2183), and returns
|
||||
// the token consumed and the rest of the string.
|
||||
// Returns ("", v) on failure to consume at least one character.
|
||||
func consumeToken(v string) (token, rest string) {
|
||||
notPos := strings.IndexFunc(v, isNotTokenChar)
|
||||
if notPos == -1 {
|
||||
return v, ""
|
||||
}
|
||||
if notPos == 0 {
|
||||
return "", v
|
||||
}
|
||||
return v[0:notPos], v[notPos:]
|
||||
}
|
||||
|
||||
// consumeValue copy paste mime/mediatype.go:253
|
||||
// consumeValue consumes a "value" per RFC 2045, where a value is
|
||||
// either a 'token' or a 'quoted-string'. On success, consumeValue
|
||||
// returns the value consumed (and de-quoted/escaped, if a
|
||||
// quoted-string) and the rest of the string.
|
||||
// On failure, returns ("", v).
|
||||
func consumeValue(v string) (value, rest string) {
|
||||
if v == "" {
|
||||
return
|
||||
}
|
||||
if v[0] != '"' {
|
||||
return consumeToken(v)
|
||||
}
|
||||
|
||||
// parse a quoted-string
|
||||
buffer := new(strings.Builder)
|
||||
for i := 1; i < len(v); i++ {
|
||||
r := v[i]
|
||||
if r == '"' {
|
||||
return buffer.String(), v[i+1:]
|
||||
}
|
||||
// When MSIE sends a full file path (in "intranet mode"), it does not
|
||||
// escape backslashes: "C:\dev\go\foo.txt", not "C:\\dev\\go\\foo.txt".
|
||||
//
|
||||
// No known MIME generators emit unnecessary backslash escapes
|
||||
// for simple token characters like numbers and letters.
|
||||
//
|
||||
// If we see an unnecessary backslash escape, assume it is from MSIE
|
||||
// and intended as a literal backslash. This makes Go servers deal better
|
||||
// with MSIE without affecting the way they handle conforming MIME
|
||||
// generators.
|
||||
if r == '\\' && i+1 < len(v) && !isTokenChar(rune(v[i+1])) {
|
||||
buffer.WriteByte(v[i+1])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if r == '\r' || r == '\n' {
|
||||
return "", v
|
||||
}
|
||||
buffer.WriteByte(v[i])
|
||||
}
|
||||
// Did not find end quote.
|
||||
return "", v
|
||||
}
|
||||
|
||||
// isNotTokenChar copy paste from mime/mediatype.go:234.
|
||||
func isNotTokenChar(r rune) bool {
|
||||
return !isTokenChar(r)
|
||||
}
|
||||
|
||||
// isTokenChar copy paste from mime/grammar.go:19.
|
||||
// isTokenChar reports whether rune is in 'token' as defined by RFC 1521 and RFC 2045.
|
||||
func isTokenChar(r rune) bool {
|
||||
// token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
|
||||
// or tspecials>
|
||||
return r > 0x20 && r < 0x7f && !isTSpecial(r)
|
||||
}
|
||||
|
||||
// isTSpecial copy paste from mime/grammar.go:13
|
||||
// isTSpecial reports whether rune is in 'tspecials' as defined by RFC
|
||||
// 1521 and RFC 2045.
|
||||
func isTSpecial(r rune) bool {
|
||||
return strings.ContainsRune(`()<>@,;:\"/[]?=`, r)
|
||||
}
|
||||
|
||||
func percentHexEscape(raw []byte) (out string) {
|
||||
for _, v := range raw {
|
||||
out += fmt.Sprintf("%%%x", v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// percentHexUnescape copy paste from mime/mediatype.go:325.
|
||||
func percentHexUnescape(s string) ([]byte, error) {
|
||||
// Count %, check that they're well-formed.
|
||||
percents := 0
|
||||
for i := 0; i < len(s); {
|
||||
if s[i] != '%' {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
percents++
|
||||
if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
|
||||
s = s[i:]
|
||||
if len(s) > 3 {
|
||||
s = s[0:3]
|
||||
}
|
||||
return []byte{}, fmt.Errorf("mime: bogus characters after %%: %q", s)
|
||||
}
|
||||
i += 3
|
||||
}
|
||||
if percents == 0 {
|
||||
return []byte(s), nil
|
||||
}
|
||||
|
||||
t := make([]byte, len(s)-2*percents)
|
||||
j := 0
|
||||
for i := 0; i < len(s); {
|
||||
switch s[i] {
|
||||
case '%':
|
||||
t[j] = unhex(s[i+1])<<4 | unhex(s[i+2])
|
||||
j++
|
||||
i += 3
|
||||
default:
|
||||
t[j] = s[i]
|
||||
j++
|
||||
i++
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ishex copy paste from mime/mediatype.go:364.
|
||||
func ishex(c byte) bool {
|
||||
switch {
|
||||
case '0' <= c && c <= '9':
|
||||
return true
|
||||
case 'a' <= c && c <= 'f':
|
||||
return true
|
||||
case 'A' <= c && c <= 'F':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// unhex copy paste from mime/mediatype.go:376.
|
||||
func unhex(c byte) byte {
|
||||
switch {
|
||||
case '0' <= c && c <= '9':
|
||||
return c - '0'
|
||||
case 'a' <= c && c <= 'f':
|
||||
return c - 'a' + 10
|
||||
case 'A' <= c && c <= 'F':
|
||||
return c - 'A' + 10
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user