Types/PipeScript.net/PSNodeJob.cs
namespace PipeScript.Net { using System; using System.ComponentModel; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Timers; using System.Threading; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Net; #if Windows using Microsoft.Win32; using System.Security.Principal; #endif using System.Web; public class PSNodeJob : Job { static RunspacePool runspacePool; PowerShell powerShellCommand; int bufferSize = 262144; uint poolSize = 3; TimeSpan sessionTimeout = TimeSpan.FromMinutes(15); Dictionary<string, string> MimeTypes = new Dictionary<string, string>(); RunspacePool _PSNodePool; ScriptBlock _PSNodeAction; ScriptBlock _FullPSNodeAction; PSNodeJob parentJob = null; AuthenticationSchemes authenticationType = AuthenticationSchemes.Anonymous; private string PSNodeScriptPreface = @" <#ScriptPreface#> "; static PSNodeJob() { InitialSessionState iss = InitialSessionState.CreateDefault(); //#StartWindowsOnly iss.ThreadOptions = PSThreadOptions.UseNewThread; iss.ApartmentState = ApartmentState.STA; //#EndWindowsOnly runspacePool = RunspaceFactory.CreateRunspacePool(iss); runspacePool.Open(); AppDomain.CurrentDomain.ProcessExit += PooledJob_Exiting; } RunspacePool PSNodePool { get { if (_PSNodePool == null || _PSNodePool.RunspacePoolStateInfo.State != RunspacePoolState.Opened) { InitialSessionState iss = InitialSessionState.CreateDefault(); if (this.ImportModule != null) { iss.ImportPSModule(this.ImportModule); } if (this.DeclareFunction != null) { foreach (FunctionInfo df in this.DeclareFunction) { iss.Commands.Add(new SessionStateFunctionEntry(df.Name, df.Definition)); } } if (this.DeclareAlias != null) { foreach (AliasInfo af in this.DeclareAlias) { iss.Commands.Add(new SessionStateAliasEntry(af.Name, af.Definition)); } } if (this.ImportTypeFile != null) { foreach (string typeFile in this.ImportTypeFile) { iss.Types.Add(new SessionStateTypeEntry(typeFile)); } } if (this.ImportFormatFile != null) { foreach (string formatFile in this.ImportFormatFile) { iss.Formats.Add(new SessionStateFormatEntry(formatFile)); } } _PSNodePool = RunspaceFactory.CreateRunspacePool(iss); //#StartWindowsOnly _PSNodePool.ThreadOptions = PSThreadOptions.UseNewThread; _PSNodePool.ApartmentState = System.Threading.ApartmentState.STA; //#EndWindowsOnly _PSNodePool.SetMaxRunspaces((int)PoolSize); _PSNodePool.Open(); } return _PSNodePool; } } public HttpListener Listener { get; set; } public bool AllowBrowseDirectory { get; set; } public bool AllowScriptExecution { get; set; } public string CORS { get; set;} public AuthenticationSchemes AuthenticationType { get { return authenticationType; } set { authenticationType = value; } } public FunctionInfo[] DeclareFunction { get; set; } public AliasInfo[] DeclareAlias { get; set; } public string[] ImportTypeFile { get; set; } public string[] ImportFormatFile { get; set; } public string[] FileBlacklist { get; set; } public int BufferSize { get { return bufferSize; } set { bufferSize = value; } } public string[] ImportModule { get; set; } public string[] ListenerLocation { get; set; } public uint PoolSize { get { return poolSize; } set { poolSize = value; } } public string RootPath { get; set; } public TimeSpan SessionTimeout { get { return sessionTimeout; } set { sessionTimeout = value; } } public ScriptBlock PSNodeAction { get { return _PSNodeAction; } set { _PSNodeAction = value; _FullPSNodeAction = ScriptBlock.Create(this.PSNodeScriptPreface + _PSNodeAction.ToString()); } } public AsyncCallback Callback { get { return new AsyncCallback(this.ListenerCallback); } } static void PooledJob_Exiting(object sender, EventArgs e) { runspacePool.Close(); runspacePool.Dispose(); runspacePool = null; } public PSNodeJob(string name, string command, ScriptBlock scriptBlock) : base(command, name) {} private PSNodeJob(ScriptBlock scriptBlock) {} public PSNodeJob(string name, string command, ScriptBlock scriptBlock, Hashtable parameters) : base(command, name) {} public PSNodeJob(string name, string command, ScriptBlock scriptBlock, Hashtable parameters, PSObject[] argumentList) : base(command, name) {} private PSNodeJob(string name, string command, ScriptBlock scriptBlock, Hashtable parameters, PSObject[] argumentList, bool isChildJob) : base(command, name) { if (! isChildJob) { PSNodeJob childJob = new PSNodeJob(name, command, scriptBlock, parameters, argumentList, true); childJob.StateChanged += new EventHandler<JobStateEventArgs>(childJob_StateChanged); this.ChildJobs.Add(childJob); } } void childJob_StateChanged(object sender, JobStateEventArgs e) { this.SetJobState(e.JobStateInfo.State); } /// <summary> /// Synchronizes Job State with Background Runspace /// </summary> /// <param name="sender"></param> /// <param name="e"></param> void powerShellCommand_InvocationStateChanged(object sender, PSInvocationStateChangedEventArgs e) { try { if (e.InvocationStateInfo.State == PSInvocationState.Failed) { ErrorRecord err = new ErrorRecord(e.InvocationStateInfo.Reason, "JobFailed", ErrorCategory.OperationStopped, this); Error.Add(err); } JobState js = (JobState)Enum.Parse(typeof(JobState), e.InvocationStateInfo.State.ToString(), true); this.SetJobState(js); } catch (Exception ex) { this.Error.Add(new ErrorRecord(ex, "PSNode.Unknown.Error", ErrorCategory.NotSpecified, this)); } } public void ServeFile(string fullPath, HttpListenerRequest request, HttpListenerResponse response) { if (File.Exists(fullPath)) { FileInfo fileInfo = new FileInfo(fullPath); if (FileBlacklist != null ){ foreach (string f in FileBlacklist) { WildcardPattern wp = new WildcardPattern(f, WildcardOptions.IgnoreCase); if (wp.IsMatch(fileInfo.FullName)) { return; } } } if (MimeTypes.ContainsKey(fileInfo.Extension.ToLower())) { response.ContentType = MimeTypes[fileInfo.Extension.ToLower()]; } int read = 0; if (request.HttpMethod.ToUpper() == "HEAD") { response.ContentLength64 = fileInfo.Length; response.OutputStream.Close(); return; } response.Headers["Accept-Ranges"] = "bytes"; long start = 0; long end = fileInfo.Length; if (!String.IsNullOrEmpty(request.Headers["Range"])) { var RangeMatch = Regex.Match(request.Headers["Range"], "bytes=(?<Start>\\d{1,})(-(?<End>\\d{1,})){0,1}"); if (RangeMatch != null && RangeMatch.Groups["Start"].Success && RangeMatch.Groups["End"].Success) { start = long.Parse(RangeMatch.Groups["Start"].ToString()); end = long.Parse(RangeMatch.Groups["End"].ToString()); } } using ( var fs = File.OpenRead(fullPath)) { if (start > 0 && end > 0) { byte[] buffer = new byte[this.BufferSize]; fs.Seek(start, SeekOrigin.Begin); read = fs.Read(buffer, 0, this.BufferSize); string contentRange = start.ToString() + "-" + (start + read - 1).ToString() + "/" + fs.Length.ToString(); response.StatusCode = 206; response.ContentLength64 = read; response.Headers.Add("Content-Range", contentRange); response.OutputStream.Write(buffer, 0, read); response.OutputStream.Close(); } else { response.ContentLength64 = fs.Length; fs.CopyTo(response.OutputStream); response.OutputStream.Close(); } } response.Close(); return; } } Dictionary<string, PSObject> UserSessions = new Dictionary<string, PSObject>(); Dictionary<string, string> ContentTypeCommands = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); System.Timers.Timer SessionTimer = null; Dictionary<string, Object> Application = new Dictionary<string, Object>(); public Cookie NewSessionCookie() { string sessionGuid = Guid.NewGuid().ToString(); Cookie sessionKey = new Cookie("SessionKey", sessionGuid); sessionKey.Expires = DateTime.UtcNow.AddMinutes(15); UserSessions[sessionGuid] = new PSObject(); UserSessions[sessionGuid].Properties.Add(new PSNoteProperty("Expires", sessionKey.Expires), true); UserSessions[sessionGuid].Properties.Add(new PSNoteProperty("SessionKey", sessionKey.Value), true); return sessionKey; } public void ServeScript(string powerShellScript, HttpListenerContext context) { HttpListenerRequest request = context.Request; HttpListenerResponse response = context.Response; PSObject session = null; // If we're serving a script, we'll want a SessionKey, so we can have _some_ sense of Web Session State. if (request.Cookies["SessionKey"] == null) { Cookie sessionCookie = NewSessionCookie(); response.AppendCookie(sessionCookie); session = UserSessions[sessionCookie.Value]; } else { string sessionKey = request.Cookies["SessionKey"].Value.ToString(); if (UserSessions.ContainsKey(sessionKey)) { session = UserSessions[sessionKey]; ((PSNoteProperty)UserSessions[sessionKey].Properties["Expires"]).Value = DateTime.UtcNow; } else { Cookie sessionCookie = NewSessionCookie(); response.AppendCookie(sessionCookie); session = UserSessions[sessionCookie.Value]; } } string contentType = request.ContentType; if (contentType != null) { contentType = contentType.ToLower(); } using ( PowerShell command = PowerShell.Create() .AddScript(powerShellScript, false) .AddArgument(request) .AddArgument(response) .AddArgument(context) //#StartWindowsOnly .AddArgument(context.User) //#EndWindowsOnly .AddArgument(session) .AddArgument(this.Application) .AddArgument(this)) { if (String.IsNullOrEmpty(contentType) || contentType.ToLower() == "text/html") { command.AddCommand("Out-Html"); } else if (request.ContentType != null && (contentType == "application/json" || contentType == "text/json")) { command.AddCommand("ConvertTo-Json").AddParameter("Compress", true); } else if (request.ContentType != null && contentType == "text/plain") { command.AddCommand("Out-String"); } else if (request.ContentType != null && contentType == "application/clixml") { command.AddScript("process { [Management.Automation.PSSerializer]::Serialize($_) }"); } command.RunspacePool = PSNodePool; if (! string.IsNullOrEmpty(this.CORS)) { response.Headers["Access-Control-Allow-Origin"] = this.CORS; } int offset = 0; try { foreach (PSObject psObject in command.Invoke<PSObject>()) { if (psObject.BaseObject == null) { continue; } byte[] buffer = null; string stringified = psObject.ToString(); buffer = System.Text.Encoding.UTF8.GetBytes(stringified); if (response.OutputStream.CanWrite) { response.OutputStream.Write(buffer, 0, buffer.Length); } offset += buffer.Length; buffer = null; } foreach (ErrorRecord err in command.Streams.Error) { string errorString = err.Exception.ToString() + ' ' + err.InvocationInfo.PositionMessage; byte[] buffer = System.Text.Encoding.UTF8.GetBytes(errorString); if (response.OutputStream.CanWrite) { response.OutputStream.Write(buffer, 0, buffer.Length); } offset += buffer.Length; buffer = null; } } catch (Exception ex) { byte[] buffer = System.Text.Encoding.UTF8.GetBytes(ex.Message); response.StatusCode = 500; if (response.OutputStream.CanWrite) { response.OutputStream.Write(buffer, 0, buffer.Length); } offset += buffer.Length; buffer = null; } finally { try { response.Close(); } catch (Exception ex) { this.Error.Add(new ErrorRecord(ex, "PSNode.Unknown.Error", ErrorCategory.NotSpecified, this)); } } } } public void ListenerCallback(IAsyncResult result) { try { HttpListener listener = (HttpListener)result.AsyncState; // Call EndGetContext to complete the asynchronous operation. HttpListenerContext context = listener.EndGetContext(result); HttpListenerRequest request = context.Request; // Obtain a response object. HttpListenerResponse response = context.Response; if (request.Url.Segments.Length > 2 && request.Url.Segments[1].ToLower() == "favicon.ico") { response.StatusCode = 200; response.Close(); return; } if (! String.IsNullOrEmpty(this.RootPath)) { string url = request.RawUrl.ToString(); url = url.Replace('/', System.IO.Path.DirectorySeparatorChar); url = HttpUtility.UrlDecode(url, Encoding.UTF8); url = url.Substring(1); string fullPath = string.IsNullOrEmpty(url) ? this.RootPath : Path.Combine(this.RootPath, url); if (Directory.Exists(fullPath) && AllowBrowseDirectory && fullPath != this.RootPath) { context.Response.ContentType = "text/html"; context.Response.ContentEncoding = Encoding.UTF8; using (var sw = new StreamWriter(context.Response.OutputStream)) { sw.WriteLine("<html>"); sw.WriteLine("<head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head>"); sw.WriteLine("<body><ul>"); foreach (string d in Directory.GetDirectories(fullPath)) { string link = d.Replace(this.RootPath, "").Replace(System.IO.Path.DirectorySeparatorChar, '/'); sw.WriteLine("<li><DIR> <a href=\"" + link + "\">" + Path.GetFileName(d) + "</a></li>"); } foreach (string f in Directory.GetFiles(fullPath)) { string link = f.Replace(this.RootPath, "").Replace(System.IO.Path.DirectorySeparatorChar, '/'); sw.WriteLine("<li><a href=\"" + link + "\">" + Path.GetFileName(f) + "</a></li>"); } sw.WriteLine("</ul></body></html>"); } context.Response.OutputStream.Close(); return; } if (request.Url.Segments[request.Url.Segments.Length - 1].Contains(".")) { if (File.Exists(fullPath)) { FileInfo fileInfo = new FileInfo(fullPath); if (this.AllowScriptExecution && fileInfo.Extension.ToLower() == ".ps1") { this.ServeScript(File.ReadAllText(fullPath), context); } else { this.ServeFile(fullPath, request, response); } return; } } } this.ServeScript(_FullPSNodeAction.ToString(), context); } catch (Exception e) { this.Error.Add(new ErrorRecord(e, e.Message, ErrorCategory.NotSpecified, e)); } } public void WriteOutput(PSObject item) { this.Output.Add(item); } private ElapsedEventHandler elapsedEventHandler = null; public void Start() { #if Windows WindowsIdentity current = System.Security.Principal.WindowsIdentity.GetCurrent(); WindowsPrincipal principal = new WindowsPrincipal(current); if (!principal.IsInRole(WindowsBuiltInRole.Administrator)) { throw new UnauthorizedAccessException(); } #endif if (SessionTimer != null) { SessionTimer.Stop(); SessionTimer.Elapsed -= elapsedEventHandler; } SessionTimer = new System.Timers.Timer(); SessionTimer.Interval = 5119; elapsedEventHandler = new ElapsedEventHandler(SessionTimerElapsed); SessionTimer.Elapsed += elapsedEventHandler; SessionTimer.Start(); if (! String.IsNullOrEmpty(this.RootPath)) { #if Windows RegistryKey hkcr = Microsoft.Win32.Registry.ClassesRoot; RegistryKey ctKey = hkcr.OpenSubKey("MIME\\Database\\Content Type"); foreach (string ctName in ctKey.GetSubKeyNames()) { object extension = ctKey.OpenSubKey(ctName).GetValue("Extension"); if (extension != null) { MimeTypes[extension.ToString().ToLower()]= ctName; } } #endif if (! MimeTypes.ContainsKey(".js")) { MimeTypes[".css"] = "text/javascript"; } if (! MimeTypes.ContainsKey(".css")) { MimeTypes[".css"] = "text/css"; } } Listener = new HttpListener(); //this.ListenerLocation = listenerLocation; //this.PSNodeAction = scriptblock; //this.AuthenticationType = authenticationType; int max = runspacePool.GetMaxRunspaces(); runspacePool.SetMaxRunspaces(max + 1); powerShellCommand = PowerShell.Create(); powerShellCommand.RunspacePool = runspacePool; powerShellCommand.Streams.Error = this.Error; powerShellCommand.Streams.Warning = this.Warning; powerShellCommand.Streams.Verbose = this.Verbose; powerShellCommand.Streams.Debug = this.Debug; //powerShellCommand.Streams.Progress = this.Progress; //this.Progress.DataAdded += new EventHandler<DataAddedEventArgs>(Progress_DataAdded); PSNodeJob nodeJob = this; if (this.parentJob != null) { nodeJob = this.parentJob; } powerShellCommand.AddScript(@" param($PSNodeJob, $listener) :ResetPSNode while (1) { $AuthenticationType = $PSNodeJob.AuthenticationType $ListenerLocation = $PSNodeJob.ListenerLocation if ($AuthenticationType) { $listener.AuthenticationSchemes =$AuthenticationType } foreach ($ll in $ListenerLocation) { $listener.Prefixes.Add($ListenerLocation); } # Start the listener to begin listening for requests. $listener.IgnoreWriteExceptions = $true; try { $listener.Start(); } catch { Write-Error -ErrorRecord $_ return } :NodeIsRunning while (1) { $result = $listener.BeginGetContext($PSNodeJob.Callback, $listener); if (-not $result) { return } $null = $result.AsyncWaitHandle.WaitOne(1kb); if (""$($PSNodeJob.ListenerLocation)"" -ne ""$ListenerLocation"" -or $PSNodeJob.AuthenticationType -ne $AuthenticationType) { if ($listener) { $listener.Stop() $listener.Close(); $listener.Prefixes.Clear(); [GC]::Collect() } continue ResetPSNode } } $listener.Stop() $listener.Close() [GC]::Collect() }", false).AddArgument(nodeJob).AddArgument(this.Listener); powerShellCommand.InvocationStateChanged += new EventHandler<PSInvocationStateChangedEventArgs>(powerShellCommand_InvocationStateChanged); powerShellCommand.BeginInvoke<Object, PSObject>(null, this.Output); } void SessionTimerElapsed(object sender, ElapsedEventArgs e) { try { List<string> toRemove = new List<string>(); foreach (PSObject sessionObject in UserSessions.Values) { try { DateTime sessionExpiresAt = (DateTime)sessionObject.Properties["Expires"].Value; if (sessionExpiresAt != null && sessionExpiresAt >= DateTime.UtcNow ) { string sessionKey = sessionObject.Properties["SessionKey"].Value as string; toRemove.Add(sessionKey); } } catch (Exception ex) { this.Error.Add(new ErrorRecord(ex, "PSNode.Unknown.Error", ErrorCategory.NotSpecified, this)); } } foreach (string tr in toRemove) { UserSessions.Remove(tr); } } catch (Exception ex) { this.Error.Add(new ErrorRecord(ex, "PSNode.Unknown.Error", ErrorCategory.NotSpecified, this)); } } /// <summary> /// If the comamnd is running, the job indicates it has more data /// </summary> public override bool HasMoreData { get { if (powerShellCommand.InvocationStateInfo.State == PSInvocationState.Running) { return true; } else { return false; } } } public override string Location { get { if (this.JobStateInfo.State == JobState.Running) { if (this.ListenerLocation == null) { return " "; } else { return this.ListenerLocation[0]; } } else { return " "; } } } public override string StatusMessage { get { return string.Empty; } } public override void StopJob() { try { powerShellCommand.BeginStop(null, null); if (Listener != null) { Listener.Stop(); Listener.Close(); } } catch (Exception ex) { this.Error.Add(new ErrorRecord(ex, "PSNode.StopJob.Error", ErrorCategory.CloseError, this)); } } protected override void Dispose(bool disposing) { if (disposing) { powerShellCommand.Dispose(); runspacePool.Close(); runspacePool.Dispose(); } base.Dispose(disposing); } } } |