Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example compatible with sessions #8

Open
suehshtri opened this issue Jul 18, 2022 · 3 comments
Open

Add example compatible with sessions #8

suehshtri opened this issue Jul 18, 2022 · 3 comments

Comments

@suehshtri
Copy link

Consider adding an example that is compatible with server code that uses a session and session cookie.

@marman-hp
Copy link

marman-hp commented Jul 28, 2023

Hello,
First I want to thank @amaitland for this library. This is very useful.

Well I think is not about the compatible session example, but the session is always null, because OnStarting in SessionMiddleware never triggered. the OnStarting is responsible for sending session to cookie.

My Startup.cs
---ConfigureServices -- >
services.AddDistributedMemoryCache();
services.AddSession(opt =>
{
opt.Cookie.HttpOnly = true;
opt.Cookie.IsEssential = true;
opt.IdleTimeout = TimeSpan.FromHours(1);
});

---Configure -- >
app.UseSession();

image

HttpContext.Session.SetString("hi", "Hello world"); --> doesn't work , because "Session" never sent to cookie by SessionMiddleware.

So I find out. SessionMiddleware uses OnStarting to write session cookies, but it's never triggered by the owin server
I made a simple test middleware using OnStarting and the result OnStarting never triggered ,

I think All middleware that implements OnStarting/OnComplete is never triggered by owin server. (my oppinion about the flow, OnStarting/OnComplete Triggerd by StartAsync, StartAsync Triggered by RequestDelegate, since it use Owin, StartAsync must be called manually on the server, if I'am not wrong).

so I checked implementation of IHttpResponseFeature, I found the IHttpResponseFeature.OnStarting is empty, it should be implemented.
I made small change in OwinFeatureImpl.cs and OwinServer.cs.

OwinFeatureImpl.cs ` public class OwinFeatureImpl : IHttpRequestFeature, IHttpResponseFeature, IHttpResponseBodyFeature, IHttpRequestIdentifierFeature, IOwinEnvironmentFeature { /// /// Gets or sets OWIN environment values. /// public IDictionary Environment { get; set; } private PipeWriter _responseBodyWrapper; private IHeaderDictionary _requestHeaders; private IHeaderDictionary _responseHeaders;
    private Func<Task> _responseStartingAsync = () => Task.CompletedTask;
    private Func<Task> _responseCompletedAsync = () => Task.CompletedTask;
    
    private bool _started;

    /// <summary>
    /// Initializes a new instance of <see cref="Microsoft.AspNetCore.Owin.OwinFeatureCollection"/>.
    /// </summary>
    /// <param name="environment">The environment values.</param>
    public OwinFeatureImpl(IDictionary<string, object> environment)
    {
        Environment = environment;
    }

    private T GetEnvironmentPropertyOrDefault<T>(string key)
    {
        object value;
        if (Environment.TryGetValue(key, out value) && value is T)
        {
            return (T)value;
        }
        return default(T);
    }

    private void SetEnvironmentProperty(string key, object value)
    {
        Environment[key] = value;
    }

    string IHttpRequestFeature.Protocol
    {
        get { return GetEnvironmentPropertyOrDefault<string>(OwinConstants.RequestProtocol); }
        set { SetEnvironmentProperty(OwinConstants.RequestProtocol, value); }
    }

    string IHttpRequestFeature.Scheme
    {
        get { return GetEnvironmentPropertyOrDefault<string>(OwinConstants.RequestScheme); }
        set { SetEnvironmentProperty(OwinConstants.RequestScheme, value); }
    }

    string IHttpRequestFeature.Method
    {
        get { return GetEnvironmentPropertyOrDefault<string>(OwinConstants.RequestMethod); }
        set { SetEnvironmentProperty(OwinConstants.RequestMethod, value); }
    }

    string IHttpRequestFeature.PathBase
    {
        get { return GetEnvironmentPropertyOrDefault<string>(OwinConstants.RequestPathBase); }
        set { SetEnvironmentProperty(OwinConstants.RequestPathBase, value); }
    }

    string IHttpRequestFeature.Path
    {
        get { return GetEnvironmentPropertyOrDefault<string>(OwinConstants.RequestPath); }
        set { SetEnvironmentProperty(OwinConstants.RequestPath, value); }
    }

    string IHttpRequestFeature.QueryString
    {
        get { return AddQuestionMark(GetEnvironmentPropertyOrDefault<string>(OwinConstants.RequestQueryString)); }
        set { SetEnvironmentProperty(OwinConstants.RequestQueryString, RemoveQuestionMark(value)); }
    }

    string IHttpRequestFeature.RawTarget
    {
        get { return string.Empty; }
        set { throw new NotSupportedException(); }
    }

    IHeaderDictionary IHttpRequestFeature.Headers
    {
        get
        {
            if(_requestHeaders == null)
            {
                var dict = GetEnvironmentPropertyOrDefault<IDictionary<string, string[]>>(OwinConstants.RequestHeaders);
                _requestHeaders = dict is IHeaderDictionary ? (IHeaderDictionary)dict : new DictionaryStringValuesWrapper(dict);
            }
            return _requestHeaders;
        }
        set { SetEnvironmentProperty(OwinConstants.RequestHeaders, MakeDictionaryStringArray(value)); }
    }

    string IHttpRequestIdentifierFeature.TraceIdentifier
    {
        get { return GetEnvironmentPropertyOrDefault<string>(OwinConstants.RequestId); }
        set { SetEnvironmentProperty(OwinConstants.RequestId, value); }
    }

    Stream IHttpRequestFeature.Body
    {
        get { return GetEnvironmentPropertyOrDefault<Stream>(OwinConstants.RequestBody); }
        set { SetEnvironmentProperty(OwinConstants.RequestBody, value); }
    }

    int IHttpResponseFeature.StatusCode
    {
        get { return GetEnvironmentPropertyOrDefault<int>(OwinConstants.ResponseStatusCode); }
        set { SetEnvironmentProperty(OwinConstants.ResponseStatusCode, value); }
    }

    string IHttpResponseFeature.ReasonPhrase
    {
        get { return GetEnvironmentPropertyOrDefault<string>(OwinConstants.ResponseReasonPhrase); }
        set { SetEnvironmentProperty(OwinConstants.ResponseReasonPhrase, value); }
    }

    IHeaderDictionary IHttpResponseFeature.Headers
    {
        get
        {
            if (_responseHeaders == null)
            {
                var dict = GetEnvironmentPropertyOrDefault<IDictionary<string, string[]>>(OwinConstants.ResponseHeaders);
                _responseHeaders = dict is IHeaderDictionary ? (IHeaderDictionary)dict : new DictionaryStringValuesWrapper(dict);
            }
            return _responseHeaders;
        }
        set { SetEnvironmentProperty(OwinConstants.ResponseHeaders, MakeDictionaryStringArray(value)); }
    }

    Stream IHttpResponseFeature.Body
    {
        get { return GetEnvironmentPropertyOrDefault<Stream>(OwinConstants.ResponseBody); }
        set { SetEnvironmentProperty(OwinConstants.ResponseBody, value); }
    }

    Stream IHttpResponseBodyFeature.Stream
    {
        get { return GetEnvironmentPropertyOrDefault<Stream>(OwinConstants.ResponseBody); }
    }

    PipeWriter IHttpResponseBodyFeature.Writer
    {
        get
        {
            if (_responseBodyWrapper == null)
            {
                _responseBodyWrapper = PipeWriter.Create(GetEnvironmentPropertyOrDefault<Stream>(OwinConstants.ResponseBody), new StreamPipeWriterOptions(leaveOpen: true));
            }

            return _responseBodyWrapper;
        }
    }

    bool IHttpResponseFeature.HasStarted => false;

    void IHttpResponseFeature.OnStarting(Func<object, Task> callback, object state)
    {
        //not protecting with HasStarted because is always false

        var prior = _responseStartingAsync;
        _responseStartingAsync = async () =>
        {
            await callback(state);
            await prior();
        };

    }

    void IHttpResponseFeature.OnCompleted(Func<object, Task> callback, object state)
    {
        //not protecting with HasStarted because is always false

        var prior = _responseCompletedAsync;
        _responseCompletedAsync = async () =>
        {
            try
            {
                await callback(state);
            }
            finally
            {
                await prior();
            }   
        };
    }

    Task IHttpResponseBodyFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
    {
        var writer = ((IHttpResponseBodyFeature)this).Writer;

        return SendFileFallback.SendFileAsync(writer.AsStream(), path, offset, length, cancellation);
    }

    void IHttpResponseBodyFeature.DisableBuffering()
    {
    }

    async Task FireOnSendingHeadersAsync()
    {
        
            try
            {
                await _responseStartingAsync();
            }
            finally
            {
                //HasStarted = true;
            }
    }

    Task FireOnResponseCompletedAsync()
    {
        return _responseCompletedAsync();
    }

    async Task IHttpResponseBodyFeature.StartAsync(CancellationToken cancellationToken)
    {
        try
        {
        _started = true;
        await FireOnSendingHeadersAsync();

        }
        catch (Exception)
        {
            throw;
        }

        if (_responseBodyWrapper != null)
        {
            await _responseBodyWrapper.FlushAsync(cancellationToken);
        }

        //// The pipe may or may not have flushed the stream. Make sure the stream gets flushed to trigger response start.
        await GetEnvironmentPropertyOrDefault<Stream>(OwinConstants.ResponseBody).FlushAsync(cancellationToken);

        
    }

    Task IHttpResponseBodyFeature.CompleteAsync()
    {
       


        if (!_started)
        {
             ((IHttpResponseBodyFeature)this).StartAsync().Wait();
        }

        FireOnResponseCompletedAsync();

        if (_responseBodyWrapper != null)
        {
              _responseBodyWrapper.FlushAsync().ConfigureAwait(false);
        }


        return ((IHttpResponseBodyFeature)this).Writer.CompleteAsync().AsTask();
    }

    /// <inheritdoc/>
    public void Dispose()
    {
    }

    private static string RemoveQuestionMark(string queryString)
    {
        if (!string.IsNullOrEmpty(queryString))
        {
            if (queryString[0] == '?')
            {
                return queryString.Substring(1);
            }
        }
        return queryString;
    }

    private static string AddQuestionMark(string queryString)
    {
        if (!string.IsNullOrEmpty(queryString))
        {
            return '?' + queryString;
        }
        return queryString;
    }

    private static IDictionary<string, string[]> MakeDictionaryStringArray(IHeaderDictionary dictionary)
    {
        var wrapper = dictionary as DictionaryStringValuesWrapper;
        if (wrapper != null)
        {
            return wrapper.Inner;
        }
        return new DictionaryStringArrayWrapper(dictionary);
    }
}

`

OwinServer.cs ` public class OwinServer : IServer { private IFeatureCollection _features = new FeatureCollection(); private Action useOwin;
    IFeatureCollection IServer.Features
    {
        get { return _features; }
    }

    void IDisposable.Dispose()
    {
        
    }

    /// <summary>
    /// The <paramref name="action"/> will be called when the OWIN <see cref="AppFunc"/> 
    /// is ready for use. 
    /// </summary>
    /// <param name="action">called when OWIN <see cref="AppFunc"/> is ready for use.</param>
    public void UseOwin(Action<AppFunc> action)
    {
        useOwin = action;
    }

    Task IServer.StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
    {
        AppFunc appFunc = async env =>
        {
            var features = new OwinFeatureCollection(env);

            var context = application.CreateContext(features);

            try
            {
                await application.ProcessRequestAsync(context);

                //Normaly startasync triggered by RequestDelegate by invoking HttpContext 
                //when default httpcontext create by owinserver, startasync has never been triggered ,
                //so I need to triggerd manualy 
                await features.Get<IHttpResponseBodyFeature>().StartAsync();



            }
            catch (Exception ex)
            {
                application.DisposeContext(context, ex);
                throw;
            }

            application.DisposeContext(context, null);
        };

        useOwin?.Invoke(appFunc);

        return Task.CompletedTask;
    }

    Task IServer.StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

`

and now my session are working, tested with chromly and webview2 sample

image

I hope this help if someone have the same issue with "onstarting middleware" especialy SessionMiddleware(UseSession).

Thanks.

@amaitland
Copy link
Collaborator

@marman-hp Thanks for posting your findings 👍

Can you please submit your changes as a PR so they can be included in the library itself. Thanks.

@marman-hp
Copy link

marman-hp commented Jul 29, 2023

I have create fork from your repository , and make minor changes, added missing ability with IHttpResponseFeature, so that anyone can test or investigate the code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants