From 0429bc65e8c4752fb231a5481f5873e478eda7d5 Mon Sep 17 00:00:00 2001 From: Guillaume Poirier-Morency Date: Sat, 6 Feb 2016 22:25:33 -0500 Subject: [PATCH] Middleware to decode content encodings The 'decode' middleware unapply various codings specified in 'Content-Encoding' headers. It's also possible to forward remaining encodings instead of simply raising a '501 Not Implemented'. Provide tests and documentation. --- docs/middlewares/decode.rst | 48 ++++++++++++ docs/middlewares/index.rst | 1 + examples/app/app.vala | 2 + src/valum/valum-decode.vala | 86 +++++++++++++++++++++ tests/decode-test.vala | 144 ++++++++++++++++++++++++++++++++++++ tests/tests.vala | 6 ++ 6 files changed, 287 insertions(+) create mode 100644 docs/middlewares/decode.rst create mode 100644 src/valum/valum-decode.vala create mode 100644 tests/decode-test.vala diff --git a/docs/middlewares/decode.rst b/docs/middlewares/decode.rst new file mode 100644 index 000000000..25c28fd64 --- /dev/null +++ b/docs/middlewares/decode.rst @@ -0,0 +1,48 @@ +Decode +====== + +The ``decode`` middleware is used to unapply various content codings. + +:: + + app.use (decode ()); + + app.post ("/", (req, res) => { + var posted_data = req.flatten_utf8 (); + }); + +It is typically put at the top of an application. + +=============== ========================= +Encoding Action +=============== ========================= +deflate `GLib.ZlibDecompressor`_ +gzip and x-gzip `GLib.ZlibDecompressor`_ +identity nothing +=============== ========================= + +.. _GLib.ZlibDecompressor: http://valadoc.org/#!api=gio-2.0/GLib.ZlibDecompressor + +If an encoding is not supported, a ``501 Not Implemented`` is raised and +remaining encodings are *reapplied* on the request. + +To prevent this behavior, the ``DecodeFlags.FORWARD_REMAINING_ENCODINGS`` flag +can be passed to forward unsupported content codings. + +:: + + app.use (decode (DecodeFlags.FORWARD_REMAINING_ENCODINGS)); + + app.use (() => { + if (req.headers.get_one ("Content-Encoding") == "br") { + req.headers.remove ("Content-Encoding"); + req.convert (new BrotliDecompressor ()); + } + return next (); + }); + + app.post ("/", (req, res) => { + var posted_data = req.flatten_utf8 (); + }); + + diff --git a/docs/middlewares/index.rst b/docs/middlewares/index.rst index e5a580c1a..d4d9b3b40 100644 --- a/docs/middlewares/index.rst +++ b/docs/middlewares/index.rst @@ -3,5 +3,6 @@ Middlewares .. toctree:: basepath + decode server-sent-events subdomain diff --git a/examples/app/app.vala b/examples/app/app.vala index b24633018..4e4015684 100644 --- a/examples/app/app.vala +++ b/examples/app/app.vala @@ -33,6 +33,8 @@ public async void respond_async (VSGI.Request req, VSGI.Response res) throws Err var app = new Router (); +app.use (decode ()); + app.use ((req, res, next) => { res.headers.append ("Server", "Valum/1.0"); HashTable? @params = new HashTable (str_hash, str_equal); diff --git a/src/valum/valum-decode.vala b/src/valum/valum-decode.vala new file mode 100644 index 000000000..96ccf1ffd --- /dev/null +++ b/src/valum/valum-decode.vala @@ -0,0 +1,86 @@ +/* + * This file is part of Valum. + * + * Valum is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * Valum is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Valum. If not, see . + */ + +using GLib; +using VSGI; + +namespace Valum { + + public enum DecodeFlags { + /** + * @since 0.3 + */ + NONE, + /** + * Forward with the remaining content encodings if they are expected to + * be processed later. + * + * @since 0.3 + */ + FORWARD_REMAINING_ENCODINGS + } + + /** + * Decode any applied 'Content-Encoding'. + * + * Supports 'gzip', 'deflate' and 'identity', otherwise raise a + * {@link Valum.ServerError.NOT_IMPLEMENTED}. + * + * @since 0.3 + */ + public HandlerCallback decode (DecodeFlags flags = DecodeFlags.NONE) { + return (req, res, next, ctx) => { + var encodings = Soup.header_parse_list (req.headers.get_list ("Content-Encoding") ?? ""); + + // decode is in the opposite order of application + encodings.reverse (); + + req.headers.remove ("Content-Encoding"); + + for (unowned SList encoding = encodings; encoding != null; encoding = encoding.next) { + switch (encoding.data.down ()) { + case "gzip": + case "x-gzip": + req.headers.set_encoding (Soup.Encoding.EOF); + req.convert (new ZlibDecompressor (ZlibCompressorFormat.GZIP)); + break; + case "deflate": + req.headers.set_encoding (Soup.Encoding.EOF); + req.convert (new ZlibDecompressor (ZlibCompressorFormat.RAW)); + break; + case "identity": + // nothing to do, let's take a break ;) + break; + default: + // reapply remaining encodings + encoding.reverse (); + foreach (var remaining in encoding) { + req.headers.append ("Content-Encoding", remaining); + } + if (DecodeFlags.FORWARD_REMAINING_ENCODINGS in flags) { + return next (); + } else { + throw new ServerError.NOT_IMPLEMENTED ("The '%s' encoding is not supported.", + encoding.data); + } + } + } + + return next (); + }; + } +} diff --git a/tests/decode-test.vala b/tests/decode-test.vala new file mode 100644 index 000000000..2524e2474 --- /dev/null +++ b/tests/decode-test.vala @@ -0,0 +1,144 @@ +/* + * This file is part of Valum. + * + * Valum is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * Valum is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Valum. If not, see . + */ + +using Valum; +using VSGI.Mock; + +/** + * @since 0.3 + */ +public void test_decode_gzip () { + var req = new Request.with_method ("POST", new Soup.URI ("http://127.0.0.1/")); + var res = new Response (req); + + req.headers.append ("Content-Encoding", "gzip"); + + assert ("gzip" == req.headers.get_list ("Content-Encoding")); + + try { + decode () (req, res, () => { + assert (null == req.headers.get_list ("Content-Encoding")); + return true; + }, new Context ()); + } catch (Error err) { + assert_not_reached (); + } +} + +/** + * @since 0.3 + */ +public void test_decode_xgzip () { + var req = new Request.with_method ("POST", new Soup.URI ("http://127.0.0.1/")); + var res = new Response (req); + + req.headers.append ("Content-Encoding", "x-gzip"); + + assert ("x-gzip" == req.headers.get_list ("Content-Encoding")); + + try { + decode () (req, res, () => { + assert (null == req.headers.get_list ("Content-Encoding")); + return true; + }, new Context ()); + } catch (Error err) { + assert_not_reached (); + } +} + +/** + * @since 0.3 + */ +public void test_decode_deflate () { + var req = new Request.with_method ("POST", new Soup.URI ("http://127.0.0.1/")); + var res = new Response (req); + + req.headers.append ("Content-Encoding", "deflate"); + + assert ("deflate" == req.headers.get_list ("Content-Encoding")); + + try { + decode () (req, res, () => { + assert (null == req.headers.get_list ("Content-Encoding")); + return true; + }, new Context ()); + } catch (Error err) { + assert_not_reached (); + } +} + +/** + * @since 0.3 + */ +public void test_decode_identity () { + var req = new Request.with_method ("POST", new Soup.URI ("http://127.0.0.1/")); + var res = new Response (req); + + req.headers.append ("Content-Encoding", "identity"); + + assert ("identity" == req.headers.get_list ("Content-Encoding")); + + try { + decode () (req, res, () => { + assert (null == req.headers.get_list ("Content-Encoding")); + return true; + }, new Context ()); + } catch (Error err) { + assert_not_reached (); + } +} + +/** + * @since 0.3 + */ +public void test_decode_unknown_encoding () { + var req = new Request.with_method ("POST", new Soup.URI ("http://127.0.0.1/")); + var res = new Response (req); + + req.headers.append ("Content-Encoding", "br, gzip"); + + try { + decode () (req, res, () => { + assert_not_reached (); + }, new Context ()); + assert_not_reached (); + } catch (ServerError.NOT_IMPLEMENTED err) { + assert ("br" == req.headers.get_list ("Content-Encoding")); + } catch (Error err) { + assert_not_reached (); + } +} + +/** + * @since 0.3 + */ +public void test_decode_forward_remaining_encodings () { + var req = new Request.with_method ("POST", new Soup.URI ("http://127.0.0.1/")); + var res = new Response (req); + + req.headers.append ("Content-Encoding", "gzip, br, deflate"); // brotli is not handled + + try { + decode (DecodeFlags.FORWARD_REMAINING_ENCODINGS) (req, res, () => { + assert ("gzip, br" == req.headers.get_list ("Content-Encoding")); + return true; + }, new Context ()); + } catch (Error err) { + assert_not_reached (); + } +} + diff --git a/tests/tests.vala b/tests/tests.vala index dc4cb0084..2b6f58d00 100644 --- a/tests/tests.vala +++ b/tests/tests.vala @@ -103,6 +103,12 @@ public int main (string[] args) { Test.add_func ("/route/match/not_matching", test_route_match_not_matching); Test.add_func ("/route/fire", test_route_match_not_matching); + Test.add_func ("/decode/gzip", test_decode_gzip); + Test.add_func ("/decode/xgzip", test_decode_xgzip); + Test.add_func ("/decode/deflate", test_decode_deflate); + Test.add_func ("/decode/unknown_encoding", test_decode_unknown_encoding); + Test.add_func ("/decode/forward_remaining_encodings", test_decode_forward_remaining_encodings); + Test.add_func ("/subdomain", test_subdomain); Test.add_func ("/subdomain/joker", test_subdomain_joker); Test.add_func ("/subdomain/strict", test_subdomain_strict);