Added parsing of mime messages needed to verify signed messages.
This commit is contained in:
parent
896ad654c8
commit
6604a9bc8c
286
mime/mime.go
Normal file
286
mime/mime.go
Normal file
@ -0,0 +1,286 @@
|
||||
//Package MIME implemets parsing of MIME and MIME/multipart messages
|
||||
//needed to verfiy multipart/signed messages
|
||||
package mime
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
gomime "mime"
|
||||
)
|
||||
|
||||
//A MIME message
|
||||
type MIME struct {
|
||||
headerFld Lines
|
||||
interm Line
|
||||
body Lines
|
||||
}
|
||||
|
||||
//Returns the header of the message
|
||||
func (m *MIME) Header() []byte {
|
||||
return m.headerFld.bytes(nil)
|
||||
}
|
||||
|
||||
//Sets the body of the message
|
||||
func (m *MIME) SetBody(body []byte) {
|
||||
m.body = ParseLines((body))
|
||||
}
|
||||
|
||||
//Gets the body of the message
|
||||
func (m *MIME) Body() []byte {
|
||||
return m.body.bytes(nil)
|
||||
}
|
||||
|
||||
//Gets the full message
|
||||
func (m *MIME) Full() []byte {
|
||||
|
||||
var sep []byte
|
||||
sep = append(sep, m.interm.Line...)
|
||||
sep = append(sep, m.interm.endOfLine...)
|
||||
|
||||
return append(append(m.Header(), sep...), m.Body()...)
|
||||
}
|
||||
|
||||
//Adds a header field to the header of the message
|
||||
func (m *MIME) AddHeaderField(key, value []byte) {
|
||||
|
||||
var field []byte
|
||||
|
||||
field = append(field, key...)
|
||||
field = append(field, []byte(": ")...)
|
||||
field = append(field, value...)
|
||||
|
||||
newField := Line{field, []byte("\n")}
|
||||
|
||||
m.headerFld = append(m.headerFld, newField)
|
||||
|
||||
}
|
||||
|
||||
//Removes a header fild from the header of the message
|
||||
func (m *MIME) DeleteHeaderField(key []byte) {
|
||||
|
||||
for i := len(m.headerFld) - 1; i >= 0; i-- {
|
||||
colonInd := bytes.Index(m.headerFld[i].Line, []byte(":"))
|
||||
k := m.headerFld[i].Line[:colonInd]
|
||||
if bytes.Equal(bytes.ToLower(k), bytes.ToLower(key)) {
|
||||
m.headerFld = append(m.headerFld[:i], m.headerFld[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Gets the header field with the given key
|
||||
func (m *MIME) GetHeaderField(key []byte) (values [][]byte) {
|
||||
|
||||
for i := range m.headerFld {
|
||||
colonInd := bytes.Index(m.headerFld[i].Line, []byte(":"))
|
||||
if colonInd < 1 {
|
||||
fmt.Printf("%q\n", (m.headerFld[i].Line))
|
||||
}
|
||||
k := m.headerFld[i].Line[:colonInd]
|
||||
if bytes.Equal(bytes.ToLower(k), bytes.ToLower(key)) {
|
||||
if colonInd+2 < len(m.headerFld[i].Line) {
|
||||
values = append(values, m.headerFld[i].Line[colonInd+2:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//Sets the header field with the given key
|
||||
func (m *MIME) SetHeaderField(key, value []byte) {
|
||||
|
||||
m.DeleteHeaderField(key)
|
||||
|
||||
var field []byte
|
||||
|
||||
field = append(field, key...)
|
||||
field = append(field, []byte(": ")...)
|
||||
field = append(field, value...)
|
||||
|
||||
newField := Line{field, []byte("\n")}
|
||||
|
||||
m.headerFld = append(m.headerFld, newField)
|
||||
|
||||
}
|
||||
|
||||
//Parses the mediatype of the message
|
||||
func (m *MIME) ParseMediaType() (mediatype string, params map[string]string, err error) {
|
||||
|
||||
contentType := m.GetHeaderField([]byte("Content-Type"))
|
||||
if len(contentType) != 1 {
|
||||
err = errors.New("Multiple or no Content-Type field")
|
||||
return
|
||||
}
|
||||
|
||||
mediatype, params, err = gomime.ParseMediaType(string(m.GetHeaderField([]byte("Content-Type"))[0]))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//Get the parts of a multipart Message
|
||||
func (m *MIME) MultipartGetParts() (parts []Lines, err error) {
|
||||
|
||||
mediaType, params, err := m.ParseMediaType()
|
||||
|
||||
if !strings.HasPrefix(mediaType, "multipart/") {
|
||||
err = errors.New("Message is not multipart")
|
||||
return
|
||||
}
|
||||
|
||||
boundary := params["boundary"]
|
||||
if boundary == "" {
|
||||
err = errors.New("Mulitpart message has no boundary")
|
||||
return
|
||||
}
|
||||
|
||||
var boundaryIndex []int
|
||||
|
||||
for i := range m.body {
|
||||
if strings.HasPrefix(string(m.body[i].Line), "--"+boundary) {
|
||||
boundaryIndex = append(boundaryIndex, i)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(string(m.body[i].Line), "--"+boundary+"--") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(boundaryIndex)-1; i++ {
|
||||
part := make([]Line, boundaryIndex[i+1]-(boundaryIndex[i]+1))
|
||||
copy(part, m.body[boundaryIndex[i]+1:boundaryIndex[i+1]])
|
||||
parts = append(parts, part) //m.body[boundaryIndex[i]+1:boundaryIndex[i+1]])
|
||||
}
|
||||
|
||||
for i := range parts {
|
||||
parts[i][len(parts[i])-1] = Line{parts[i][len(parts[i])-1].Line, nil}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
CRLF = []byte("\r\n")
|
||||
CR = []byte("\r")
|
||||
LF = []byte("\n")
|
||||
|
||||
SPACE = []byte(" ")[0]
|
||||
HTAB = []byte("\t")[0]
|
||||
)
|
||||
|
||||
//Parses a MIME message
|
||||
func Parse(raw []byte) (m MIME) {
|
||||
|
||||
rawLines := ParseLines(raw)
|
||||
|
||||
return parseMIME(rawLines)
|
||||
}
|
||||
|
||||
func parseMIME(rawLines Lines) (m MIME) {
|
||||
var headerField Line
|
||||
|
||||
for i := range rawLines {
|
||||
|
||||
if len(rawLines[i].Line) > 0 && isContinuedLine(rawLines[i].Line) && i > 0 && !isEmpty(rawLines[i].Line) {
|
||||
headerField.Line = append(headerField.Line, headerField.endOfLine...)
|
||||
headerField.Line = append(headerField.Line, rawLines[i].Line...)
|
||||
headerField.endOfLine = rawLines[i].endOfLine
|
||||
} else {
|
||||
|
||||
if i > 0 {
|
||||
m.headerFld = append(m.headerFld, headerField)
|
||||
}
|
||||
|
||||
//Next headerField
|
||||
headerField = rawLines[i]
|
||||
}
|
||||
|
||||
// Empty line seprates header and body
|
||||
if isEmpty(rawLines[i].Line) {
|
||||
m.interm = rawLines[i]
|
||||
m.body = rawLines[i+1:]
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//Line of a MIME message
|
||||
type Line struct {
|
||||
Line []byte
|
||||
endOfLine []byte
|
||||
}
|
||||
|
||||
//Multiple lines, needed for body and header
|
||||
type Lines []Line
|
||||
|
||||
//Parsing linebreaks
|
||||
func ParseLines(raw []byte) (lines Lines) {
|
||||
|
||||
oneLine := Line{raw, nil}
|
||||
lines = Lines{oneLine}
|
||||
|
||||
lines = lines.splitLine(CRLF)
|
||||
lines = lines.splitLine(CR)
|
||||
lines = lines.splitLine(LF)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l Lines) splitLine(sep []byte) (newL Lines) {
|
||||
newL = Lines{}
|
||||
for _, line := range l {
|
||||
split := bytes.Split(line.Line, sep)
|
||||
if len(split) > 1 {
|
||||
for i := 0; i < len(split)-1; i++ {
|
||||
newL = append(newL, Line{split[i], sep})
|
||||
}
|
||||
newL = append(newL, Line{split[len(split)-1], nil})
|
||||
} else {
|
||||
newL = append(newL, line)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//Gives the bytes of the Lines with given linebreak. (e.g. for signed S/MIME use Bytes(mime.CRLF))
|
||||
func (l Lines) Bytes(sep []byte) (raw []byte) {
|
||||
return l.bytes(sep)
|
||||
}
|
||||
|
||||
func (l Lines) bytes(sep []byte) (raw []byte) {
|
||||
|
||||
for i := range l {
|
||||
raw = append(raw, l[i].Line...)
|
||||
if len(l[i].endOfLine) != 0 && sep != nil {
|
||||
raw = append(raw, sep...)
|
||||
} else {
|
||||
raw = append(raw, l[i].endOfLine...)
|
||||
}
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
//Header field folding https://tools.ietf.org/html/rfc822#section-3.1.1
|
||||
func isEmpty(s []byte) bool {
|
||||
return len(bytes.TrimSpace(s)) == 0
|
||||
}
|
||||
|
||||
func isContinuedLine(s []byte) bool {
|
||||
if !(s[0] != ' ' && s[0] != '\t') {
|
||||
return true
|
||||
}
|
||||
|
||||
if unicode.IsSpace(bytes.Runes(s[:2])[0]) {
|
||||
fmt.Println("Continued Line with unicode space found.")
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
77
mime/mime_test.go
Normal file
77
mime/mime_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
package mime
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParsing(t *testing.T) {
|
||||
|
||||
mail := Parse([]byte(sampleMsg))
|
||||
|
||||
full := mail.Full()
|
||||
|
||||
if !bytes.Equal(full, []byte(sampleMsg)) {
|
||||
t.Error("Parsing mail changes the content!")
|
||||
}
|
||||
|
||||
mediaType, _, err := mail.ParseMediaType()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if strings.Compare(mediaType, "multipart/signed") != 0 {
|
||||
t.Error("Mediatype parsed not corretly!")
|
||||
}
|
||||
|
||||
parts, err := mail.MultipartGetParts()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
for i := range parts {
|
||||
if !bytes.Equal(parts[i].Bytes(nil), msgParts[i]) {
|
||||
t.Fatal("Parts not corretly parsed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//3.4.3.3. Sample multipart/signed Message https://tools.ietf.org/html/rfc5751#section-3.4
|
||||
const sampleMsg = `Content-Type: multipart/signed;
|
||||
protocol="application/pkcs7-signature";
|
||||
micalg=sha1; boundary=boundary42
|
||||
|
||||
--boundary42
|
||||
Content-Type: text/plain
|
||||
|
||||
This is a clear-signed message.
|
||||
|
||||
--boundary42
|
||||
Content-Type: application/pkcs7-signature; name=smime.p7s
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment; filename=smime.p7s
|
||||
|
||||
ghyHhHUujhJhjH77n8HHGTrfvbnj756tbB9HG4VQpfyF467GhIGfHfYT6
|
||||
4VQpfyF467GhIGfHfYT6jH77n8HHGghyHhHUujhJh756tbB9HGTrfvbnj
|
||||
n8HHGTrfvhJhjH776tbB9HG4VQbnj7567GhIGfHfYT6ghyHhHUujpfyF4
|
||||
7GhIGfHfYT64VQbnj756
|
||||
|
||||
--boundary42--`
|
||||
|
||||
var msgParts = [][]byte{[]byte(part1), []byte(part2)}
|
||||
|
||||
const part1 = `Content-Type: text/plain
|
||||
|
||||
This is a clear-signed message.
|
||||
`
|
||||
|
||||
const part2 = `Content-Type: application/pkcs7-signature; name=smime.p7s
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment; filename=smime.p7s
|
||||
|
||||
ghyHhHUujhJhjH77n8HHGTrfvbnj756tbB9HG4VQpfyF467GhIGfHfYT6
|
||||
4VQpfyF467GhIGfHfYT6jH77n8HHGghyHhHUujhJh756tbB9HGTrfvbnj
|
||||
n8HHGTrfvhJhjH776tbB9HG4VQbnj7567GhIGfHfYT6ghyHhHUujpfyF4
|
||||
7GhIGfHfYT64VQbnj756
|
||||
`
|
Loading…
Reference in New Issue
Block a user