/* * Copyright (c) 2003, 2004 The University of Wroclaw. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the University may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN * NO EVENT SHALL THE UNIVERSITY BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ namespace Sioux { using Nemerle.Collections; using Nemerle.Xml; using System; using System.Net.Sockets; using System.IO; /** * Description of an HTTP request */ public variant RequestInfo { | StaticGet { url : string; } | DynamicGet { url : string; get_params : list [string * string]; } | Post { url : string; post_params : list [string * string]; posted_files : list [string * string * string * int]; } | Invalid } /** * Request parser */ public class Request { /* -- CONSTRUCTORS ----------------------------------------------------- */ /** * Parses the request data. */ internal this (stream : NetworkStream) { request_time = DateTime.Now; request_stream = stream; logger = Httpd.GetLogger (); cookies = []; parse_request_data () } /* -- PUBLIC PROPERTIES --------------------------------------------------- */ /** * Returns the request info associated with this instance */ public RequestInfo : RequestInfo { get { request_info } } /** * Returns a list of headers. The ordering is the same * as sent by the client. */ public Headers : list [string * string] { get { headers } } /** * Return time of the request as DateTime struct */ public RequestTime : DateTime { get {request_time } } /** * Returns requested url as a string */ public RequestUri : string { get { request_uri } } /** * Returns cookies sent with this request */ public Cookies : list[Cookie] { get { cookies } } /** * Returns request's protocol */ public Protocol : string { get { protocol } } /* -- PUBLIC METHODS --------------------------------------------------- */ /** * Returns Some(Cookie) if browser sent a cookie with given name, else None */ public GetCookie (name : string) : option [Cookie] { def loop(lst : list [Cookie]) : option [Cookie] { match(lst) { | [] => None () | c :: rest => if(c.Name == name) Some(c) else loop(rest) } } loop(Cookies) } /** * Returns session assosciated with this request */ public GetSession () : Session { session } /** * Returns session assosciated with this request, if create is true creates new session */ public GetSession (create : bool) : Session { when(create) { Httpd.RemoveSession(session.SessionID); session = Session (); Httpd.AddSession(session); } session } /** * Returns value of specified header */ public GetHeader ( name : string ) : option [string] { def loop(list ) : option [string] { match(list) { | [] => None () | (h_name,value) :: rest => if(h_name==name) Some(value) else loop(rest) } } loop (headers); } /* -- INTERNAL METHODS ------------------------------------------------- */ /** * Reads a line from the stream */ internal ReadLine () : string { def sb = System.Text.StringBuilder (); def loop () { def ich = request_stream.ReadByte (); def ch = (ich :> char); if (ich == -1 || ch == '\n') () else if (ch == '\r') loop () else { ignore (sb.Append (ch)); loop () } }; loop (); System.Console.WriteLine ("REQUEST: '{0}'", sb.ToString ()); sb.ToString () } /** * cuts the "/webapps/app_name" part from request url */ internal CutRequestUrl () : void { match(request_info) { | RequestInfo.Post(url,request_params,files) => when(url.StartsWith("/webapp/")) { def url = url.Substring(8); def (_,url,_) = split_unique_at('/',url); request_info = RequestInfo.Post("/" + url,request_params,files) } | RequestInfo.DynamicGet(url,request_params) => when(url.StartsWith("/webapp/")) { def url = url.Substring(8); def (_,url,_) = split_unique_at('/',url); if(url == "") { request_info = RequestInfo.DynamicGet("/index.xml",request_params); } else request_info = RequestInfo.DynamicGet( "/" + url,request_params) } | _ => () } } /* -- PRIVATE METHODS -------------------------------------------------- */ /** * Splits a string given a separator character. Checks if the split was * unique -- or, if the separator occured exactly once in the original * string. */ private split_unique_at (separator : char, str : string) : string * string * bool { mutable l = str; mutable r = ""; def pos = str.IndexOf (separator); //Console.WriteLine(pos.ToString() + " " + str); when (pos != -1) { l = str.Substring (0, pos); r = str.Substring (pos + 1); } /* else def (l, r) = (str, "");*/ def validity = l.IndexOf (separator) == -1 && r.IndexOf (separator) == -1; (l, r, validity) } /** * A helper function ignoring the validity result from * the split_unique_at function. */ private split_at (separator : char, str : string) : string * string { def (l, r, _) = split_unique_at (separator, str); (l, r) } /** * Parses a string of form: * * url?param_1=val_1¶m_2=val_2&...¶m_k=val_k * * Checks the validity of such a request. */ private parse_get_params (orig_url : string) : string * list [string * string] * bool { def (url, get_params, split_was_unique) = split_unique_at ('?', orig_url); def invalid = (orig_url, [], false); if (url.Length > 0 && split_was_unique) { // check the case for no GET parameters if (get_params.Length == 0) { (url, [], true) } else { def delimiter = array ['&']; def split = get_params.Split (delimiter); // check for the url?param special case if (split.Length == 1) { def (param, value, split_was_unique) = split_unique_at ('=', split [0]); if (split_was_unique) (url, [(param, value)], true) else invalid } else { mutable params_are_valid = true; mutable result = []; mutable index = 0; while (index < split.Length) { def (param, value, split_was_unique) = split_unique_at ('=', split [index]); def param = System.Web.HttpUtility.UrlDecode (param, System.Text.Encoding.UTF8); def value = System.Web.HttpUtility.UrlDecode (value, System.Text.Encoding.UTF8); unless (split_was_unique) params_are_valid = false; unless (param.Length == 0) result = (param, value) :: result; index = index + 1 }; if (params_are_valid) (url, List.Rev (result), true) else invalid } } } else invalid } /** * Retrieves the post data * * FIXME: what about file uploads? */ private parse_post_data () : list [string * string] * list [string * string * string * int] * bool { assert (Option.IsSome (content_length)); mutable file = false; mutable length = Option.UnSome (content_length); def sb = System.Text.StringBuilder (length); mutable multipart_params = []; mutable files = []; def loop(len) { mutable l = len; while (l > 0) { def ch = request_stream.ReadByte (); unless (ch == -1) ignore (sb.Append ((ch :> char))); l = l - 1 }; System.Console.WriteLine ("POST -- '{0}'", sb.ToString ()); } match(content_type) { | None => loop(length) | Some(val) => if (val.StartsWith("multipart/form-data;")) { def readline() : string * list[byte] { def sb = System.Text.StringBuilder(); mutable byte_list = []; mutable ich = request_stream.ReadByte (); byte_list = (ich :> byte) :: byte_list; mutable ch = (ich :> char); while(ich != -1 && ch != '\n') { when(ch != '\r') ignore (sb.Append (ch)); ich = request_stream.ReadByte (); byte_list = (ich :> byte) :: byte_list; ch = (ich :> char) } (sb.ToString (),List.Rev(byte_list)) } mutable bw = BinaryWriter.Null; def boundary = val.Substring(30); def section_start = "--" + boundary; def end = section_start + "--"; def (lin,byt) = readline(); def max_file_size = Int32.Parse(Httpd.GetConfig().Get("server/max_uploaded_file_size")); mutable line = lin; mutable bytes = byt; mutable can_write = true; mutable mode = 1; mutable start = true; mutable current_param_value = ""; mutable current_param_name = ""; mutable current_file_filename = ""; mutable current_file_name = ""; mutable current_file_tempname = ""; mutable current_file_size = 0; while(line != end) { match((line,bytes,mode)) { | (l,b,1) => if(l==section_start) { if(start) start = false else { if(!file) { multipart_params = (current_param_name,current_param_value.Substring(0,current_param_value.Length-1)) :: multipart_params; current_param_name = ""; current_param_value = ""; } else { if(!can_write) files = (current_file_name,current_file_filename,current_file_tempname,-1) :: files; else files = (current_file_name, current_file_filename, current_file_tempname, current_file_size-2) :: files; can_write = true; bw.Close(); } } mode = 2; } else { match(file) { | true => def loop ( bts ) { match(bts : list[byte]) { | [] => () | b :: rest => when(can_write) bw.Write(b); loop(rest) } } when(current_file_size > max_file_size && max_file_size != -1) { can_write = false; File.Delete(Httpd.GetConfig().Get("server/uploads_dir") +"/"+ current_file_tempname); } current_file_size = current_file_size + b.Length; loop(b) | false => current_param_value = current_param_value + l + "\n"; } } def (lin,byt) = readline(); line = lin; bytes = byt; | (l,_,_) => { if(l.ToUpper().StartsWith("CONTENT-DISPOSITION")) { def (_,s) = split_at(':',l); def delimiter = array[';']; def split = s.Split(delimiter); if(split.Length == 3 && split[0].ToUpper().StartsWith(" FORM-DATA") && split[1].ToUpper().StartsWith(" NAME=") && split[2].ToUpper().StartsWith(" FILENAME")) { def (_,split1) = split_at('=',split[1]); def (_,split2) = split_at('=',split[2]); current_file_name = split1.Substring(1,split1.Length-2); current_file_filename = split2.Substring(1,split2.Length-2); current_file_tempname = "file_" + SessionID.GetSessionID(14); current_file_size = 0; if(current_file_filename != "") bw = BinaryWriter(File.Open( Httpd.GetConfig().Get("server/uploads_dir") +"/"+ current_file_tempname, FileMode.Create)); else bw = BinaryWriter.Null; file = true; } else when(split.Length == 2) { def (_,split1) = split_at('=',split[1]); file = false; current_param_name = split1.Substring(1,split1.Length-2); } } else if(l.ToUpper().StartsWith("CONTENT-TYPE")) () else when(l == "") mode = 1; def (lin,byt) = readline(); line = lin; bytes = byt; } } } if(!file) { multipart_params = (current_param_name,current_param_value.Substring(0,current_param_value.Length-1)) :: multipart_params; current_param_name = ""; current_param_value = ""; } else { if(!can_write) files = (current_file_name,current_file_filename,current_file_tempname,-1) :: files; else files = (current_file_name,current_file_filename,current_file_tempname,current_file_size-2) :: files; can_write = true; bw.Close(); } } else loop(length) } def (_, post_params, validity) = parse_get_params ("post?" + sb.ToString ()); (List.Append(post_params,multipart_params),files, validity) } /** * Parses request data */ private parse_request_data () : void { def request_data = ReadLine (); def delimiter = array [' ']; def split = request_data.Split (delimiter); request_info = RequestInfo.Invalid (); content_length = None (); session_id = None(); read_headers (); put_cookies_into_list(); if (split.Length >= 2) { def request_type = (split [0]).ToUpper (); def del = array[';']; def sess = split[1].Split(del); def request_url = sess [0]; when(sess.Length == 2) session_id = Some(sess[1]); def (request_url, request_params, request_params_validity) = parse_get_params (request_url); this.request_uri = request_url; this.protocol = split[2]; match (request_type) { | "GET" => if (split.Length > 3) logger.Log ("more than three tokens in a GET request") else if (!request_params_validity) logger.Log ("invalid GET parameters") else { if (request_url.StartsWith("/webapp/")) request_info = RequestInfo.DynamicGet (request_url , request_params) else { if (List.Length (request_params) > 0) logger.Log ("static GET request had GET parameters") else request_info = RequestInfo.StaticGet (request_url) } } construct_session(); | "POST" => if (List.Length (request_params) > 0) logger.Log ("POST request had GET parameters") else if (!request_url.StartsWith ("/webapp/") || !request_url.EndsWith(".xml")) logger.Log ("POST request to an invalid URL") else { def (post_params, files, post_validity) = parse_post_data (); if (!post_validity) logger.Log ("POST params malformed") else request_info = RequestInfo.Post (request_url, post_params,files) } construct_session(); /* | "HEAD" | "PUT" | "TRACE" | "DELETE" | "OPTIONS" => request_info = Other*/ | _ => logger.Log ("unknown request type") } } else logger.Log ("request contains less than two words") } /** * Reads the HTTP headers */ private read_headers () : void { mutable result = []; def loop () { match (ReadLine ()) { | "" => () | line => def (name, val) = split_at (':', line); def (name, val) = (name.ToLower ().Trim (), val.Trim ()); result = (name,val) :: result; when (name == "content-length") content_length = Some (System.Int32.Parse (val)); when (name == "content-type") content_type = Some (val); loop (); } }; loop (); headers = List.Rev (result) } /** * Puts all cookies sent with this request into list */ private put_cookies_into_list () : void { def cookie_header = GetHeader("cookie"); match(cookie_header) { | None => () | Some(header) => def delimiter1 = array[';']; def delimiter2 = array['=']; def split1 = header.Split(delimiter1); foreach(s : string in split1) { def split2 = s.Split(delimiter2); def name = split2[0]; def value = split2[1]; cookies = Cookie(name,value) :: cookies } } } /** * Checks, if there is session id in request, loads session, otherwise creates new */ private construct_session() : void { def si = GetCookie("SIOUXSESSIONID"); mutable sid = ""; match(si) { | None => { //when(Httpd.GetConfig().Get("server/url_rewriting")=="true") //{ match(request_info) { | RequestInfo.Post (_,_,_) | RequestInfo.DynamicGet(_,_) => match(session_id) { | None => () | Some (id) => sid = id } /* match(List.Find(parms,(fun (name,_) {name=="SIOUXSESSIONID"}))) { | None => () | Some ((_,id)) => sid = id }*/ | _ => () } //} } | Some(id) => { sid = id.Value; } } def sess = Httpd.GetSession(sid); match(sess) { | None => { session = Session(); Httpd.AddSession(session); } | Some (s) => { if(s.LastAccesedTime.AddMinutes((s.Timeout :> double)) > DateTime.Now && s.IsValid) { session = s; session.SetIsNotNew(); session.SetAccesTime() } else { Httpd.RemoveSession(s.SessionID); session = Session(); Httpd.AddSession(session); } } } } /* -- PRIVATE FIELDS --------------------------------------------------- */ private mutable protocol : string; private mutable session : Session; private mutable cookies : list [Cookie] ; private mutable request_stream : NetworkStream; private mutable request_info : RequestInfo; private mutable request_uri : string; private mutable request_time : DateTime; private mutable headers : list [string * string]; private mutable content_length : option [int]; private mutable content_type : option [string]; private mutable session_id : option [string]; private mutable logger : Logger; } }