From 2841c5ec58ab3fdd342ff3514ef5290c8389856a Mon Sep 17 00:00:00 2001 From: Guillaume Poirier-Morency Date: Mon, 20 Feb 2017 01:43:46 -0500 Subject: [PATCH 1/3] vsgi: Add 'http_version' and 'headers' to 'Request' constructor --- src/vsgi/vsgi-request.vala | 7 +++++-- tests/server-sent-events-test.vala | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/vsgi/vsgi-request.vala b/src/vsgi/vsgi-request.vala index d7bef8244..fb0368036 100644 --- a/src/vsgi/vsgi-request.vala +++ b/src/vsgi/vsgi-request.vala @@ -252,8 +252,10 @@ namespace VSGI { public Request (IOStream? connection, string method, Soup.URI uri, - HashTable? query = null, - InputStream? body = null) { + HashTable? query = null, + Soup.HTTPVersion http_version = Soup.HTTPVersion.@1_1, + Soup.MessageHeaders? headers = null, + InputStream? body = null) { string[] empty_env = {}; // this is a hack for 'valac-0.24' and 'valac-0.26' #if GIO_2_44 base (connection: connection ?? new SimpleIOStream (new MemoryInputStream (), new MemoryOutputStream.resizable ()), @@ -264,6 +266,7 @@ namespace VSGI { method: method, uri: uri, query: query, + headers: headers, body: body); } diff --git a/tests/server-sent-events-test.vala b/tests/server-sent-events-test.vala index 483a97e6e..272b75dd6 100644 --- a/tests/server-sent-events-test.vala +++ b/tests/server-sent-events-test.vala @@ -32,7 +32,7 @@ public void test_server_sent_events_send () { var req_body = new MemoryInputStream (); var res_body = new MemoryOutputStream.resizable (); - var req = new Request (null, "GET", new Soup.URI ("http://127.0.0.1:3003/"), null, req_body); + var req = new Request (null, "GET", new Soup.URI ("http://127.0.0.1:3003/"), null, Soup.HTTPVersion.@1_1, null, req_body); var res = new Response (req, Soup.Status.OK, null, res_body); try { @@ -69,7 +69,7 @@ public void test_server_sent_events_send_multiline () { var req_body = new MemoryInputStream (); var res_body = new MemoryOutputStream.resizable (); - var req = new Request (null, "GET", new Soup.URI ("http://127.0.0.1:3003/"), null, req_body); + var req = new Request (null, "GET", new Soup.URI ("http://127.0.0.1:3003/"), null, Soup.HTTPVersion.@1_1, null, req_body); var res = new Response (req, Soup.Status.OK, null, res_body); try { From a4adefa6499b2154a8932206ef469603401e5553 Mon Sep 17 00:00:00 2001 From: Guillaume Poirier-Morency Date: Mon, 20 Feb 2017 15:16:06 -0500 Subject: [PATCH 2/3] vsgi: Fix 'headers' initialization in 'Response' --- src/vsgi/vsgi-response.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vsgi/vsgi-response.vala b/src/vsgi/vsgi-response.vala index 2d29b60f7..f9d7604bf 100644 --- a/src/vsgi/vsgi-response.vala +++ b/src/vsgi/vsgi-response.vala @@ -132,8 +132,8 @@ namespace VSGI { } construct { - if (headers == null) { - _headers = new MessageHeaders (MessageHeadersType.REQUEST); + if (_headers == null) { + _headers = new Soup.MessageHeaders (Soup.MessageHeadersType.RESPONSE); } } From c12c62d9d9a51952a170629a235f688cbc3e9c52 Mon Sep 17 00:00:00 2001 From: Guillaume Poirier-Morency Date: Tue, 2 Feb 2016 02:11:19 -0500 Subject: [PATCH 3/3] vsgi: Implement the Apache JServe protocol --- examples/ajp/app.vala | 12 + examples/ajp/meson.build | 1 + examples/meson.build | 1 + src/vsgi/servers/meson.build | 6 + src/vsgi/servers/vsgi-ajp.vala | 466 +++++++++++++++++++++++++++++++++ tests/server-test.vala | 5 + 6 files changed, 491 insertions(+) create mode 100644 examples/ajp/app.vala create mode 100644 examples/ajp/meson.build create mode 100644 src/vsgi/servers/vsgi-ajp.vala diff --git a/examples/ajp/app.vala b/examples/ajp/app.vala new file mode 100644 index 000000000..7b4fb7146 --- /dev/null +++ b/examples/ajp/app.vala @@ -0,0 +1,12 @@ +using VSGI; + +public class App : Handler { + + public override bool handle (Request req, Response res) throws Error { + return res.expand_utf8 ("Hello world!"); + } +} + +public int main (string[] args) { + return Server.new ("ajp", handler: new App ()).run (args); +} diff --git a/examples/ajp/meson.build b/examples/ajp/meson.build new file mode 100644 index 000000000..2afc3f0f1 --- /dev/null +++ b/examples/ajp/meson.build @@ -0,0 +1 @@ +executable('ajp', 'app.vala', dependencies: [glib, gio, soup, vsgi]) diff --git a/examples/meson.build b/examples/meson.build index 6f7a9a28d..86da3705b 100644 --- a/examples/meson.build +++ b/examples/meson.build @@ -1,3 +1,4 @@ +subdir('ajp') subdir('api-interaction') subdir('app') subdir('cgi') diff --git a/src/vsgi/servers/meson.build b/src/vsgi/servers/meson.build index 38e20edcc..70f1ebc70 100644 --- a/src/vsgi/servers/meson.build +++ b/src/vsgi/servers/meson.build @@ -25,3 +25,9 @@ shared_library('vsgi-scgi', 'vsgi-scgi.vala', vala_args: vala_defines, install: true, install_dir: '@0@/vsgi-@1@/servers'.format(get_option('libdir'), api_version)) + +shared_library('vsgi-ajp', 'vsgi-ajp.vala', + dependencies: [glib, gobject, gio, soup, vsgi], + vala_args: vala_defines, + install: true, + install_dir: '@0@/vsgi-@1@/servers'.format(get_option('libdir'), api_version)) diff --git a/src/vsgi/servers/vsgi-ajp.vala b/src/vsgi/servers/vsgi-ajp.vala new file mode 100644 index 000000000..f2d0cdeb4 --- /dev/null +++ b/src/vsgi/servers/vsgi-ajp.vala @@ -0,0 +1,466 @@ +/* + * 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; + +[ModuleInit] +public Type server_init (TypeModule type_module) { + return typeof (VSGI.Ajp.Server); +} + +/** + * Implementation of the Apache JServ Protocol. + * + * [[https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html]] + */ +namespace VSGI.Ajp { + + private errordomain Error { + BAD_COMMON_REQUEST_HEADER + } + + private enum PacketType { + FORWARD_REQUEST = 0x02, + SEND_BODY_CHUNK = 0x03, + SEND_HEADERS = 0x04, + END_RESPONSE = 0x05, + GET_BODY_CHUNK = 0x06, + SHUTDOWN = 0x07, + PING = 0x08, + CPONG = 0x09, + CPING = 0x0A + } + + private enum Method { + OPTIONS = 0x01, + GET = 0x02, + HEAD = 0x03, + POST = 0x04, + PUT = 0x05, + DELETE = 0x06, + TRACE = 0x07, + PROPFIND = 0x08, + PROPPATCH = 0x09, + MKCOL = 0x0A, + COPY = 0x0B, + MOVE = 0x0C, + LOCK = 0x0D, + UNLOCK = 0x0E, + ACL = 0x0F, + REPORT = 0x10, + VERSION_CONTROL = 0x11, + CHECKIN = 0x12, + CHECKOUT = 0x13, + UNCHECKOUT = 0x14, + SEARCH = 0x15, + MKWORKSPACE = 0x16, + UPDATE = 0x17, + LABEL = 0x18, + MERGE = 0x19, + BASELINE_CONTROL = 0x1A, + MKACTIVITY = 0x1B + } + + private const string[] COMMON_REQUEST_HEADERS = { + "accept", + "accept-charset", + "accept-encoding", + "accept-language", + "authorization", + "connection", + "content-type", + "content-length", + "cookie", + "cookie2", + "host", + "pragma", + "referer", + "user-agent" + }; + + private const string[] COMMON_RESPONSE_HEADERS = { + "content-type", + "content-language", + "content-length", + "date", + "last-modified", + "location", + "set-cookie", + "set-cookie2", + "servlet-engine", + "status", + "www-authenticate" + }; + + private enum Attribute { + CONTEXT = 0x01, + SERVLET_PATH = 0x02, + REMOTE_USER = 0x03, + AUTH_TYPE = 0x04, + QUERY_STRING = 0x05, + ROUTE = 0x06, + SSL_CERT = 0x07, + SSL_CIPHER = 0x08, + SSL_SESSION = 0x09, + REQ_ATTRIBUTE = 0x0A, + SSL_KEY_SIZE = 0x0B, + SECRET = 0x0C, + STORED_METHOD = 0x0D, + ARE_DONE = 0xFF + } + + /** + * Filter a base stream to consume body chunks. + */ + private class ChunkInputStream : FilterInputStream { + + /** + * Tells if the first chunk was read. + */ + private bool _first_chunk_read = false; + + /** + * Number of bytes remaining to read in the current chunk. + */ + private size_t _remaining_in_chunk = 0; + + public DataOutputStream output_stream { construct; get; } + + public ChunkInputStream (InputStream base_stream, DataOutputStream output_stream) { + Object (base_stream: base_stream, output_stream: output_stream); + } + + /** + * + */ + public override ssize_t read (uint8[] buffer, Cancellable? cancellable = null) throws GLib.IOError { + buffer.length = buffer.length % uint16.MAX; + + if (_first_chunk_read && _remaining_in_chunk == 0) { + // request more bytes + output_stream.put_byte ('A'); + output_stream.put_byte ('B'); + output_stream.put_uint16 (3); + output_stream.put_byte (PacketType.GET_BODY_CHUNK); + output_stream.put_uint16 ((uint16) buffer.length); + output_stream.flush (); + } + + uint8 b[4]; + base_stream.read_all (b, null); + assert (b[0] == 0x12); + assert (b[1] == 0x34); + _first_chunk_read = true; + _remaining_in_chunk = b[3] << 8 + b[4]; + + message ("%lu", _remaining_in_chunk); + + buffer.length = (int) size_t.min ((size_t) buffer.length, _remaining_in_chunk); + + var bytes_read = base_stream.read (buffer, cancellable); + + _remaining_in_chunk -= bytes_read; + + return bytes_read; + } + + public override bool close (Cancellable? cancellable = null) { + return base_stream.close (); + } + } + + /** + * Filter a base stream to send body chunks. + */ + private class ChunkOutputStream : FilterOutputStream { + + public ChunkOutputStream (OutputStream base_stream) { + Object (base_stream: base_stream); + } + + /** + * Remaining bytes to write in the current chunk. + */ + private size_t _remaining_in_chunk = 0; + + public override ssize_t write (uint8[] buffer, Cancellable? cancellable = null) throws GLib.IOError { + if (_remaining_in_chunk == 0) { + base_stream.write_all ({'A', 'B', PacketType.SEND_BODY_CHUNK, (uint8) buffer.length >> 8, (uint8) buffer.length}, + null); + _remaining_in_chunk = buffer.length; + } + + var bytes_written = base_stream.write (buffer[0:_remaining_in_chunk], cancellable); + + _remaining_in_chunk -= bytes_written; + + return bytes_written; + } + + /** + * Write a {@link PacketType.END_RESPONSE} packet to indicate that this + * request has ended. + * + * The {@link FilterOutputStream.close_base_stream} property is used to + * determine if the connection can be reused. + */ + public override bool close (Cancellable? cancellable = null) throws GLib.IOError { + size_t bytes_written; + return base_stream.write_all ({'A', 'B', 2, PacketType.END_RESPONSE, 0x01}, out bytes_written) && + base_stream.close (); + } + } + + private class Response : VSGI.Response { + + private DataOutputStream @out; + + public Response (Request request, ChunkOutputStream body) { + base (request, Soup.Status.OK, null, body); + } + + construct { + @out = new DataOutputStream (request.connection.output_stream); + } + + private Soup.HTTPVersion _stored_http_version; + private uint _stored_status; + private string _stored_reason_phrase; + + /** + * Build a 'SEND_HEADERS' packet. + */ + protected override bool write_status_line (Soup.HTTPVersion http_version, + uint status, + string reason_phrase, + out size_t bytes_written, + Cancellable? cancellable = null) throws GLib.IOError { + _stored_http_version = http_version; + _stored_status = status; + _stored_reason_phrase = reason_phrase; + + bytes_written = 0; + return true; + } + + protected override bool write_headers (Soup.MessageHeaders headers, + out size_t bytes_written, + Cancellable? cancellable = null) throws GLib.IOError { + @out.put_byte ('A'); + @out.put_byte ('B'); + @out.put_byte (PacketType.SEND_HEADERS); + + var packet_len = 3 + 2 + _stored_reason_phrase.length + 1; + var num_headers = 0; + headers.foreach ((n, h) => { + packet_len += (uint16) n.length + 1; + packet_len += (uint16) h.length + 1; + num_headers++; + }); + + @out.put_uint16 (packet_len); + + @out.put_uint16 ((uint16) _stored_status); + @out.put_uint16 ((uint16) _stored_reason_phrase.length + 1); + @out.put_string (_stored_reason_phrase); + + // room for 'num_headers' + @out.put_uint16 (num_headers); + + // register common headers + for (var i = 0; i < COMMON_RESPONSE_HEADERS.length; i++) { + var header = COMMON_RESPONSE_HEADERS[i].has_prefix ("set-cookie") ? headers.get_list (COMMON_RESPONSE_HEADERS[i]) : + headers.get_one (COMMON_RESPONSE_HEADERS[i]); + if (header == null) + continue; + @out.put_byte (0x0A); + @out.put_byte (i + 1); + @out.put_uint16 ((uint16) header.length); + @out.put_string (header); + } + + Soup.MessageHeadersIter headers_iter; + Soup.MessageHeadersIter.init (out headers_iter, headers); + string name; + string header; + while (headers_iter.next (out name, out header)) { + // skip common headers + if (strv_contains (COMMON_RESPONSE_HEADERS, name.down ())) + continue; + @out.put_uint16 ((uint16) name.length + 1); + @out.put_string (name); + @out.put_uint16 ((uint16) header.length + 1); + @out.put_string (header); + } + + bytes_written = 3 + packet_len; + + @out.flush (); + + return true; + } + } + + [Version (since = "0.4")] + public class Server : SocketServer { + + protected override string scheme { get { return "ajp"; } } + + protected override bool incoming (SocketConnection connection) { + process_connection.begin (connection, (obj, result) => { + try { + process_connection.end (result); + } catch (GLib.Error err) { + critical ("%s", err.message); + } + }); + return false; + } + + private static bool read_bool (DataInputStream @in) throws GLib.Error { + return @in.read_byte () == 1; + } + + private static string? read_string (DataInputStream @in) throws GLib.Error { + var len = @in.read_uint16 (); + if (len == uint16.MAX) { + return null; + } + var buffer = new uint8[len + 1]; + @in.read_all (buffer, null, null); + return (string) buffer; + } + + private async void process_connection (SocketConnection connection) throws GLib.Error { + var @in = new DataInputStream (connection.input_stream); + var @out = new DataOutputStream (connection.output_stream); + + var packet_magic = @in.read_uint16 () != 0x1234; + var packet_len = @in.read_uint16 (); + yield @in.fill_async (packet_len); + + switch (@in.read_byte ()) { + case PacketType.FORWARD_REQUEST: + var method = ((EnumClass) typeof (Method).class_ref ()) + .get_value (@in.read_byte ()) + .value_nick + .up (); + + var protocol = read_string (@in); + + var http_version = protocol == "HTTP/1.0" ? Soup.HTTPVersion.@1_0 : Soup.HTTPVersion.@1_1; + + var req_uri = read_string (@in); + + var remote_addr = read_string (@in); // 'remote_addr' + var remote_host = read_string (@in); // 'remote_host' + var server_name = read_string (@in); // 'server_name' + var server_port = @in.read_uint16 (); // 'server_port' + + var uri = new Soup.URI ("http://%s:%u%s".printf (server_name, server_port, req_uri)); + + // 'is_ssl' + if (read_bool (@in)) + uri.set_scheme ("https"); + + var headers = new Soup.MessageHeaders (Soup.MessageHeadersType.REQUEST); + var num_headers = @in.read_uint16 (); + for (int i = 0; i < num_headers; i++) { + string name; + var name_len = @in.read_uint16 (); + if ((name_len >> 8) == 0xA0) { + if (likely (0x01 <= (name_len & 0x00FF) <= 0x0E)) { + name = COMMON_REQUEST_HEADERS[(name_len & 0x00FF) - 1]; + } else { + throw new Error.BAD_COMMON_REQUEST_HEADER (""); + } + } else { + var name_buf = new uint8[name_len + 1]; + @in.read_all (name_buf, null); + name = (string) name_buf; + } + var header = read_string (@in); + headers.append (name, header); + } + + // attributes + uint8 attribute = 0; + while (true) { + attribute = @in.read_byte (); + if (attribute == Attribute.ARE_DONE) { + break; + } + switch (attribute) { + case Attribute.REMOTE_USER: // 'remote_user' + uri.set_user (read_string (@in)); + break; + case Attribute.QUERY_STRING: // 'query_string' + uri.set_query (read_string (@in)); + break; + case Attribute.REQ_ATTRIBUTE: + read_string (@in); // name + read_string (@in); // value + break; + default: + read_string (@in); + break; + } + } + + var content_length = headers.get_content_length (); + + var req = new Request (connection, + method, + uri, + null, + http_version, + headers, + new BoundedInputStream (new ChunkInputStream (@in, @out), content_length)); + var res = new Response (req, new ChunkOutputStream (@out)); + + yield handler.handle_async (req, res); + break; + case PacketType.SHUTDOWN: + if (connection.get_remote_address ().get_family () == SocketFamily.UNIX || + (connection.get_remote_address () as InetAddress).get_is_loopback ()) { + stop (); + } else { + warning ("Shutdown attempt from a non-local client: '%s'.", connection.get_remote_address ().to_string ()); + } + break; + case PacketType.PING: + break; + case PacketType.CPING: + try { + @out.put_byte ('A'); + @out.put_byte ('B'); + @out.put_uint16 (1); + @out.put_byte (PacketType.CPONG); + yield @out.flush_async (); + } catch (GLib.IOError err) { + critical ("Could not reply 'CPONG' to a 'CPING' request."); + } + break; + default: + critical ("Unknown packet type."); + break; + } + + yield connection.close_async (); + } + } +} diff --git a/tests/server-test.vala b/tests/server-test.vala index 4859484f7..b217f6781 100644 --- a/tests/server-test.vala +++ b/tests/server-test.vala @@ -31,6 +31,11 @@ public int main (string[] args) { assert ("VSGISCGIServer" == server.get_type ().name ()); }); + Test.add_func ("/server/new/ajp", () => { + var server = Server.@new ("ajp"); + assert ("VSGIAjpServer" == server.get_type ().name ()); + }); + Test.add_func ("/server/fork", () => { var server = Server.@new ("http"); Pid pid;