#region Copyright 2013-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0 /* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #endregion using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Xml; using CSharpTest.Net.Http; namespace CSharpTest.Net.Commands { #region Http Attributes /// /// Specifies that an argument should be ignored for HTTP requests. /// [Serializable] [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)] public class HttpIgnoreAttribute : Attribute { /// /// A default value that applies to an argument when invoked from the http service /// public object HttpDefaultValue { get; set; } } /// /// Specifies that an argument should be fill in from an HTTP header. /// [Serializable] [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] public class HttpHeaderBindingAttribute : Attribute { private string _header; private string[] _regexMatch; /// The HTTP header to bind the attribute to, i.e. "Accept" public string Header { get { return _header; } } /// One or More regular expressions that match a specific pattern in the HTTP header, i.e. "^(?<=text/|application/)xml" public string[] RegexMatch { get { return _regexMatch; } } /// Constructs the attribute public HttpHeaderBindingAttribute(string header) : this(header, ".*") { } /// /// /// /// The HTTP header to bind the attribute to, i.e. "Accept" /// One or More regular expressions that match a specific pattern in the HTTP header, i.e. "^(?<=text/|application/)xml" public HttpHeaderBindingAttribute(string header, params string[] regexMatch) { _header = header; _regexMatch = regexMatch; } } /// /// Indicates that the argument is a filename that should be uploaded from the client /// [Serializable] [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] public class HttpRequestFileAttribute : Attribute { private readonly string _mimeType; /// Constructs the attribute public HttpRequestFileAttribute() : this(null) { } /// Constructs the attribute public HttpRequestFileAttribute(string mimeType) { _mimeType = mimeType; } /// Returns the mime type given, or null public string MimeType { get { return _mimeType; } } /// True to allow multiple files to be uploaded, callee will recieve directory path if more than one file is uploaded. public bool AllowMultiple { get; set; } } /// /// Indicates that the argument is an output filename of the command /// [Serializable] [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] public class HttpResponseFileAttribute : Attribute { private readonly string _extension; private readonly string _mimeType; /// Constructs the attribute public HttpResponseFileAttribute(string extension) : this(extension, "application/binary") { } /// Constructs the attribute public HttpResponseFileAttribute(string extension, string mimeType) { _extension = extension; _mimeType = mimeType; } /// Returns the mime type given, or null public string Extension { get { return _extension; } } /// Returns the mime type given, or null public string MimeType { get { return _mimeType; } } } /// /// Sets the response type to a fixed mime-type on a command /// [Serializable] [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false)] public class HttpResponseTypeAttribute : Attribute { private readonly string _mimeType; /// Constructs the attribute public HttpResponseTypeAttribute(string mimeType) {_mimeType = mimeType; } /// Returns the mime type given public string MimeType { get { return _mimeType; } } } #endregion partial class CommandInterpreter { /// /// Returns the list of HTTP prefixes that will be used for servicing the hosthttp command. /// public static string[] GetHttpPrefixes(bool allowRemote, int port, string rootFolder) { rootFolder = String.IsNullOrEmpty(rootFolder) ? "/" : (rootFolder.Trim().Trim('/', '\\') + '/'); rootFolder = '/' + rootFolder.TrimStart('/'); return allowRemote ? new[] { String.Format(@"http://{0}:{1}{2}", Environment.MachineName, port, rootFolder), String.Format(@"http://*:{0}{1}", port, rootFolder) } : new[] { String.Format(@"http://localhost:{0}{1}", port, rootFolder), String.Format(@"http://127.0.0.1:{0}{1}", port, rootFolder) }; } /// /// Starts a HttpListener for hosting the commands available on this instance over http/REST /// [Command("HostHttp", Category = "Built-in", Description = "Hosts a local http listener for RESTful access.", Visible = true), HttpIgnore] public void HostHttp( [Argument("port", DefaultValue = 8080, Description = "The port number to listen on.")] int port, [Argument("root", DefaultValue = "", Description = "Requests constrained within the URI folder.")] string rootFolder, [Argument("remote", DefaultValue = false, Description = "Allow requests from other machines.")] bool allowRemote, [Argument("browse", DefaultValue = false, Description = "Allow requests from other machines.")] bool startBrowser) { string[] prefixes = GetHttpPrefixes(allowRemote, port, rootFolder); using (HttpServer server = new CSharpTest.Net.Http.HttpServer(5)) { server.ProcessRequest += ServerOnProcessRequest; server.Start(prefixes); Console.WriteLine("Listening on {0}", prefixes[0]); if (startBrowser) System.Diagnostics.Process.Start(prefixes[0]); Console.WriteLine("Press [Enter] to quit"); Console.ReadLine(); server.Stop(); } } private void ServerOnProcessRequest(object sender, HttpContextEventArgs eventArg) { HttpListenerContext context = eventArg.Context; try { context.Response.Headers["Server"] = "C0D3"; context.Response.Headers["Expires"] = "0"; context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; string applicationPath = context.Request.Url.AbsolutePath.TrimEnd('/', '\\') + '/'; if (applicationPath.StartsWith(eventArg.Host.ApplicationPath, StringComparison.OrdinalIgnoreCase)) applicationPath = applicationPath.Substring(eventArg.Host.ApplicationPath.Length - 1); string[] segments = applicationPath.TrimEnd('/', '\\').Split('/', '\\'); HttpIgnoreAttribute ignore; ICommand cmd; bool execute = _commands.TryGetValue(context.Request.Url.AbsolutePath, out cmd); if (cmd == null && segments.Length == 2) { if (_commands.TryGetValue(segments[1], out cmd)) execute = (!String.IsNullOrEmpty(context.Request.Url.Query) || context.Request.HttpMethod.ToUpperInvariant() != "GET"); } if (cmd != null && cmd.TryGetAttribute(out ignore)) { throw new UnauthorizedAccessException("The command is not available."); } else if (cmd != null && execute) { ExecCommand(context, cmd); return; } else if (cmd != null || (cmd == null && segments.Length == 1)) { context.Response.ContentType = "text/html; charset=utf-8"; using (SwitchedOutputStream output = new SwitchedOutputStream(context.Response.OutputStream, ushort.MaxValue)) using (StreamWriter wtr = new StreamWriter(output)) { GenerateHtmlPage(wtr, eventArg.Host.ApplicationPath, cmd != null ? cmd.DisplayName : null); if (!output.OutputSent) context.Response.ContentLength64 = output.BufferPosition; output.Commit(); } return; } WriteErrorPage(context, 404, "Not Found", "The url is malformed or the command name is incorrect."); } catch (InterpreterException e) { try { WriteErrorPage(context, 400, "Bad Request", e.Message); } catch { } } catch (UnauthorizedAccessException e) { try { WriteErrorPage(context, 403, "Forbidden", e.Message); } catch { } } catch (Exception e) { try { WriteErrorPage(context, 500, "Internal Server Error", e.Message); } catch { } } } private void ExecCommand(HttpListenerContext context, ICommand cmd) { TextWriter stdOut; TextWriter stdErr = new StringWriter(); string tempdir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N").Substring(0, 16)); SwitchedOutputStream output = new SwitchedOutputStream(context.Response.OutputStream, ushort.MaxValue); try { string contentType = "text/plain"; IArgument reqFile = null; IArgument resFile = null; foreach (IArgument argument in cmd.Arguments) { HttpRequestFileAttribute reqFileAttr = null; HttpResponseFileAttribute resFileAttr = null; if (argument.TryGetAttribute(out reqFileAttr)) Check.Assert(null == Interlocked.Exchange(ref reqFile, argument)); if (argument.TryGetAttribute(out resFileAttr)) Check.Assert(null == Interlocked.Exchange(ref resFile, argument)); } if (reqFile != null && reqFile.Required && ( context.Request.HttpMethod.ToUpperInvariant() != "POST" || !context.Request.ContentType.StartsWith( "multipart/form-data", StringComparison.OrdinalIgnoreCase))) throw new InvalidOperationException(); List args = new List(); args.Add(cmd.DisplayName); HttpResponseTypeAttribute ctypeAttr; if (cmd.TryGetAttribute(out ctypeAttr) && !String.IsNullOrEmpty(ctypeAttr.MimeType)) contentType = ctypeAttr.MimeType; GetArguments(context, cmd, tempdir, ref contentType, args); if (resFile != null) { HttpResponseFileAttribute fattr; resFile.TryGetAttribute(out fattr); Directory.CreateDirectory(tempdir); string tempPath = Path.Combine(tempdir, cmd.DisplayName + fattr.Extension); args.Add(String.Format("/{0}={1}", resFile.DisplayName, tempPath)); using (stdOut = new StreamWriter(output, Encoding.UTF8)) Run(args.ToArray(), stdOut, stdErr, TextReader.Null); if (output.OutputSent) throw new ApplicationException("Headers already sent."); context.Response.ContentType = fattr.MimeType ?? "application/binary"; context.Response.Headers.Add("Content-Disposition", String.Format("attachment; filename=\"{0}{1}\"", cmd.DisplayName, fattr.Extension)); using (Stream ostream = context.Response.OutputStream) using (Stream istream = new FileStream(tempPath, FileMode.Open, FileAccess.Read, FileShare.None)) { int len; byte[] buffer = new byte[ushort.MaxValue]; while (0 != (len = istream.Read(buffer, 0, buffer.Length))) ostream.Write(buffer, 0, len); } } else { context.Response.ContentType = contentType + (contentType.Contains("text") || contentType.Contains("xml") || contentType.Contains("json") ? "; charset=utf-8" : ""); using (stdOut = new StreamWriter(output, Encoding.UTF8)) Run(args.ToArray(), stdOut, stdErr, TextReader.Null); } if (!output.OutputSent) context.Response.ContentLength64 = output.BufferPosition; output.Commit(); } catch (InterpreterException) { throw; } catch (Exception e) { if (output.OutputSent) { using (stdOut = new StreamWriter(output, Encoding.UTF8)) { stdOut.Write("EXCEPTION: "); stdOut.WriteLine(e.Message); stdOut.WriteLine(stdErr.ToString()); } output.Commit(); } else WriteErrorPage(context, 500, "Internal Server Error", e.Message, stdErr.ToString(), output.ToString()); } finally { if (Directory.Exists(tempdir)) { try { Directory.Delete(tempdir, true); } catch { } } } } private void GetArguments(HttpListenerContext context, ICommand cmd, string tempdir, ref string contentType, List args) { string acceptArgument = null; string originalType = contentType; HttpIgnoreAttribute ignored; Dictionary exists = new Dictionary(); foreach (IArgument arg in cmd.Arguments) { string value = null; HttpHeaderBindingAttribute hdr; if (arg.TryGetAttribute(out hdr)) { string text = context.Request.Headers[hdr.Header]; if (text == null) value = null; else if (StringComparer.OrdinalIgnoreCase.Equals(hdr.Header, "Accept")) { acceptArgument = arg.DisplayName; foreach (string x in text.Split(',')) { string part = x.Trim(); if (part.IndexOf(';') > 0) part = part.Substring(0, part.IndexOf(';')).Trim(); foreach (string exp in hdr.RegexMatch) { Match m = Regex.Match(part, exp); if (m.Success) { contentType = part; value = m.Value; break; } } if (value != null) break; } } else { foreach (string exp in hdr.RegexMatch) { Match m = Regex.Match(text, exp); if (m.Success) { value = m.Value; break; } } } } exists.Add(arg.DisplayName, value); } string query = context.Request.Url.Query; //application/x-www-form-urlencoded if (context.Request.HttpMethod.ToUpperInvariant() == "POST" && context.Request.ContentType != null && context.Request.ContentType.Contains("application/x-www-form-urlencoded")) { using (TextReader r = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding ?? Encoding.UTF8)) query += "&" + r.ReadToEnd(); } //multipart/form-data if (context.Request.HttpMethod.ToUpperInvariant() == "POST" && context.Request.ContentType != null && context.Request.ContentType.StartsWith("multipart/form-data", StringComparison.OrdinalIgnoreCase)) { MimeMultiPartData form = new MimeMultiPartData(context.Request.InputStream, context.Request.Headers); foreach (KeyValuePair set in form.ToDictionary()) { if (exists.ContainsKey(set.Key) && !String.IsNullOrEmpty(set.Value)) exists[set.Key] = set.Value; } foreach (IArgument arg in cmd.Arguments) { HttpRequestFileAttribute a; if (arg.TryGetAttribute(out a)) { List parts = new List(form.GetAllPartsByName(arg.DisplayName)); Directory.CreateDirectory(tempdir); foreach (MimeMessagePart part in parts) File.WriteAllBytes(Path.Combine(tempdir, part.FileName), part.Body); if (parts.Count == 1) exists[arg.DisplayName] = Path.Combine(tempdir, parts[0].FileName); else if (parts.Count > 1) exists[arg.DisplayName] = tempdir; } } } foreach (string arg in query.TrimStart('?').Split('&')) { string[] parts = arg.Split(new char[] { '=' }, 2); if (parts.Length == 2 && parts[1].Length > 0) { parts[0] = Uri.UnescapeDataString(parts[0]); parts[1] = Uri.UnescapeDataString(parts[1]); if (exists.ContainsKey(parts[0])) { exists[parts[0]] = parts[1]; if (StringComparer.OrdinalIgnoreCase.Equals(acceptArgument, parts[0])) contentType = originalType; } } } foreach (IArgument arg in cmd.Arguments) if (arg.TryGetAttribute(out ignored)) exists[arg.DisplayName] = ignored.HttpDefaultValue == null ? null : ignored.HttpDefaultValue.ToString(); foreach (KeyValuePair set in exists) { if (set.Value != null) args.Add(String.Format("/{0}={1}", set.Key, set.Value)); } } private void WriteErrorPage(HttpListenerContext context, int code, string status, string errorDesc, params string[] messages) { context.Response.StatusCode = code; context.Response.StatusDescription = status; context.Response.ContentType = "text/html; charset=utf-8"; using (XmlTextWriter w = new XmlTextWriter(context.Response.OutputStream, Encoding.UTF8)) { w.Formatting = System.Xml.Formatting.Indented; w.WriteStartElement("html"); w.WriteStartElement("head"); w.WriteElementString("title", code + " - " + status); w.WriteEndElement(); w.WriteStartElement("body"); { w.WriteElementString("h1", code + " - " + status); w.WriteStartElement("p"); w.WriteElementString("strong", errorDesc); w.WriteEndElement(); foreach (string message in messages) w.WriteElementString("p", message); } w.WriteEndElement(); w.WriteEndElement(); w.Flush(); } } private void GenerateHtmlPage(TextWriter sw, string appRoot, string itemName) { List items = new List(); if (itemName != null) items.Add(_commands[itemName]); else items.AddRange(Commands); using (XmlTextWriter w = new XmlTextWriter(sw)) { w.Formatting = System.Xml.Formatting.Indented; w.WriteStartElement("html"); w.WriteStartElement("head"); w.WriteElementString("title", Constants.ProcessName + " Help"); w.WriteEndElement(); w.WriteStartElement("body"); { HttpIgnoreAttribute ignored; if (items.Count == 1) { Command command = (Command)items[0]; HttpResponseFileAttribute resFileAttr = null; HttpRequestFileAttribute reqFileAttr = null; foreach (IArgument arg in command.Arguments) { if (reqFileAttr == null) arg.TryGetAttribute(out reqFileAttr); if (resFileAttr == null) arg.TryGetAttribute(out resFileAttr); } w.WriteElementString("h1", command.DisplayName); w.WriteElementString("p", command.Description); w.WriteStartElement("a"); w.WriteAttributeString("href", appRoot); w.WriteString("<< Back to Top"); w.WriteEndElement(); w.WriteStartElement("form"); { w.WriteAttributeString("name", command.DisplayName.ToLowerInvariant() + "Form"); if (resFileAttr == null) w.WriteAttributeString("onsubmit", "document.getElementById('" + command.DisplayName.ToLowerInvariant() + "Submit').disabled = true;"); w.WriteAttributeString("action", appRoot + command.DisplayName + "/"); if (reqFileAttr != null) { w.WriteAttributeString("method", "post"); w.WriteAttributeString("enctype", "multipart/form-data"); } else { w.WriteAttributeString("method", "get"); } w.WriteStartElement("ul"); w.WriteAttributeString("style", "list-style-type: none;"); w.WriteStartElement("li"); w.WriteElementString("strong", "Arguments:"); w.WriteEndElement(); foreach (Argument arg in command.Arguments) { if (arg.Visible == false || arg.Type == typeof(ICommandInterpreter)) continue; if (arg.IsAllArguments) continue; HttpHeaderBindingAttribute hdr; if (arg.TryGetAttribute(out resFileAttr) || arg.TryGetAttribute(out hdr) || arg.TryGetAttribute(out ignored)) continue; w.WriteStartElement("li"); w.WriteStartElement("strong"); w.WriteAttributeString("style", "display:inline-block; width: 100px; text-align: right;"); w.WriteString(arg.DisplayName); w.WriteEndElement(); w.WriteStartElement("input"); w.WriteAttributeString("name", arg.DisplayName); w.WriteAttributeString("style", "width: 300px;"); if (arg.Required) w.WriteAttributeString("required", "required"); if (arg.TryGetAttribute(out reqFileAttr)) { w.WriteAttributeString("type", "file"); if (reqFileAttr.AllowMultiple) w.WriteAttributeString("multiple", "multiple"); if (!String.IsNullOrEmpty(reqFileAttr.MimeType)) w.WriteAttributeString("accept", reqFileAttr.MimeType); } else { w.WriteAttributeString("type", "text"); w.WriteAttributeString("value", String.Format("{0}", arg.DefaultValue)); } w.WriteEndElement(); w.WriteString(arg.Required ? " * " : " - "); w.WriteString(arg.Description.TrimEnd('.') + "."); w.WriteEndElement(); } w.WriteStartElement("li"); w.WriteAttributeString("style", "padding-top:10px;"); { w.WriteStartElement("strong"); w.WriteAttributeString("style", "display:inline-block; width: 100px; text-align: right;"); w.WriteString(" "); w.WriteEndElement(); w.WriteStartElement("input"); w.WriteAttributeString("id", command.DisplayName.ToLowerInvariant() + "Submit"); w.WriteAttributeString("name", command.DisplayName.ToLowerInvariant() + "Submit"); w.WriteAttributeString("type", "submit"); w.WriteAttributeString("value", command.DisplayName); w.WriteEndElement(); } w.WriteEndElement(); w.WriteEndElement(); } w.WriteEndElement(); } else { w.WriteElementString("h1", Constants.ProductName + " " + Constants.ProductVersion); w.WriteElementString("p", System.Diagnostics.FileVersionInfo.GetVersionInfo(Constants.ProcessFile).Comments); Dictionary> all = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (IDisplayInfo item in items) { if (item.ReflectedType == typeof(CommandInterpreter) || item.ReflectedType == typeof(BuiltInCommands.BuiltIn)) continue; if (item.TryGetAttribute(out ignored)) continue; List l; if (!all.TryGetValue(item.Category, out l)) all.Add(item.Category, l = new List()); l.Add(item); } List keys = new List(all.Keys); keys.Sort(); foreach (string key in keys) { List list = all[key]; w.WriteElementString("h2", key + ":"); foreach (IDisplayInfo info in list) { ICommand command = info as ICommand; if (command == null || !info.Visible) continue; w.WriteStartElement("li"); w.WriteStartElement("a"); w.WriteAttributeString("href", appRoot + command.DisplayName + '/'); w.WriteElementString("b", command.DisplayName); w.WriteEndElement(); w.WriteString(" - "); w.WriteString(info.Description.TrimEnd('.') + "."); w.WriteEndElement(); } } } } foreach (AssemblyCopyrightAttribute copy in Constants.EntryAssembly.GetCustomAttributes(typeof(AssemblyCopyrightAttribute), true)) w.WriteElementString("p", copy.Copyright); w.WriteEndElement(); w.WriteEndElement(); w.Flush(); } } #region SwitchedOutputStream private class SwitchedOutputStream : Stream { private int _pos; private bool _outputSent; private byte[] _bytes; private Stream _output; public SwitchedOutputStream(Stream outputStream, int bufferSize) { _pos = 0; _bytes = new byte[bufferSize]; _output = outputStream; } public bool OutputSent { get { return _outputSent; } } public override bool CanRead { get { return false; } } public override bool CanSeek { get { return false; } } public override bool CanWrite { get { return true; } } public override void Flush() { } public override long Length { get { throw new NotSupportedException(); } } public override long Position { get { throw new NotSupportedException(); } set { throw new NotSupportedException(); } } public int BufferPosition { get { return _pos; } } public override int Read(byte[] buffer, int offset, int count) { throw new InvalidOperationException(); } public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } public override void SetLength(long value) { throw new NotSupportedException(); } public override void Write(byte[] buffer, int offset, int count) { if (count + _pos >= _bytes.Length) { _outputSent = true; _output.Write(_bytes, 0, Interlocked.Exchange(ref _pos, 0)); } if (count + _pos >= _bytes.Length) { _outputSent = true; _output.Write(buffer, offset, count); } else { Buffer.BlockCopy(buffer, offset, _bytes, _pos, count); _pos += count; } } public override string ToString() { return Encoding.UTF8.GetString(_bytes, 0, _pos); } public void Commit() { _output.Write(_bytes, 0, _pos); _bytes = null; _output.Flush(); _output.Dispose(); } } #endregion } }