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..3f5ecc7d9 100644 --- a/tests/tests.vala +++ b/tests/tests.vala @@ -103,6 +103,11 @@ 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/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);