1defmodule Plug.Parsers do 2 defmodule RequestTooLargeError do 3 @moduledoc """ 4 Error raised when the request is too large. 5 """ 6 7 defexception message: "the request is too large. If you are willing to process " <> 8 "larger requests, please give a :length to Plug.Parsers", 9 plug_status: 413 10 end 11 12 defmodule UnsupportedMediaTypeError do 13 @moduledoc """ 14 Error raised when the request body cannot be parsed. 15 """ 16 17 defexception media_type: nil, plug_status: 415 18 19 def message(exception) do 20 "unsupported media type #{exception.media_type}" 21 end 22 end 23 24 defmodule BadEncodingError do 25 @moduledoc """ 26 Raised when the request body contains bad encoding. 27 """ 28 29 defexception message: nil, plug_status: 415 30 end 31 32 defmodule ParseError do 33 @moduledoc """ 34 Error raised when the request body is malformed. 35 """ 36 37 defexception exception: nil, plug_status: 400 38 39 def message(%{exception: exception}) do 40 "malformed request, a #{inspect exception.__struct__} exception was raised " <> 41 "with message #{inspect(Exception.message(exception))}" 42 end 43 end 44 45 @moduledoc """ 46 A plug for parsing the request body. 47 48 This module also specifies a behaviour that all the parsers to be used with 49 Plug should adopt. 50 51 This plug also fetches query params in the connection through 52 `Plug.Conn.fetch_query_params/2`. 53 54 Once a connection goes through this plug, it will have `:body_params` set to 55 the map of params parsed by one of the parsers listed in `:parsers` and 56 `:params` set to the result of merging the `:body_params` and `:query_params`. 57 58 This plug will raise `Plug.Parsers.UnsupportedMediaTypeError` by default if 59 the request cannot be parsed by any of the given types and the MIME type has 60 not been explicity accepted with the `:pass` option. 61 62 `Plug.Parsers.RequestTooLargeError` will be raised if the request goes over 63 the given limit. 64 65 Parsers may raise a `Plug.Parsers.ParseError` if the request has a malformed 66 body. 67 68 This plug only parses the body if the request method is one of the following: 69 70 * `POST` 71 * `PUT` 72 * `PATCH` 73 * `DELETE` 74 75 For requests with a different request method, this plug will only fetch the 76 query params. 77 78 ## Options 79 80 * `:parsers` - a list of modules to be invoked for parsing. 81 These modules need to implement the behaviour outlined in 82 this module. 83 84 * `:pass` - an optional list of MIME type strings that are allowed 85 to pass through. Any mime not handled by a parser and not explicitly 86 listed in `:pass` will `raise UnsupportedMediaTypeError`. For example: 87 88 * `["*/*"]` - never raises 89 * `["text/html", "application/*"]` - doesn't raise for those values 90 * `[]` - always raises (default) 91 92 * `:query_string_length` - the maximum allowed size for query strings 93 94 ## Examples 95 96 plug Plug.Parsers, parsers: [:urlencoded, :multipart] 97 98 plug Plug.Parsers, parsers: [:urlencoded, :json], 99 pass: ["text/*"], 100 json_decoder: Poison 101 102 Each parser also accepts options to be given directly to it by using tuples. 103 For example, to support file uploads it is common pass the `:length`, 104 `:read_length` and `:read_timeout` option to the multipart parser: 105 106 plug Plug.Parsers, 107 parsers: [ 108 :url_encoded, 109 {:multipart, length: 20_000_000} # Increase to 20MB max upload 110 ] 111 112 ## Built-in parsers 113 114 Plug ships with the following parsers: 115 116 * `Plug.Parsers.URLENCODED` - parses `application/x-www-form-urlencoded` 117 requests (can be used as `:urlencoded` as well in the `:parsers` option) 118 * `Plug.Parsers.MULTIPART` - parses `multipart/form-data` and 119 `multipart/mixed` requests (can be used as `:multipart` as well in the 120 `:parsers` option) 121 * `Plug.Parsers.JSON` - parses `application/json` requests with the given 122 `:json_decoder` (can be used as `:json` as well in the `:parsers` option) 123 124 ## File handling 125 126 If a file is uploaded via any of the parsers, Plug will 127 stream the uploaded contents to a file in a temporary directory in order to 128 avoid loading the whole file into memory. For such, the `:plug` application 129 needs to be started in order for file uploads to work. More details on how the 130 uploaded file is handled can be found in the documentation for `Plug.Upload`. 131 132 When a file is uploaded, the request parameter that identifies that file will 133 be a `Plug.Upload` struct with information about the uploaded file (e.g. 134 filename and content type) and about where the file is stored. 135 136 The temporary directory where files are streamed to can be customized by 137 setting the `PLUG_TMPDIR` environment variable on the host system. If 138 `PLUG_TMPDIR` isn't set, Plug will look at some environment 139 variables which usually hold the value of the system's temporary directory 140 (like `TMPDIR` or `TMP`). If no value is found in any of those variables, 141 `/tmp` is used as a default. 142 """ 143 144 alias Plug.Conn 145 146 @callback init(opts :: Keyword.t) :: Plug.opts 147 148 @doc """ 149 Attempts to parse the connection's request body given the content-type type, 150 subtype, and its parameters. 151 152 The arguments are: 153 154 * the `Plug.Conn` connection 155 * `type`, the content-type type (e.g., `"x-sample"` for the 156 `"x-sample/json"` content-type) 157 * `subtype`, the content-type subtype (e.g., `"json"` for the 158 `"x-sample/json"` content-type) 159 * `params`, the content-type parameters (e.g., `%{"foo" => "bar"}` 160 for the `"text/plain; foo=bar"` content-type) 161 162 This function should return: 163 164 * `{:ok, body_params, conn}` if the parser is able to handle the given 165 content-type; `body_params` should be a map 166 * `{:next, conn}` if the next parser should be invoked 167 * `{:error, :too_large, conn}` if the request goes over the given limit 168 169 """ 170 @callback parse(conn :: Conn.t, type :: binary, subtype :: binary, 171 params :: Keyword.t, state :: term) :: 172 {:ok, Conn.params, Conn.t} | 173 {:error, :too_large, Conn.t} | 174 {:next, Conn.t} 175 176 # TODO: Remove me 177 @optional_callbacks [init: 1] 178 179 @behaviour Plug 180 @methods ~w(POST PUT PATCH DELETE) 181 182 def init(opts) do 183 {parsers, opts} = Keyword.pop(opts, :parsers) 184 {pass, opts} = Keyword.pop(opts, :pass, []) 185 {query_string_length, opts} = Keyword.pop(opts, :query_string_length, 1_000_000) 186 187 unless parsers do 188 raise ArgumentError, "Plug.Parsers expects a set of parsers to be given in :parsers" 189 end 190 191 {convert_parsers(parsers, opts), pass, query_string_length} 192 end 193 194 defp convert_parsers(parsers, root_opts) do 195 for parser <- parsers do 196 {parser, opts} = 197 case parser do 198 {parser, opts} when is_atom(parser) and is_list(opts) -> 199 {parser, Keyword.merge(root_opts, opts)} 200 parser when is_atom(parser) -> 201 {parser, root_opts} 202 end 203 204 module = 205 case Atom.to_string(parser) do 206 "Elixir." <> _ -> parser 207 reference -> Module.concat(Plug.Parsers, String.upcase(reference)) 208 end 209 210 if Code.ensure_compiled?(module) and function_exported?(module, :init, 1) do 211 {module, module.init(opts)} 212 else 213 {module, opts} 214 end 215 end 216 end 217 218 def call(%{method: method, body_params: %Plug.Conn.Unfetched{}} = conn, options) 219 when method in @methods do 220 {parsers, pass, query_string_length} = options 221 %{req_headers: req_headers} = conn 222 conn = Conn.fetch_query_params(conn, length: query_string_length) 223 224 case List.keyfind(req_headers, "content-type", 0) do 225 {"content-type", ct} -> 226 case Conn.Utils.content_type(ct) do 227 {:ok, type, subtype, params} -> 228 reduce(conn, parsers, type, subtype, params, pass, query_string_length) 229 :error -> 230 merge_params(conn, %{}, query_string_length) 231 end 232 nil -> 233 merge_params(conn, %{}, query_string_length) 234 end 235 end 236 237 def call(%{body_params: body_params} = conn, {_, _, query_string_length}) do 238 merge_params(conn, make_empty_if_unfetched(body_params), query_string_length) 239 end 240 241 defp reduce(conn, [{parser, options} | rest], type, subtype, params, pass, query_string_length) do 242 case parser.parse(conn, type, subtype, params, options) do 243 {:ok, body, conn} -> 244 merge_params(conn, body, query_string_length) 245 {:next, conn} -> 246 reduce(conn, rest, type, subtype, params, pass, query_string_length) 247 {:error, :too_large, _conn} -> 248 raise RequestTooLargeError 249 end 250 end 251 252 defp reduce(conn, [], type, subtype, _params, pass, _query_string_length) do 253 ensure_accepted_mimes(conn, type, subtype, pass) 254 end 255 256 defp ensure_accepted_mimes(conn, _type, _subtype, ["*/*"]), do: conn 257 defp ensure_accepted_mimes(conn, type, subtype, pass) do 258 if "#{type}/#{subtype}" in pass || "#{type}/*" in pass do 259 conn 260 else 261 raise UnsupportedMediaTypeError, media_type: "#{type}/#{subtype}" 262 end 263 end 264 265 defp merge_params(conn, body_params, query_string_length) do 266 %{params: params, path_params: path_params} = conn 267 params = make_empty_if_unfetched(params) 268 conn = Plug.Conn.fetch_query_params(conn, length: query_string_length) 269 params = 270 conn.query_params 271 |> Map.merge(params) 272 |> Map.merge(body_params) 273 |> Map.merge(path_params) 274 %{conn | params: params, body_params: body_params} 275 end 276 277 defp make_empty_if_unfetched(%Plug.Conn.Unfetched{}), do: %{} 278 defp make_empty_if_unfetched(params), do: params 279end 280