From: Zeeshan Ali (Khattak) Date: Mon, 9 Feb 2009 22:27:54 +0000 (+0000) Subject: Put HTTP request handling into a separate class: HTTPRequest. X-Git-Tag: RYGEL_0_2_2~102 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=66ea6b228ac6fe16c27475b282a1c0907f306ebf;p=profile%2Fivi%2Frygel.git Put HTTP request handling into a separate class: HTTPRequest. This will allow us to make async calls during the handling of HTTP requests. svn path=/trunk/; revision=541 --- diff --git a/src/rygel/Makefile.am b/src/rygel/Makefile.am index 0c89b96..4703d63 100644 --- a/src/rygel/Makefile.am +++ b/src/rygel/Makefile.am @@ -42,6 +42,8 @@ BUILT_SOURCES = rygel-1.0.vapi \ rygel-plugin-loader.c \ rygel-http-server.c \ rygel-http-server.h \ + rygel-http-request.c \ + rygel-http-request.h \ rygel-http-response.c \ rygel-http-response.h \ rygel-live-response.c \ @@ -88,6 +90,8 @@ rygel_SOURCES = rygel-1.0.vapi \ rygel-plugin-loader.vala \ rygel-http-server.c \ rygel-http-server.h \ + rygel-http-request.c \ + rygel-http-request.h \ rygel-http-response.c \ rygel-http-response.h \ rygel-live-response.c \ @@ -130,6 +134,7 @@ VAPI_SOURCE_FILES = rygel-content-directory.vala \ rygel-connection-manager.vala \ rygel-media-receiver-registrar.vala \ rygel-http-server.vala \ + rygel-http-request.vala \ rygel-http-response.vala \ rygel-live-response.vala \ rygel-seekable-response.vala \ diff --git a/src/rygel/rygel-http-request.vala b/src/rygel/rygel-http-request.vala new file mode 100644 index 0000000..a5df60d --- /dev/null +++ b/src/rygel/rygel-http-request.vala @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2008, 2009 Nokia Corporation, all rights reserved. + * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd. + * + * Author: Zeeshan Ali (Khattak) + * + * Jorn Baayen + * + * This file is part of Rygel. + * + * Rygel 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 2 of the License, or + * (at your option) any later version. + * + * Rygel 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 this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Rygel; +using Gst; + +public errordomain Rygel.HTTPRequestError { + UNACCEPTABLE = Soup.KnownStatusCode.NOT_ACCEPTABLE, + INVALID_RANGE = Soup.KnownStatusCode.BAD_REQUEST, + OUT_OF_RANGE = Soup.KnownStatusCode.REQUESTED_RANGE_NOT_SATISFIABLE, + BAD_REQUEST = Soup.KnownStatusCode.BAD_REQUEST, + NOT_FOUND = Soup.KnownStatusCode.NOT_FOUND +} + +/** + * Responsible for handling HTTP client requests. + */ +public class Rygel.HTTPRequest : GLib.Object { + private unowned ContentDirectory content_dir; + private Soup.Server server; + private Soup.Message msg; + private HashTable? query; + + private HTTPResponse response; + + public signal void handled (); + + private string item_id; + private MediaItem item; + private Seek seek; + + public HTTPRequest (ContentDirectory content_dir, + Soup.Server server, + Soup.Message msg, + HashTable? query) { + this.content_dir = content_dir; + this.server = server; + this.msg = msg; + this.query = query; + } + + public void start_processing () { + if (this.msg.method != "HEAD" && this.msg.method != "GET") { + /* We only entertain 'HEAD' and 'GET' requests */ + this.handle_error ( + new HTTPRequestError.BAD_REQUEST ("Invalid Request")); + return; + } + + if (query != null) { + this.item_id = query.lookup ("itemid"); + } + + if (this.item_id == null) { + this.handle_error (new HTTPRequestError.NOT_FOUND ("Not Found")); + return; + } + + this.handle_item_request (); + } + + private void stream_from_gst_source (Element# src) throws Error { + var response = new LiveResponse (this.server, + this.msg, + "RygelLiveResponse", + src); + this.response = response; + + response.start (); + response.ended += on_response_ended; + } + + private void serve_uri (string uri, size_t size) { + var response = new SeekableResponse (this.server, + this.msg, + uri, + this.seek, + size); + this.response = response; + + response.ended += on_response_ended; + } + + private void on_response_ended (HTTPResponse response) { + this.end (Soup.KnownStatusCode.NONE); + } + + private void handle_item_request () { + // Fetch the requested item + this.fetch_requested_item (); + if (this.item == null) { + return; + } + + try { + this.parse_range (); + } catch (Error error) { + this.handle_error (error); + return; + } + + // Add headers + this.add_item_headers (); + + if (this.msg.method == "HEAD") { + // Only headers requested, no need to send contents + this.end (Soup.KnownStatusCode.OK); + return; + } + + if (this.item.size > 0) { + this.handle_interactive_item (); + } else { + this.handle_streaming_item (); + } + } + + private void add_item_headers () { + if (this.item.mime_type != null) { + this.msg.response_headers.append ("Content-Type", + this.item.mime_type); + } + + if (this.item.size >= 0) { + this.msg.response_headers.append ("Content-Length", + this.item.size.to_string ()); + } + + if (this.item.size > 0) { + int64 first_byte; + int64 last_byte; + + if (this.seek != null) { + first_byte = this.seek.start; + last_byte = this.seek.stop; + } else { + first_byte = 0; + last_byte = this.item.size - 1; + } + + // Content-Range: bytes START_BYTE-STOP_BYTE/TOTAL_LENGTH + var content_range = "bytes " + + first_byte.to_string () + "-" + + last_byte.to_string () + "/" + + this.item.size.to_string (); + this.msg.response_headers.append ("Content-Range", content_range); + this.msg.response_headers.append ("Accept-Ranges", "bytes"); + } + } + + private void handle_streaming_item () { + string uri = this.item.uri; + dynamic Element src = null; + + if (uri != null) { + // URI provided, try to create source element from it + src = Element.make_from_uri (URIType.SRC, uri, null); + } else { + // No URI provided, ask for source element + src = this.item.create_stream_source (); + } + + if (src == null) { + this.handle_error (new HTTPRequestError.NOT_FOUND ("Not Found")); + return; + } + + // For rtspsrc since some RTSP sources takes a while to start + // transmitting + src.tcp_timeout = (int64) 60000000; + + try { + // Then start the gst stream + this.stream_from_gst_source (src); + } catch (Error error) { + this.handle_error (error); + return; + } + } + + private void handle_interactive_item () { + string uri = this.item.uri; + + if (uri == null) { + var error = new HTTPRequestError.NOT_FOUND ( + "Requested item '%s' didn't provide a URI\n", + this.item.id); + this.handle_error (error); + return; + } + + this.serve_uri (uri, this.item.size); + } + + private void parse_range () throws HTTPRequestError { + string range; + string[] range_tokens; + + range = this.msg.request_headers.get ("Range"); + if (range == null) { + return; + } + + // We have a Range header. Parse. + if (!range.has_prefix ("bytes=")) { + throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'", + range); + } + + range_tokens = range.offset (6).split ("-", 2); + + if (range_tokens[0] == null || range_tokens[1] == null) { + throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'", + range); + } + + this.seek = new Seek (Format.BYTES, 0, this.item.size - 1); + + // Get first byte position + string first_byte = range_tokens[0]; + if (first_byte[0].isdigit ()) { + this.seek.start = first_byte.to_int64 (); + } else if (first_byte != "") { + throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'", + range); + } + + // Get last byte position if specified + string last_byte = range_tokens[1]; + if (last_byte[0].isdigit ()) { + this.seek.stop = last_byte.to_int64 (); + } else if (last_byte != "") { + throw new HTTPRequestError.INVALID_RANGE ("Invalid Range '%s'", + range); + } + + if (this.item.size > 0) { + // shouldn't go beyond actual length of media + if (this.seek.start > this.item.size || + this.seek.length > this.item.size) { + throw new HTTPRequestError.OUT_OF_RANGE ( + "Range '%s' not setsifiable", range); + } + + // No need to seek if whole stream is requested + if (this.seek.start == 0 && + this.seek.length == this.item.size) { + this.seek == null; + return; + } + } else if (this.seek.start == 0) { + // Might be an attempt to get the size, in which case it's not + // an error. Just don't seek. + this.seek == null; + return; + } else { + throw new HTTPRequestError.UNACCEPTABLE ( + "Partial download not applicable for item %s", + this.item.id); + } + } + + private void fetch_requested_item () { + MediaObject media_object; + + try { + media_object = this.content_dir.find_object_by_id (this.item_id); + } catch (Error error) { + this.handle_error (error); + return; + } + + if (media_object == null || !(media_object is MediaItem)) { + this.handle_error (new HTTPRequestError.NOT_FOUND ( + "requested item '%s' not found", + this.item_id)); + return; + } + + this.item = (MediaItem) media_object; + } + + private void handle_error (Error error) { + warning ("%s", error.message); + + uint status; + if (error is HTTPRequestError) { + status = error.code; + } else { + status = Soup.KnownStatusCode.NOT_FOUND; + } + + this.end (status); + } + + public void end (uint status) { + if (status != Soup.KnownStatusCode.NONE) { + this.msg.set_status (status); + } + + this.handled (); + } +} + diff --git a/src/rygel/rygel-http-server.vala b/src/rygel/rygel-http-server.vala index 95fba96..226e05b 100644 --- a/src/rygel/rygel-http-server.vala +++ b/src/rygel/rygel-http-server.vala @@ -1,10 +1,8 @@ /* * Copyright (C) 2008, 2009 Nokia Corporation, all rights reserved. - * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd. * * Author: Zeeshan Ali (Khattak) * - * Jorn Baayen * * This file is part of Rygel. * @@ -28,12 +26,6 @@ using Gst; using GUPnP; using Gee; -public errordomain Rygel.HTTPServerError { - UNACCEPTABLE = Soup.KnownStatusCode.NOT_ACCEPTABLE, - INVALID_RANGE = Soup.KnownStatusCode.BAD_REQUEST, - OUT_OF_RANGE = Soup.KnownStatusCode.REQUESTED_RANGE_NOT_SATISFIABLE -} - public class Rygel.HTTPServer : GLib.Object { private const string SERVER_PATH_PREFIX = "/RygelHTTPServer"; private string path_root; @@ -41,13 +33,13 @@ public class Rygel.HTTPServer : GLib.Object { // Reference to associated ContentDirectory service private unowned ContentDirectory content_dir; private GUPnP.Context context; - private ArrayList responses; + private ArrayList requests; public HTTPServer (ContentDirectory content_dir, string name) { this.content_dir = content_dir; this.context = content_dir.context; - this.responses = new ArrayList (); + this.requests = new ArrayList (); this.path_root = SERVER_PATH_PREFIX + "/" + name; @@ -72,35 +64,9 @@ public class Rygel.HTTPServer : GLib.Object { return create_uri_for_path (query); } - private void stream_from_gst_source (Element# src, - Soup.Message msg) throws Error { - var response = new LiveResponse (this.context.server, - msg, - "RygelLiveResponse", - src); - response.start (); - response.ended += on_response_ended; - - this.responses.add (response); - } - - private void serve_uri (string uri, - Soup.Message msg, - Seek? seek, - size_t size) throws Error { - var response = new SeekableResponse (this.context.server, - msg, - uri, - seek, - size); - response.ended += on_response_ended; - - this.responses.add (response); - } - - private void on_response_ended (HTTPResponse response) { - /* Remove the response from our list. */ - this.responses.remove (response); + private void on_request_handled (HTTPRequest request) { + /* Remove the request from our list. */ + this.requests.remove (request); } private void server_handler (Soup.Server server, @@ -108,244 +74,12 @@ public class Rygel.HTTPServer : GLib.Object { string server_path, HashTable? query, Soup.ClientContext soup_client) { - if (msg.method != "HEAD" && msg.method != "GET") { - /* We only entertain 'HEAD' and 'GET' requests */ - msg.set_status (Soup.KnownStatusCode.BAD_REQUEST); - return; - } - - string item_id = null; - if (query != null) { - item_id = query.lookup ("itemid"); - } - - if (item_id == null) { - msg.set_status (Soup.KnownStatusCode.NOT_FOUND); - return; - } - - this.handle_item_request (msg, item_id); - } - - private void handle_item_request (Soup.Message msg, - string item_id) { - MediaItem item; - - // Fetch the requested item - item = this.get_requested_item (item_id); - if (item == null) { - msg.set_status (Soup.KnownStatusCode.NOT_FOUND); - return; - } - - Seek seek = null; - - try { - seek = this.parse_range (msg, item); - } catch (HTTPServerError err) { - warning ("%s", err.message); - msg.set_status (err.code); - return; - } - - // Add headers - this.add_item_headers (msg, item, seek); - - if (msg.method == "HEAD") { - // Only headers requested, no need to send contents - msg.set_status (Soup.KnownStatusCode.OK); - return; - } - - if (item.size > 0) { - this.handle_interactive_item (msg, item, seek); - } else { - this.handle_streaming_item (msg, item); - } - } - - private void add_item_headers (Soup.Message msg, - MediaItem item, - Seek? seek) { - if (item.mime_type != null) { - msg.response_headers.append ("Content-Type", item.mime_type); - } - - if (item.size >= 0) { - msg.response_headers.append ("Content-Length", - item.size.to_string ()); - } - - if (item.size > 0) { - int64 first_byte; - int64 last_byte; - - if (seek != null) { - first_byte = seek.start; - last_byte = seek.stop; - } else { - first_byte = 0; - last_byte = item.size - 1; - } - - // Content-Range: bytes START_BYTE-STOP_BYTE/TOTAL_LENGTH - var content_range = "bytes " + - first_byte.to_string () + "-" + - last_byte.to_string () + "/" + - item.size.to_string (); - msg.response_headers.append ("Content-Range", content_range); - msg.response_headers.append ("Accept-Ranges", "bytes"); - } - } - - private void handle_streaming_item (Soup.Message msg, - MediaItem item) { - string uri = item.uri; - dynamic Element src = null; - - if (uri != null) { - // URI provided, try to create source element from it - src = Element.make_from_uri (URIType.SRC, uri, null); - } else { - // No URI provided, ask for source element - src = item.create_stream_source (); - } - - if (src == null) { - warning ("Failed to create source element for item: %s\n", - item.id); - msg.set_status (Soup.KnownStatusCode.NOT_FOUND); - return; - } - - // For rtspsrc since some RTSP sources takes a while to start - // transmitting - src.tcp_timeout = (int64) 60000000; - - try { - // Then start the gst stream - this.stream_from_gst_source (src, msg); - } catch (Error error) { - critical ("Error in attempting to start streaming %s: %s", - uri, - error.message); - } - } - - private void handle_interactive_item (Soup.Message msg, - MediaItem item, - Seek? seek) { - string uri = item.uri; - - if (uri == null) { - warning ("Requested item '%s' didn't provide a URI\n", item.id); - msg.set_status (Soup.KnownStatusCode.NOT_FOUND); - return; - } - - try { - this.serve_uri (uri, msg, seek, item.size); - } catch (Error error) { - warning ("Error in attempting to serve %s: %s", - uri, - error.message); - msg.set_status (Soup.KnownStatusCode.NOT_FOUND); - } - } - - /* Parses the HTTP Range header on @message and sets: - * - * @offset to the requested offset (left unchanged if none specified), - * @length to the requested length (left unchanged if none specified). - * - * Both @offset and @length are expected to be initialised to their default - * values. Throws a #HTTPServerError in case of error. - * - * Returns %true a range header was found, false otherwise. */ - private Seek? parse_range (Soup.Message message, - MediaItem item) - throws HTTPServerError { - string range; - string[] range_tokens; - Seek seek = null; - - range = message.request_headers.get ("Range"); - if (range == null) { - return seek; - } - - // We have a Range header. Parse. - if (!range.has_prefix ("bytes=")) { - throw new HTTPServerError.INVALID_RANGE ("Invalid Range '%s'", - range); - } - - range_tokens = range.offset (6).split ("-", 2); - - if (range_tokens[0] == null || range_tokens[1] == null) { - throw new HTTPServerError.INVALID_RANGE ("Invalid Range '%s'", - range); - } - - seek = new Seek (Format.BYTES, 0, item.size - 1); - - // Get first byte position - string first_byte = range_tokens[0]; - if (first_byte[0].isdigit ()) { - seek.start = first_byte.to_int64 (); - } else if (first_byte != "") { - throw new HTTPServerError.INVALID_RANGE ("Invalid Range '%s'", - range); - } - - // Get last byte position if specified - string last_byte = range_tokens[1]; - if (last_byte[0].isdigit ()) { - seek.stop = last_byte.to_int64 (); - } else if (last_byte != "") { - throw new HTTPServerError.INVALID_RANGE ("Invalid Range '%s'", - range); - } - - if (item.size > 0) { - // shouldn't go beyond actual length of media - if (seek.start > item.size || - seek.length > item.size) { - throw new HTTPServerError.OUT_OF_RANGE ( - "Range '%s' not setsifiable", range); - } - - // No need to seek if whole stream is requested - if (seek.start == 0 && seek.length == item.size) { - return null; - } - } else if (seek.start == 0) { - // Might be an attempt to get the size, in which case it's not - // an error. Just don't seek. - return null; - } else { - throw new HTTPServerError.UNACCEPTABLE ( - "Partial download not applicable for item %s", - item.id); - } - - return seek; - } - - private MediaItem? get_requested_item (string item_id) { - MediaObject media_object; + var request = new HTTPRequest (this.content_dir, server, msg, query); - try { - media_object = this.content_dir.find_object_by_id (item_id); - } catch (Error err) { - return null; - } + request.handled += this.on_request_handled; + this.requests.add (request); - if (media_object != null && media_object is MediaItem) { - return (MediaItem) media_object; - } else { - return null; - } + request.start_processing (); } }