| // 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 multipart |
| |
| import ( |
| "bytes" |
| "crypto/rand" |
| "errors" |
| "fmt" |
| "io" |
| "maps" |
| "net/textproto" |
| "slices" |
| "strings" |
| ) |
| |
| // A Writer generates multipart messages. |
| type Writer struct { |
| w io.Writer |
| boundary string |
| lastpart *part |
| } |
| |
| // NewWriter returns a new multipart [Writer] with a random boundary, |
| // writing to w. |
| func NewWriter(w io.Writer) *Writer { |
| return &Writer{ |
| w: w, |
| boundary: randomBoundary(), |
| } |
| } |
| |
| // Boundary returns the [Writer]'s boundary. |
| func (w *Writer) Boundary() string { |
| return w.boundary |
| } |
| |
| // SetBoundary overrides the [Writer]'s default randomly-generated |
| // boundary separator with an explicit value. |
| // |
| // SetBoundary must be called before any parts are created, may only |
| // contain certain ASCII characters, and must be non-empty and |
| // at most 70 bytes long. |
| func (w *Writer) SetBoundary(boundary string) error { |
| if w.lastpart != nil { |
| return errors.New("mime: SetBoundary called after write") |
| } |
| // rfc2046#section-5.1.1 |
| if len(boundary) < 1 || len(boundary) > 70 { |
| return errors.New("mime: invalid boundary length") |
| } |
| end := len(boundary) - 1 |
| for i, b := range boundary { |
| if 'A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' || '0' <= b && b <= '9' { |
| continue |
| } |
| switch b { |
| case '\'', '(', ')', '+', '_', ',', '-', '.', '/', ':', '=', '?': |
| continue |
| case ' ': |
| if i != end { |
| continue |
| } |
| } |
| return errors.New("mime: invalid boundary character") |
| } |
| w.boundary = boundary |
| return nil |
| } |
| |
| // FormDataContentType returns the Content-Type for an HTTP |
| // multipart/form-data with this [Writer]'s Boundary. |
| func (w *Writer) FormDataContentType() string { |
| b := w.boundary |
| // We must quote the boundary if it contains any of the |
| // tspecials characters defined by RFC 2045, or space. |
| if strings.ContainsAny(b, `()<>@,;:\"/[]?= `) { |
| b = `"` + b + `"` |
| } |
| return "multipart/form-data; boundary=" + b |
| } |
| |
| func randomBoundary() string { |
| var buf [30]byte |
| _, err := io.ReadFull(rand.Reader, buf[:]) |
| if err != nil { |
| panic(err) |
| } |
| return fmt.Sprintf("%x", buf[:]) |
| } |
| |
| // CreatePart creates a new multipart section with the provided |
| // header. The body of the part should be written to the returned |
| // [Writer]. After calling CreatePart, any previous part may no longer |
| // be written to. |
| func (w *Writer) CreatePart(header textproto.MIMEHeader) (io.Writer, error) { |
| if w.lastpart != nil { |
| if err := w.lastpart.close(); err != nil { |
| return nil, err |
| } |
| } |
| var b bytes.Buffer |
| if w.lastpart != nil { |
| fmt.Fprintf(&b, "\r\n--%s\r\n", w.boundary) |
| } else { |
| fmt.Fprintf(&b, "--%s\r\n", w.boundary) |
| } |
| |
| for _, k := range slices.Sorted(maps.Keys(header)) { |
| for _, v := range header[k] { |
| fmt.Fprintf(&b, "%s: %s\r\n", k, v) |
| } |
| } |
| fmt.Fprintf(&b, "\r\n") |
| _, err := io.Copy(w.w, &b) |
| if err != nil { |
| return nil, err |
| } |
| p := &part{ |
| mw: w, |
| } |
| w.lastpart = p |
| return p, nil |
| } |
| |
| var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") |
| |
| func escapeQuotes(s string) string { |
| return quoteEscaper.Replace(s) |
| } |
| |
| // CreateFormFile is a convenience wrapper around [Writer.CreatePart]. It creates |
| // a new form-data header with the provided field name and file name. |
| func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error) { |
| h := make(textproto.MIMEHeader) |
| h.Set("Content-Disposition", |
| fmt.Sprintf(`form-data; name="%s"; filename="%s"`, |
| escapeQuotes(fieldname), escapeQuotes(filename))) |
| h.Set("Content-Type", "application/octet-stream") |
| return w.CreatePart(h) |
| } |
| |
| // CreateFormField calls [Writer.CreatePart] with a header using the |
| // given field name. |
| func (w *Writer) CreateFormField(fieldname string) (io.Writer, error) { |
| h := make(textproto.MIMEHeader) |
| h.Set("Content-Disposition", |
| fmt.Sprintf(`form-data; name="%s"`, escapeQuotes(fieldname))) |
| return w.CreatePart(h) |
| } |
| |
| // WriteField calls [Writer.CreateFormField] and then writes the given value. |
| func (w *Writer) WriteField(fieldname, value string) error { |
| p, err := w.CreateFormField(fieldname) |
| if err != nil { |
| return err |
| } |
| _, err = p.Write([]byte(value)) |
| return err |
| } |
| |
| // Close finishes the multipart message and writes the trailing |
| // boundary end line to the output. |
| func (w *Writer) Close() error { |
| if w.lastpart != nil { |
| if err := w.lastpart.close(); err != nil { |
| return err |
| } |
| w.lastpart = nil |
| } |
| _, err := fmt.Fprintf(w.w, "\r\n--%s--\r\n", w.boundary) |
| return err |
| } |
| |
| type part struct { |
| mw *Writer |
| closed bool |
| we error // last error that occurred writing |
| } |
| |
| func (p *part) close() error { |
| p.closed = true |
| return p.we |
| } |
| |
| func (p *part) Write(d []byte) (n int, err error) { |
| if p.closed { |
| return 0, errors.New("multipart: can't write to finished part") |
| } |
| n, err = p.mw.w.Write(d) |
| if err != nil { |
| p.we = err |
| } |
| return |
| } |