Skip to content

Commit

Permalink
Add multipart parser
Browse files Browse the repository at this point in the history
Signed-off-by: Gabriel Adrian Samfira <[email protected]>
  • Loading branch information
gabriel-samfira committed Jun 7, 2023
1 parent b6c2994 commit 51f8153
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 14 deletions.
20 changes: 10 additions & 10 deletions kola/tests/misc/cloudinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ import (
"github.com/flatcar/mantle/platform/conf"
)

var multipartMimeUserdata = `Content-Type: multipart/mixed; boundary="===============1598784645116016685=="
var multipartMimeUserdata = `Content-Type: multipart/mixed; boundary="MIMEMULTIPART"
MIME-Version: 1.0
--===============1598784645116016685==
--MIMEMULTIPART
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Expand All @@ -35,15 +35,15 @@ Content-Disposition: attachment; filename="cloud-config"
ssh_authorized_keys:
- ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEftQIHTRvUmyDCN7VGve4srz03Jmq6rPnqq+XMHMQUIL9c/b0l7B5tWfQvQecKyLte94HOPzAyMJlktWTVGQnY=
--===============1598784645116016685==
--MIMEMULTIPART
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config"
hostname: "example"
--===============1598784645116016685==
--MIMEMULTIPART
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Expand Down Expand Up @@ -81,7 +81,7 @@ write_files:
path: /tmp/kola_gzip_base64_2
permissions: '0644'
--===============1598784645116016685==
--MIMEMULTIPART
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Expand All @@ -90,23 +90,23 @@ Content-Disposition: attachment; filename="create_file.ps1"
#!/bin/sh
touch /coreos-cloudinit_multipart.txt
--===============1598784645116016685==
--MIMEMULTIPART
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config"
#test_to_check_if_cloud_config_can_contain_a_comment
--===============1598784645116016685==
--MIMEMULTIPART
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="some_text.txt"
This is just some random text.
--===============1598784645116016685==
--MIMEMULTIPART
Content-Type: application/json; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Expand All @@ -127,7 +127,7 @@ Content-Disposition: attachment; filename="ignition.txt"
}
}
--===============1598784645116016685==
--MIMEMULTIPART
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Expand All @@ -141,7 +141,7 @@ write_files:
path: /kola_undercover
permissions: '0644'
--===============1598784645116016685==--
--MIMEMULTIPART--
`

func init() {
Expand Down
38 changes: 34 additions & 4 deletions platform/conf/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"net/textproto"
"net/url"
"os"
"reflect"
Expand Down Expand Up @@ -52,6 +53,7 @@ import (
ignvalidate "github.com/flatcar/ignition/config/validate"
"github.com/vincent-petithory/dataurl"
"golang.org/x/crypto/ssh/agent"
"gopkg.in/yaml.v3"
)

type kind int
Expand Down Expand Up @@ -92,7 +94,7 @@ type Conf struct {
ignitionV33 *v33types.Config
cloudconfig *cci.CloudConfig
script string
multipartMime string
multipartMime *MultipartUserdata
user string
}

Expand Down Expand Up @@ -303,7 +305,11 @@ func (u *UserData) Render(ctPlatform string) (*Conf, error) {
// pass through scripts unmodified, you are on your own.
c.script = u.data
case kindMultipartMime:
c.multipartMime = u.data
data, err := NewMultipartUserdata(u.data)
if err != nil {
return nil, err
}
c.multipartMime = data
case kindIgnition:
err := renderIgnition()
if err != nil {
Expand Down Expand Up @@ -401,8 +407,9 @@ func (c *Conf) String() string {
return c.cloudconfig.String()
} else if c.script != "" {
return c.script
} else if c.multipartMime != "" {
return c.multipartMime
} else if c.multipartMime != nil {
data, _ := c.multipartMime.Serialize()
return data
}

return ""
Expand Down Expand Up @@ -1260,6 +1267,27 @@ func (c *Conf) copyKeysScript(keys []*agent.Key) {
c.script = strings.Replace(c.script, "@SSH_KEYS@", keyString, -1)
}

func (c *Conf) copyKeysMultipartMime(keys []*agent.Key) {
keysAsStrings := keysToStrings(keys)
header := textproto.MIMEHeader{
"Content-Type": []string{"text/plain; charset=\"us-ascii\""},
"MIME-Version": []string{"1.0"},
"Content-Transfer-Encoding": []string{"7bit"},
"Content-Disposition": []string{"attachment; filename=\"testing-keys.yaml\""},
}

udata := map[string][]string{
"ssh_authorized_keys": keysAsStrings,
}

asYaml, err := yaml.Marshal(udata)
if err != nil {
plog.Errorf("failed to marshal yaml: %v", err)
return
}
c.multipartMime.AddPart(header, asYaml)
}

// CopyKeys copies public keys from agent ag into the configuration to the
// appropriate configuration section for the core user.
func (c *Conf) CopyKeys(keys []*agent.Key) {
Expand All @@ -1285,6 +1313,8 @@ func (c *Conf) CopyKeys(keys []*agent.Key) {
c.copyKeysCloudConfig(keys)
} else if c.script != "" {
c.copyKeysScript(keys)
} else if c.multipartMime != nil {
c.copyKeysMultipartMime(keys)
}
}

Expand Down
179 changes: 179 additions & 0 deletions platform/conf/multipart.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package conf

import (
"bytes"
"errors"
"fmt"
"io"
"mime"
"mime/multipart"
"net/mail"
"net/textproto"
"strings"
)

func NewMultipartUserdata(data string) (*MultipartUserdata, error) {
m, err := mail.ReadMessage(strings.NewReader(data))
if err != nil {
return nil, fmt.Errorf("error parsing multipart MIME: %w", err)
}
parts, hdrInfo, err := messageToPartsAndHeaderInfo(m)
if err != nil {
return nil, fmt.Errorf("error parsing multipart MIME: %w", err)
}

buf := &bytes.Buffer{}
mpWr := multipart.NewWriter(buf)
mpWr.SetBoundary(hdrInfo.params["boundary"])

for _, part := range parts {
partWr, err := mpWr.CreatePart(part.header)
if err != nil {
return nil, err
}
_, err = io.Copy(partWr, part.body)
if err != nil {
return nil, err
}
}

mpMsg := &MultipartUserdata{
header: hdrInfo,
parts: parts,

writer: mpWr,
newMultipart: buf,
}

return mpMsg, nil
}

type MultipartUserdata struct {
// Using the header and parts we can reconstitute the original MIME message
header headerInfo
parts []partInfo

// We import the parts into a multipart.Writer to allow adding new parts
writer *multipart.Writer
newMultipart *bytes.Buffer
}

func (m *MultipartUserdata) AddPart(header textproto.MIMEHeader, body []byte) error {
if m.writer == nil {
return errors.New("cannot add part to read-only multipart")
}
partWr, err := m.writer.CreatePart(header)
if err != nil {
return err
}
_, err = io.Copy(partWr, bytes.NewReader(body))
if err != nil {
return err
}
return nil
}

func (m *MultipartUserdata) Serialize() (string, error) {
if m.writer == nil {
return "", errors.New("cannot serialize read-only multipart")
}

err := m.writer.Close()
if err != nil {
return "", err
}
asStr := &bytes.Buffer{}
for k, v := range m.header.origHeader {
asStr.Write([]byte(fmt.Sprintf("%s: %s\n", k, v[0])))
}
asStr.Write([]byte("\n"))

_, err = io.Copy(asStr, m.newMultipart)
if err != nil {
return "", err
}
return asStr.String(), nil
}

func parseMimeHeader(header mail.Header) (headerInfo, error) {
contentType := header.Get("Content-Type")
if contentType == "" {
return headerInfo{}, errors.New("no Content-Type header found")
}

mediaType, params, err := mime.ParseMediaType(contentType)
if err != nil {
return headerInfo{}, fmt.Errorf("error parsing header: %w", err)
}
partTransferEncoding := header.Get("Content-Transfer-Encoding")

var contentDisposition string
var fileName string
contentDispositionHeader := header.Get("Content-Disposition")
if contentDispositionHeader != "" {
mediaType, params, err := mime.ParseMediaType(contentDispositionHeader)
if err != nil {
return headerInfo{}, fmt.Errorf("error parsing header: %w", err)
}
contentDisposition = mediaType
fileName = params["filename"]
}
return headerInfo{
mediaType: mediaType,
params: params,
transferEncoding: partTransferEncoding,
contentDisposition: contentDisposition,
fileName: fileName,
origHeader: header,
}, nil
}

type headerInfo struct {
mediaType string
params map[string]string
fileName string
contentDisposition string
transferEncoding string
origHeader mail.Header
}

type partInfo struct {
header textproto.MIMEHeader
body *bytes.Buffer
}

func messageToPartsAndHeaderInfo(m *mail.Message) ([]partInfo, headerInfo, error) {
hdrInfo, err := parseMimeHeader(m.Header)
if err != nil {
return nil, headerInfo{}, fmt.Errorf("error parsing MIME header: %w", err)
}

boundary, ok := hdrInfo.params["boundary"]
if !ok {
return nil, headerInfo{}, errors.New("no boundary found in MIME header")
}

multipartReader := multipart.NewReader(m.Body, boundary)

parts := []partInfo{}
for {
part, err := multipartReader.NextPart()
if err == io.EOF {
break
}
if err != nil {
return nil, headerInfo{}, fmt.Errorf("error reading part: %w", err)
}
partHeader := part.Header
partBody := &bytes.Buffer{}
_, err = io.Copy(partBody, part)
if err != nil {
return nil, headerInfo{}, fmt.Errorf("error reading part: %w", err)
}
parts = append(parts, partInfo{
header: partHeader,
body: partBody,
})
}
return parts, hdrInfo, nil
}

0 comments on commit 51f8153

Please sign in to comment.