Skip to content

Commit

Permalink
add Context.ReadMultipartRelated as requested at #1787
Browse files Browse the repository at this point in the history
  • Loading branch information
kataras committed Mar 2, 2022
1 parent 43c85d0 commit f28203f
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 21 deletions.
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ The codebase for Dependency Injection, Internationalization and localization and

## Fixes and Improvements

- Add `Context.ReadMultipartRelated` as requested at: [issues/#1787](https://github.com/kataras/iris/issues/1787).

- Add `Container.DependencyMatcher` and `Dependency.Match` to implement the feature requested at [issues/#1842](https://github.com/kataras/iris/issues/1842).

- Register [CORS middleware](middleware/cors) to the Application by default when `iris.Default()` is used instead of `iris.New()`.
Expand Down
183 changes: 170 additions & 13 deletions context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"io"
"io/ioutil"
"mime"
"mime/multipart"
"net"
"net/http"
Expand Down Expand Up @@ -1783,15 +1784,16 @@ func GetForm(r *http.Request, postMaxMemory int64, resetBody bool) (form map[str
}
}

var bodyCopy []byte

if resetBody {
// on POST, PUT and PATCH it will read the form values from request body otherwise from URL queries.
if m := r.Method; m == "POST" || m == "PUT" || m == "PATCH" {
bodyCopy, _ = GetBody(r, resetBody)
if len(bodyCopy) == 0 {
body, restoreBody, err := GetBody(r, resetBody)
if err != nil {
return nil, false
}
setBody(r, body) // so the ctx.request.Body works
defer restoreBody() // so the next GetForm calls work.

// r.Body = ioutil.NopCloser(io.TeeReader(r.Body, buf))
} else {
resetBody = false
Expand All @@ -1803,9 +1805,9 @@ func GetForm(r *http.Request, postMaxMemory int64, resetBody bool) (form map[str
// After one call to ParseMultipartForm or ParseForm,
// subsequent calls have no effect, are idempotent.
err := r.ParseMultipartForm(postMaxMemory)
if resetBody {
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyCopy))
}
// if resetBody {
// r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyCopy))
// }
if err != nil && err != http.ErrNotMultipart {
return nil, false
}
Expand Down Expand Up @@ -2247,20 +2249,28 @@ func (ctx *Context) SetMaxRequestBodySize(limitOverBytes int64) {
ctx.request.Body = http.MaxBytesReader(ctx.writer, ctx.request.Body, limitOverBytes)
}

var emptyFunc = func() {}

// GetBody reads and returns the request body.
func GetBody(r *http.Request, resetBody bool) ([]byte, error) {
func GetBody(r *http.Request, resetBody bool) ([]byte, func(), error) {
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
return nil, nil, err
}

if resetBody {
// * remember, Request.Body has no Bytes(), we have to consume them first
// and after re-set them to the body, this is the only solution.
r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
return data, func() {
setBody(r, data)
}, nil
}

return data, nil
return data, emptyFunc, nil
}

func setBody(r *http.Request, data []byte) {
r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
}

const disableRequestBodyConsumptionContextKey = "iris.request.body.record"
Expand All @@ -2285,7 +2295,12 @@ func (ctx *Context) IsRecordingBody() bool {
//
// However, whenever you can use the `ctx.Request().Body` instead.
func (ctx *Context) GetBody() ([]byte, error) {
return GetBody(ctx.request, ctx.IsRecordingBody())
body, release, err := GetBody(ctx.request, ctx.IsRecordingBody())
if err != nil {
return nil, err
}
release()
return body, nil
}

// Validator is the validator for request body on Context methods such as
Expand Down Expand Up @@ -2452,10 +2467,11 @@ func (ctx *Context) ReadJSON(outPtr interface{}, opts ...JSONReader) error {
}

if ctx.IsRecordingBody() {
body, err := GetBody(ctx.request, true)
body, restoreBody, err := GetBody(ctx.request, true)
if err != nil {
return err
}
restoreBody()

err = options.unmarshal(ctx.request.Context(), body, outPtr)
if err != nil {
Expand Down Expand Up @@ -2643,6 +2659,143 @@ func (ctx *Context) ReadForm(formObject interface{}) error {
return ctx.app.Validate(formObject)
}

type (
// MultipartRelated is the result of the context.ReadMultipartRelated method.
MultipartRelated struct {
// ContentIDs keeps an ordered list of all the
// content-ids of the multipart related request.
ContentIDs []string
// Contents keeps each part's information by Content-ID.
// Contents holds each of the multipart/related part's data.
Contents map[string]MultipartRelatedContent
}

// MultipartRelatedContent holds a multipart/related part's id, header and body.
MultipartRelatedContent struct {
// ID holds the Content-ID.
ID string
// Headers holds the part's request headers.
Headers map[string][]string
// Body holds the part's body.
Body []byte
}
)

// ReadMultipartRelated returns a structure which contain
// information about each part (id, headers, body).
//
// Read more at: https://www.ietf.org/rfc/rfc2387.txt.
//
// Example request (2387/5.2 Text/X-Okie):
// Content-Type: Multipart/Related; boundary=example-2;
// start="<950118.AEBH@XIson.com>"
// type="Text/x-Okie"
//
// --example-2
// Content-Type: Text/x-Okie; charset=iso-8859-1;
// declaration="<950118.AEB0@XIson.com>"
// Content-ID: <950118.AEBH@XIson.com>
// Content-Description: Document
//
// {doc}
// This picture was taken by an automatic camera mounted ...
// {image file=cid:950118.AECB@XIson.com}
// {para}
// Now this is an enlargement of the area ...
// {image file=cid:950118:AFDH@XIson.com}
// {/doc}
// --example-2
// Content-Type: image/jpeg
// Content-ID: <950118.AFDH@XIson.com>
// Content-Transfer-Encoding: BASE64
// Content-Description: Picture A
//
// [encoded jpeg image]
// --example-2
// Content-Type: image/jpeg
// Content-ID: <950118.AECB@XIson.com>
// Content-Transfer-Encoding: BASE64
// Content-Description: Picture B
//
// [encoded jpeg image]
// --example-2--
func (ctx *Context) ReadMultipartRelated() (MultipartRelated, error) {
contentType, params, err := mime.ParseMediaType(ctx.GetHeader(ContentTypeHeaderKey))
if err != nil {
return MultipartRelated{}, err
}

if !strings.HasPrefix(contentType, ContentMultipartRelatedHeaderValue) {
return MultipartRelated{}, ErrEmptyForm
}

var (
contentIDs []string
contents = make(map[string]MultipartRelatedContent)
)

if ctx.IsRecordingBody() {
// * remember, Request.Body has no Bytes(), we have to consume them first
// and after re-set them to the body, this is the only solution.
body, restoreBody, err := GetBody(ctx.request, true)
if err != nil {
return MultipartRelated{}, fmt.Errorf("multipart related: body copy because of iris.Configuration.DisableBodyConsumptionOnUnmarshal: %w", err)
}
setBody(ctx.request, body) // so the ctx.request.Body works
defer restoreBody() // so the next ctx.GetBody calls work.
}

multipartReader := multipart.NewReader(ctx.request.Body, params["boundary"])
for {
part, err := multipartReader.NextPart()
if err != nil {
if err == io.EOF {
break
}

return MultipartRelated{}, fmt.Errorf("multipart related: next part: %w", err)
}
defer part.Close()

b, err := ioutil.ReadAll(part)
if err != nil {
return MultipartRelated{}, fmt.Errorf("multipart related: next part: read: %w", err)
}

contentID := part.Header.Get("Content-ID")
contentIDs = append(contentIDs, contentID)
contents[contentID] = MultipartRelatedContent{ // replace if same Content-ID appears, which it shouldn't.
ID: contentID,
Headers: http.Header(part.Header),
Body: b,
}
}

if len(contents) != len(contentIDs) {
contentIDs = distinctStrings(contentIDs)
}

result := MultipartRelated{
ContentIDs: contentIDs,
Contents: contents,
}
return result, nil
}

func distinctStrings(values []string) (result []string) {
seen := make(map[string]struct{})

for v := range values {
val := values[v]
if _, ok := seen[val]; !ok {
seen[val] = struct{}{}
result = append(result, val)
}
}

return result
}

// ReadQuery binds URL Query to "ptr". The struct field tag is "url".
//
// Example: https://github.com/kataras/iris/blob/master/_examples/request-body/read-query/main.go
Expand Down Expand Up @@ -2818,6 +2971,8 @@ func (ctx *Context) ReadBody(ptr interface{}) error {
return ctx.ReadYAML(ptr)
case ContentFormHeaderValue, ContentFormMultipartHeaderValue:
return ctx.ReadForm(ptr)
case ContentMultipartRelatedHeaderValue:
return fmt.Errorf("context: read body: cannot bind multipart/related: use ReadMultipartRelated instead")
case ContentJSONHeaderValue:
return ctx.ReadJSON(ptr)
case ContentProtobufHeaderValue:
Expand Down Expand Up @@ -3541,6 +3696,8 @@ const (
ContentFormHeaderValue = "application/x-www-form-urlencoded"
// ContentFormMultipartHeaderValue header value for post multipart form data.
ContentFormMultipartHeaderValue = "multipart/form-data"
// ContentMultipartRelatedHeaderValue header value for multipart related data.
ContentMultipartRelatedHeaderValue = "multipart/related"
// ContentGRPCHeaderValue Content-Type header value for gRPC.
ContentGRPCHeaderValue = "application/grpc"
)
Expand Down
10 changes: 2 additions & 8 deletions core/host/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"fmt"
"io"
"net/http"
"runtime"
"time"

"github.com/kataras/iris/v12/core/netutil"
Expand All @@ -28,13 +27,8 @@ func WriteStartupLogOnServe(w io.Writer) func(TaskHost) {
addr = h.Supervisor.Server.Addr
}
listeningURI := netutil.ResolveURL(guessScheme, addr)
interruptkey := "CTRL"
if runtime.GOOS == "darwin" {
interruptkey = "CMD"
}

_, _ = fmt.Fprintf(w, "Now listening on: %s\nApplication started. Press %s+C to shut down.\n",
listeningURI, interruptkey)
_, _ = fmt.Fprintf(w, "Now listening on: %s\nApplication started. Press CTRL/CMD+C to shut down.\n",
listeningURI)
}
}

Expand Down

0 comments on commit f28203f

Please sign in to comment.