-
Notifications
You must be signed in to change notification settings - Fork 1k
/
Copy pathmime.go
121 lines (108 loc) · 3.37 KB
/
mime.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mime
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"mime"
"mime/multipart"
"net/mail"
"net/textproto"
"strings"
admapi "github.com/awslabs/amazon-eks-ami/nodeadm/api"
)
type ContentType string
const (
boundary = "//"
versionHeader = "MIME-Version: 1.0"
ContentTypeShellScript ContentType = `text/x-shellscript; charset="us-ascii"`
ContentTypeNodeConfig ContentType = "application/" + admapi.GroupName
ContentTypeMultipart ContentType = `multipart/mixed; boundary="` + boundary + `"`
)
type Entry struct {
ContentType ContentType
Content string
}
type Archive []Entry
func NewArchive(content string) (Archive, error) {
archive := Archive{}
if content == "" {
return archive, nil
}
reader, err := archive.getReader(content)
if err != nil {
return nil, err
}
for {
p, err := reader.NextPart()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("parsing content, %w", err)
}
slurp, err := io.ReadAll(p)
if err != nil {
return nil, fmt.Errorf("parsing content, %s, %w", string(slurp), err)
}
archive = append(archive, Entry{
ContentType: ContentType(p.Header.Get("Content-Type")),
Content: string(slurp),
})
}
return archive, nil
}
// Serialize returns a base64 encoded serialized MIME multi-part archive
func (ma Archive) Serialize() (string, error) {
buffer := bytes.Buffer{}
writer := multipart.NewWriter(&buffer)
if err := writer.SetBoundary(boundary); err != nil {
return "", err
}
buffer.WriteString(versionHeader + "\n")
buffer.WriteString(fmt.Sprintf("Content-Type: %s\n\n", ContentTypeMultipart))
for _, entry := range ma {
partWriter, err := writer.CreatePart(textproto.MIMEHeader{
"Content-Type": []string{string(entry.ContentType)},
})
if err != nil {
return "", fmt.Errorf("creating multi-part section for entry, %w", err)
}
_, err = partWriter.Write([]byte(entry.Content))
if err != nil {
return "", fmt.Errorf("writing entry, %w", err)
}
}
if err := writer.Close(); err != nil {
return "", fmt.Errorf("terminating multi-part archive, %w", err)
}
// The mime/multipart package adds carriage returns, while the rest of our logic does not. Remove all
// carriage returns for consistency.
return base64.StdEncoding.EncodeToString([]byte(strings.ReplaceAll(buffer.String(), "\r", ""))), nil
}
func (Archive) getReader(content string) (*multipart.Reader, error) {
mailMsg, err := mail.ReadMessage(strings.NewReader(content))
if err != nil {
return nil, err
}
mediaType, params, err := mime.ParseMediaType(mailMsg.Header.Get("Content-Type"))
if err != nil {
return nil, fmt.Errorf("archive doesn't have Content-Type header, %w", err)
}
if !strings.HasPrefix(mediaType, "multipart/") {
return nil, fmt.Errorf("archive is not in multipart format, %w", err)
}
return multipart.NewReader(mailMsg.Body, params["boundary"]), nil
}