123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790 |
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Net;
- using System.Net.Sockets;
- using System.Text;
- using OSHttpServer.Exceptions;
- using OSHttpServer.Parser;
- using System.Net.Security;
- using System.Security.Cryptography.X509Certificates;
- using OpenMetaverse;
- namespace OSHttpServer
- {
- /// <summary>
- /// Contains a connection to a browser/client.
- /// </summary>
- /// <remarks>
- /// Remember to <see cref="Start"/> after you have hooked the <see cref="RequestReceived"/> event.
- /// </remarks>
- public class HttpClientContext : IHttpClientContext, IDisposable
- {
- const int MAXREQUESTS = 20;
- const int MAXKEEPALIVE = 120000;
- static private int basecontextID;
- Queue<HttpRequest> m_requests;
- object m_requestsLock = new object();
- public int m_maxRequests = MAXREQUESTS;
- public bool m_waitingResponse;
- private readonly byte[] m_ReceiveBuffer;
- private int m_ReceiveBytesLeft;
- private ILogWriter m_log;
- private readonly IHttpRequestParser m_parser;
- private Socket m_sock;
- public bool Available = true;
- public bool StreamPassedOff = false;
- public int LastActivityTimeMS = 0;
- public int MonitorKeepaliveStartMS = 0;
- public bool TriggerKeepalive = false;
- public int TimeoutFirstLine = 10000; // 10 seconds
- public int TimeoutRequestReceived = 30000; // 30 seconds
- public int TimeoutMaxIdle = 180000; // 3 minutes
- public int m_TimeoutKeepAlive = 30000;
- public bool FirstRequestLineReceived;
- public bool FullRequestReceived;
- private bool isSendingResponse = false;
- private bool m_isClosing = false;
- private HttpRequest m_currentRequest;
- private HttpResponse m_currentResponse;
- public int contextID { get; private set; }
- public int TimeoutKeepAlive
- {
- get { return m_TimeoutKeepAlive; }
- set
- {
- m_TimeoutKeepAlive = (value > MAXKEEPALIVE) ? MAXKEEPALIVE : value;
- }
- }
- public bool IsClosing
- {
- get { return m_isClosing;}
- }
- public int MaxRequests
- {
- get { return m_maxRequests; }
- set
- {
- if(value <= 1)
- m_maxRequests = 1;
- else
- m_maxRequests = value > MAXREQUESTS ? MAXREQUESTS : value;
- }
- }
- public bool IsSending()
- {
- return isSendingResponse;
- }
- public bool StopMonitoring;
- public IPEndPoint LocalIPEndPoint {get; set;}
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpClientContext"/> class.
- /// </summary>
- /// <param name="secured">true if the connection is secured (SSL/TLS)</param>
- /// <param name="remoteEndPoint">client that connected.</param>
- /// <param name="stream">Stream used for communication</param>
- /// <param name="parserFactory">Used to create a <see cref="IHttpRequestParser"/>.</param>
- /// <param name="bufferSize">Size of buffer to use when reading data. Must be at least 4096 bytes.</param>
- /// <exception cref="SocketException">If <see cref="Socket.BeginReceive(byte[],int,int,SocketFlags,AsyncCallback,object)"/> fails</exception>
- /// <exception cref="ArgumentException">Stream must be writable and readable.</exception>
- public HttpClientContext(bool secured, IPEndPoint remoteEndPoint,
- Stream stream, ILogWriter m_logWriter, Socket sock)
- {
- if (!stream.CanWrite || !stream.CanRead)
- throw new ArgumentException("Stream must be writable and readable.");
- LocalIPEndPoint = remoteEndPoint;
- m_log = m_logWriter;
- m_isClosing = false;
- m_currentRequest = new HttpRequest(this);
- m_parser = new HttpRequestParser(m_log);
- m_parser.RequestCompleted += OnRequestCompleted;
- m_parser.RequestLineReceived += OnRequestLine;
- m_parser.HeaderReceived += OnHeaderReceived;
- m_parser.BodyBytesReceived += OnBodyBytesReceived;
- IsSecured = secured;
- m_stream = stream;
- m_sock = sock;
- m_ReceiveBuffer = new byte[16384];
- m_requests = new Queue<HttpRequest>();
- SSLCommonName = "";
- if (secured)
- {
- SslStream _ssl = (SslStream)m_stream;
- X509Certificate _cert1 = _ssl.RemoteCertificate;
- if (_cert1 != null)
- {
- X509Certificate2 _cert2 = new X509Certificate2(_cert1);
- if (_cert2 != null)
- SSLCommonName = _cert2.GetNameInfo(X509NameType.SimpleName, false);
- }
- }
- ++basecontextID;
- if (basecontextID <= 0)
- basecontextID = 1;
- contextID = basecontextID;
- }
- public bool CanSend()
- {
- if (contextID < 0 || m_isClosing)
- return false;
- if (m_stream == null || m_sock == null || !m_sock.Connected)
- return false;
- return true;
- }
- /// <summary>
- /// Process incoming body bytes.
- /// </summary>
- /// <param name="sender"><see cref="IHttpRequestParser"/></param>
- /// <param name="e">Bytes</param>
- protected virtual void OnBodyBytesReceived(object sender, BodyEventArgs e)
- {
- m_currentRequest.AddToBody(e.Buffer, e.Offset, e.Count);
- }
- private static readonly byte[] OSUTF8expect = osUTF8.GetASCIIBytes("expect");
- /// <summary>
- ///
- /// </summary>
- /// <param name="sender"></param>
- /// <param name="e"></param>
- protected virtual void OnHeaderReceived(object sender, HeaderEventArgs e)
- {
- if (e.Name.ACSIILowerEquals(OSUTF8expect) && e.Value.Contains("100-continue"))
- {
- lock (m_requestsLock)
- {
- if (m_maxRequests == MAXREQUESTS)
- Respond("HTTP/1.1", HttpStatusCode.Continue, null);
- }
- }
- m_currentRequest.AddHeader(e.Name.ToString(), e.Value);
- }
- private void OnRequestLine(object sender, RequestLineEventArgs e)
- {
- m_currentRequest.Method = e.HttpMethod;
- m_currentRequest.HttpVersion = e.HttpVersion;
- m_currentRequest.UriPath = e.UriPath;
- m_currentRequest.AddHeader("remote_addr", LocalIPEndPoint.Address.ToString());
- m_currentRequest.AddHeader("remote_port", LocalIPEndPoint.Port.ToString());
- m_currentRequest.ArrivalTS = ContextTimeoutManager.GetTimeStamp();
- FirstRequestLineReceived = true;
- TriggerKeepalive = false;
- MonitorKeepaliveStartMS = 0;
- LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount();
- }
- /// <summary>
- /// Start reading content.
- /// </summary>
- /// <remarks>
- /// Make sure to call base.Start() if you override this method.
- /// </remarks>
- public virtual void Start()
- {
- try
- {
- m_stream.BeginRead(m_ReceiveBuffer, 0, m_ReceiveBuffer.Length, OnReceive, null);
- }
- catch (IOException err)
- {
- LogWriter.Write(this, LogPrio.Debug, err.ToString());
- }
- }
- /// <summary>
- /// Clean up context.
- /// </summary>
- /// <remarks>
- /// </remarks>
- public virtual void Cleanup()
- {
- if (StreamPassedOff)
- return;
- contextID = -100;
- if (m_stream != null)
- {
- m_stream.Close();
- m_stream = null;
- m_sock = null;
- }
- m_currentRequest?.Clear();
- m_currentRequest = null;
- m_currentResponse?.Clear();
- m_currentResponse = null;
- if(m_requests != null)
- {
- while(m_requests.Count > 0)
- {
- HttpRequest req = m_requests.Dequeue();
- req.Clear();
- }
- m_requests = null;
- }
- m_parser.Clear();
- FirstRequestLineReceived = false;
- FullRequestReceived = false;
- LastActivityTimeMS = 0;
- StopMonitoring = true;
- MonitorKeepaliveStartMS = 0;
- TriggerKeepalive = false;
- isSendingResponse = false;
- m_ReceiveBytesLeft = 0;
- }
- public void Close()
- {
- Dispose();
- }
- /// <summary>
- /// Using SSL or other encryption method.
- /// </summary>
- [Obsolete("Use IsSecured instead.")]
- public bool Secured
- {
- get { return IsSecured; }
- }
- /// <summary>
- /// Using SSL or other encryption method.
- /// </summary>
- public bool IsSecured { get; internal set; }
- // returns the SSL commonName of remote Certificate
- public string SSLCommonName { get; internal set; }
- /// <summary>
- /// Specify which logger to use.
- /// </summary>
- public ILogWriter LogWriter
- {
- get { return m_log; }
- set
- {
- m_log = value ?? NullLogWriter.Instance;
- m_parser.LogWriter = m_log;
- }
- }
- private Stream m_stream;
- /// <summary>
- /// Gets or sets the network stream.
- /// </summary>
- internal Stream Stream
- {
- get { return m_stream; }
- set { m_stream = value; }
- }
- /// <summary>
- /// Disconnect from client
- /// </summary>
- /// <param name="error">error to report in the <see cref="Disconnected"/> event.</param>
- public void Disconnect(SocketError error)
- {
- // disconnect may not throw any exceptions
- try
- {
- try
- {
- if (m_stream != null)
- {
- if (error == SocketError.Success)
- {
- try
- {
- m_stream.Flush();
- }
- catch { }
- }
- m_stream.Close();
- m_stream = null;
- }
- m_sock = null;
- }
- catch { }
- Disconnected?.Invoke(this, new DisconnectedEventArgs(error));
- }
- catch (Exception err)
- {
- LogWriter.Write(this, LogPrio.Error, "Disconnect threw an exception: " + err);
- }
- }
- private void OnReceive(IAsyncResult ar)
- {
- try
- {
- int bytesRead = 0;
- if (m_stream == null)
- return;
- try
- {
- bytesRead = m_stream.EndRead(ar);
- }
- catch (NullReferenceException)
- {
- Disconnect(SocketError.ConnectionReset);
- return;
- }
- if (bytesRead == 0)
- {
- Disconnect(SocketError.Success);
- return;
- }
- if (m_isClosing)
- return;
- m_ReceiveBytesLeft += bytesRead;
- int offset = m_parser.Parse(m_ReceiveBuffer, 0, m_ReceiveBytesLeft);
- if (m_stream == null)
- return; // "Connection: Close" in effect.
- while (offset != 0)
- {
- int nextBytesleft = m_ReceiveBytesLeft - offset;
- if (nextBytesleft <= 0)
- break;
- int nextOffset = m_parser.Parse(m_ReceiveBuffer, offset, nextBytesleft);
- if (m_stream == null)
- return; // "Connection: Close" in effect.
- if (nextOffset == 0)
- break;
- offset = nextOffset;
- }
- // copy unused bytes to the beginning of the array
- if (offset > 0 && m_ReceiveBytesLeft > offset)
- Buffer.BlockCopy(m_ReceiveBuffer, offset, m_ReceiveBuffer, 0, m_ReceiveBytesLeft - offset);
- m_ReceiveBytesLeft -= offset;
- if (StreamPassedOff)
- return; //?
- m_stream.BeginRead(m_ReceiveBuffer, m_ReceiveBytesLeft, m_ReceiveBuffer.Length - m_ReceiveBytesLeft, OnReceive, null);
- }
- catch (BadRequestException err)
- {
- LogWriter.Write(this, LogPrio.Warning, "Bad request, responding with it. Error: " + err);
- try
- {
- Respond("HTTP/1.1", HttpStatusCode.BadRequest, err.Message);
- }
- catch (Exception err2)
- {
- LogWriter.Write(this, LogPrio.Fatal, "Failed to reply to a bad request. " + err2);
- }
- //Disconnect(SocketError.NoRecovery);
- Disconnect(SocketError.Success); // try to flush
- }
- catch (HttpException err)
- {
- LogWriter.Write(this, LogPrio.Warning, "Bad request, responding with it. Error: " + err.Message);
- try
- {
- Respond("HTTP/1.1", err.HttpStatusCode, err.Message);
- }
- catch (Exception err2)
- {
- LogWriter.Write(this, LogPrio.Fatal, "Failed to reply to a bad request. " + err2);
- }
- //Disconnect(SocketError.NoRecovery);
- Disconnect(SocketError.Success); // try to flush
- }
- catch (IOException err)
- {
- LogWriter.Write(this, LogPrio.Debug, "Failed to end receive: " + err.Message);
- if (err.InnerException is SocketException)
- Disconnect((SocketError)((SocketException)err.InnerException).ErrorCode);
- else
- Disconnect(SocketError.ConnectionReset);
- }
- catch (ObjectDisposedException err)
- {
- LogWriter.Write(this, LogPrio.Debug, "Failed to end receive : " + err.Message);
- Disconnect(SocketError.NotSocket);
- }
- catch (NullReferenceException err)
- {
- LogWriter.Write(this, LogPrio.Debug, "Failed to end receive : NullRef: " + err.Message);
- Disconnect(SocketError.NoRecovery);
- }
- catch (Exception err)
- {
- LogWriter.Write(this, LogPrio.Debug, "Failed to end receive: " + err.Message);
- Disconnect(SocketError.NoRecovery);
- }
- }
- private void OnRequestCompleted(object source, EventArgs args)
- {
- TriggerKeepalive = false;
- MonitorKeepaliveStartMS = 0;
- FullRequestReceived = true;
- LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount();
- if (m_maxRequests <= 0 || RequestReceived == null)
- return;
- if (--m_maxRequests == 0)
- m_currentRequest.Connection = ConnectionType.Close;
- if(m_currentRequest.Uri == null)
- {
- // should not happen
- try
- {
- Uri uri = new Uri(m_currentRequest.Secure ? "https://" : "http://" + m_currentRequest.UriPath);
- m_currentRequest.Uri = uri;
- m_currentRequest.UriPath = uri.AbsolutePath;
- }
- catch
- {
- return;
- }
- }
- // load cookies if they exist
- if(m_currentRequest.Headers["cookie"] != null)
- m_currentRequest.SetCookies(new RequestCookies(m_currentRequest.Headers["cookie"]));
- m_currentRequest.Body.Seek(0, SeekOrigin.Begin);
- HttpRequest currentRequest = m_currentRequest;
- m_currentRequest = new HttpRequest(this);
- lock (m_requestsLock)
- {
- if(m_waitingResponse)
- {
- m_requests.Enqueue(currentRequest);
- return;
- }
- else
- m_waitingResponse = true;
- }
- RequestReceived?.Invoke(this, new RequestEventArgs(currentRequest));
- }
- public void StartSendResponse(HttpResponse response)
- {
- LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount();
- isSendingResponse = true;
- m_currentResponse = response;
- ContextTimeoutManager.EnqueueSend(this, response.Priority);
- }
- public bool TrySendResponse(int bytesLimit)
- {
- if (m_currentResponse == null)
- return false;
- try
- {
- if (m_currentResponse.Sent)
- return false;
- if(!CanSend())
- return false;
- LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount();
- return m_currentResponse.SendNextAsync(bytesLimit);
- }
- catch
- {
- return false;
- }
- }
- public void ContinueSendResponse()
- {
- if(m_currentResponse == null)
- return;
- LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount();
- ContextTimeoutManager.EnqueueSend(this, m_currentResponse.Priority);
- }
- public void EndSendResponse(uint requestID, ConnectionType ctype)
- {
- isSendingResponse = false;
- m_currentResponse?.Clear();
- m_currentResponse = null;
- lock (m_requestsLock)
- m_waitingResponse = false;
- if(contextID < 0)
- return;
- if (ctype == ConnectionType.Close)
- {
- m_isClosing = true;
- m_requests.Clear();
- TriggerKeepalive = true;
- return;
- }
- else
- {
- if (Stream == null || !Stream.CanWrite)
- return;
- LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount();
- HttpRequest nextRequest = null;
- lock (m_requestsLock)
- {
- if (m_requests != null && m_requests.Count > 0)
- nextRequest = m_requests.Dequeue();
- if (nextRequest != null && RequestReceived != null)
- {
- m_waitingResponse = true;
- TriggerKeepalive = false;
- }
- else
- TriggerKeepalive = true;
- }
- if (nextRequest != null)
- RequestReceived?.Invoke(this, new RequestEventArgs(nextRequest));
- }
- ContextTimeoutManager.PulseWaitSend();
- }
- /// <summary>
- /// Send a response.
- /// </summary>
- /// <param name="httpVersion">Either <see cref="HttpHelper.HTTP10"/> or <see cref="HttpHelper.HTTP11"/></param>
- /// <param name="statusCode">HTTP status code</param>
- /// <param name="reason">reason for the status code.</param>
- /// <param name="body">HTML body contents, can be null or empty.</param>
- /// <param name="contentType">A content type to return the body as, i.e. 'text/html' or 'text/plain', defaults to 'text/html' if null or empty</param>
- /// <exception cref="ArgumentException">If <paramref name="httpVersion"/> is invalid.</exception>
- public void Respond(string httpVersion, HttpStatusCode statusCode, string reason, string body, string contentType)
- {
- LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount();
- if (string.IsNullOrEmpty(reason))
- reason = statusCode.ToString();
- byte[] buffer;
- if(string.IsNullOrEmpty(body))
- buffer = Encoding.ASCII.GetBytes(httpVersion + " " + (int)statusCode + " " + reason + "\r\n\r\n");
- else
- {
- if (string.IsNullOrEmpty(contentType))
- contentType = "text/html";
- buffer = Encoding.UTF8.GetBytes(
- string.Format("{0} {1} {2}\r\nContent-Type: {5}\r\nContent-Length: {3}\r\n\r\n{4}",
- httpVersion, (int)statusCode, reason ?? statusCode.ToString(),
- body.Length, body, contentType));
- }
- Send(buffer);
- }
- /// <summary>
- /// Send a response.
- /// </summary>
- /// <param name="httpVersion">Either <see cref="HttpHelper.HTTP10"/> or <see cref="HttpHelper.HTTP11"/></param>
- /// <param name="statusCode">HTTP status code</param>
- /// <param name="reason">reason for the status code.</param>
- public void Respond(string httpVersion, HttpStatusCode statusCode, string reason)
- {
- if (string.IsNullOrEmpty(reason))
- reason = statusCode.ToString();
- byte[] buffer = Encoding.ASCII.GetBytes(httpVersion + " " + (int)statusCode + " " + reason + "\r\n\r\n");
- Send(buffer);
- }
- /// <summary>
- /// send a whole buffer
- /// </summary>
- /// <param name="buffer">buffer to send</param>
- /// <exception cref="ArgumentNullException"></exception>
- public bool Send(byte[] buffer)
- {
- if (buffer == null)
- throw new ArgumentNullException("buffer");
- return Send(buffer, 0, buffer.Length);
- }
- /// <summary>
- /// Send data using the stream
- /// </summary>
- /// <param name="buffer">Contains data to send</param>
- /// <param name="offset">Start position in buffer</param>
- /// <param name="size">number of bytes to send</param>
- /// <exception cref="ArgumentNullException"></exception>
- /// <exception cref="ArgumentOutOfRangeException"></exception>
- private object sendLock = new object();
- public bool Send(byte[] buffer, int offset, int size)
- {
- if (m_stream == null || m_sock == null || !m_sock.Connected)
- return false;
- if (offset + size > buffer.Length)
- throw new ArgumentOutOfRangeException("offset", offset, "offset + size is beyond end of buffer.");
- LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount();
- bool ok = true;
- ContextTimeoutManager.ContextEnterActiveSend();
- lock (sendLock) // can't have overlaps here
- {
- try
- {
- m_stream.Write(buffer, offset, size);
- }
- catch
- {
- ok = false;
- }
- }
- ContextTimeoutManager.ContextLeaveActiveSend();
- if (!ok && m_stream != null)
- Disconnect(SocketError.NoRecovery);
- return ok;
- }
- private void SendAsyncEnd(IAsyncResult res)
- {
- bool didleave = false;
- try
- {
- m_stream.EndWrite(res);
- ContextTimeoutManager.ContextLeaveActiveSend();
- didleave = true;
- m_currentResponse.CheckSendNextAsyncContinue();
- }
- catch (Exception e)
- {
- e.GetHashCode();
- if (m_stream != null)
- Disconnect(SocketError.NoRecovery);
- }
- if(!didleave)
- ContextTimeoutManager.ContextLeaveActiveSend();
- }
- public bool SendAsyncStart(byte[] buffer, int offset, int size)
- {
- if (m_stream == null || m_sock == null || !m_sock.Connected)
- return false;
- if (offset + size > buffer.Length)
- throw new ArgumentOutOfRangeException("offset", offset, "offset + size is beyond end of buffer.");
- bool ok = true;
- ContextTimeoutManager.ContextEnterActiveSend();
- try
- {
- m_stream.BeginWrite(buffer, offset, size, SendAsyncEnd, null);
- }
- catch (Exception e)
- {
- e.GetHashCode();
- ContextTimeoutManager.ContextLeaveActiveSend();
- ok = false;
- }
- if (!ok && m_stream != null)
- Disconnect(SocketError.NoRecovery);
- return ok;
- }
- /// <summary>
- /// The context have been disconnected.
- /// </summary>
- /// <remarks>
- /// Event can be used to clean up a context, or to reuse it.
- /// </remarks>
- public event EventHandler<DisconnectedEventArgs> Disconnected;
- /// <summary>
- /// A request have been received in the context.
- /// </summary>
- public event EventHandler<RequestEventArgs> RequestReceived;
- public HTTPNetworkContext GiveMeTheNetworkStreamIKnowWhatImDoing()
- {
- StreamPassedOff = true;
- m_parser.RequestCompleted -= OnRequestCompleted;
- m_parser.RequestLineReceived -= OnRequestLine;
- m_parser.HeaderReceived -= OnHeaderReceived;
- m_parser.BodyBytesReceived -= OnBodyBytesReceived;
- m_parser.Clear();
- m_currentRequest?.Clear();
- m_currentRequest = null;
- m_currentResponse?.Clear();
- m_currentResponse = null;
- if (m_requests != null)
- {
- while (m_requests.Count > 0)
- {
- HttpRequest req = m_requests.Dequeue();
- req.Clear();
- }
- }
- m_requests.Clear();
- m_requests = null;
- return new HTTPNetworkContext() { Socket = m_sock, Stream = m_stream as NetworkStream };
- }
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
- protected void Dispose(bool disposing)
- {
- if (contextID >= 0)
- {
- StreamPassedOff = false;
- Cleanup();
- }
- }
- }
- }
|