0

I am trying to write an HttpModule that changes the request body before it arrives in an RestAPI. The goal is to add some key-values in a request body with content type "x-www-form-urlencoded". My first attempt is to only change the characters in the payload to upper case. To test this, the httpmodule is added to a simple restapi which returns the request content in the response. The logs from the httpmodule shows that the request body should be in uppercase but the RESTAPI returns the original response (which is in lower case).

The httpmodule will in the end be used in a different API which I cannot change. Here are some links that I have tried,

Here is the HttpModule code,

using IISWrapper.RequestFilters;
using NLog;
using System;
using System.Web;
using System.Web.SessionState;

namespace IISWrapper {

    public class HttpModule : IRequiresSessionState, IHttpModule {

        private static readonly Logger log = LogManager.GetCurrentClassLogger();

        public HttpModule() { }

        public void Init(HttpApplication app) {
            app.BeginRequest += OnPostAuthenticateRequest;
        }

        public void Dispose() { }

        private void OnPostAuthenticateRequest(object sender, EventArgs args) {
            HttpApplication app = (HttpApplication)sender;
            app.Context.Request.Filter = new RequestFilter(app.Context.Request.Filter);
        }
    }
}
using NLog;
using System;
using System.IO;
using System.Runtime.Remoting;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace IISWrapper.RequestFilters {
    class RequestFilter : Stream {

        private MemoryStream ms;
        private Stream _stream;
        string content;
        byte[] bytes;
        //private Encoding _encoding;

        private static readonly Logger log = LogManager.GetCurrentClassLogger();

        public RequestFilter(Stream stream) {
            log.Debug("RequestFilter: Initialise request filter");
            _stream = stream;
            //_encoding = encoding;
        }

        public override void Flush() {
            log.Debug("Flush:");
            ms.Flush();
        }

        public override long Seek(long offset, SeekOrigin origin) { 
            log.Debug("Seek:");
            return ms.Seek(offset, origin); 
        }

        public override void SetLength(long value) { 
            log.Debug("SetLength:");
            ms.SetLength(value); 
        }

        public override int Read(byte[] buffer, int offset, int count) {
            log.Debug($"Read: Content={content}");
            
            if (ms == null) {
                log.Debug("Read: Its null:");
                var sr = new StreamReader(_stream, Encoding.UTF8);
                content = sr.ReadToEnd();

                content = content.ToUpper();

                bytes = Encoding.UTF8.GetBytes(content);
                ms = new MemoryStream();
                ms.Write(bytes, 0, bytes.Length);
                ms.Seek(0, SeekOrigin.Begin);
            } 
            
            log.Debug($"Read: ms.read, content={content}");
            log.Debug($"Read: BytesLength={bytes.Length}");
            log.Debug($"Read: BufferLength={buffer.Length}");
            log.Debug($"Read: BufferToString={Encoding.UTF8.GetString(buffer)}");
            log.Debug($"Read: BytesToString={Encoding.UTF8.GetString(bytes)}");
            return ms.Read(buffer, offset, count);
        }

        public override void Write(byte[] buffer, int offset, int count) {
            log.Debug("Write:");
            throw new NotSupportedException();
        }

        public override bool CanRead {
            get {
                log.Debug("CanRead:");
                return true;
            }
        }

        public override bool CanSeek {
            get {
                log.Debug("CanSeek:");
                return false;
            }
        }

        public override bool CanWrite {
            get {
                log.Debug("CanWrite:");
                return false;
            }
        }

        public override long Length {
            get {
                log.Debug("Length:");
                return ms.Length;
            }
        }

        public override long Position {
            get {
                log.Debug("Position:");

                return ms.Position;
            }
            set { 
                throw new NotSupportedException(); 
            }
        }
    }
}

The HttpModule is applied to a simple REST API that only echos the request,

using log4net;
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web.Http;

namespace TestRestAPI.Controllers {
    public class MyController : ApiController
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(MyController));
        
        [HttpPost, Route("Echo")]
        public HttpResponseMessage Echo() {
            try {
                log.Debug("Echo: start");
                byte[] byteArrayIn = Request.Content.ReadAsByteArrayAsync().Result;
                string asString = Encoding.UTF8.GetString(byteArrayIn);
                log.Debug($"Echo: Payload={asString}");

                return Request.CreateResponse(HttpStatusCode.OK, asString);

            } catch (Exception ex) {
                log.Error($"Echo: Exception {ex}");
                return Request.CreateResponse(HttpStatusCode.OK, ex.ToString());
            }
        }
    }
}

The request body is "abcdefghijklmnopqrstuvwxyz1234567890" but the response from the RestApi is not in uppercase and the logs from the HttpModule is,

2021-04-28 16:34:41.3934 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: RequestFilter: Initialise request filter
2021-04-28 16:34:41.6073 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: Read: Content=
2021-04-28 16:34:41.6073 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: Read: Its null:
2021-04-28 16:34:41.6073 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: Read: ms.read, content=ABCDEFGHIJKLMNOPQRSUVWXYZ1234567890
2021-04-28 16:34:41.6073 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: Read: BytesLength=35
2021-04-28 16:34:41.6073 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: Read: BufferLength=8192
2021-04-28 16:34:41.6073 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: Read: BufferToString=
2021-04-28 16:34:41.6073 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: Read: BytesToString=ABCDEFGHIJKLMNOPQRSUVWXYZ1234567890
2021-04-28 16:34:41.6073 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: Read: Content=ABCDEFGHIJKLMNOPQRSUVWXYZ1234567890
2021-04-28 16:34:41.6073 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: Read: ms.read, content=ABCDEFGHIJKLMNOPQRSUVWXYZ1234567890
2021-04-28 16:34:41.6073 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: Read: BytesLength=35
2021-04-28 16:34:41.6073 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: Read: BufferLength=8192
2021-04-28 16:34:41.6143 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: Read: BufferToString=ABCDEFGHIJKLMNOPQRSUVWXYZ1234567890
2021-04-28 16:34:41.6143 [7] DEBUG IISWrapper.RequestFilters.RequestFilter: Read: BytesToString=ABCDEFGHIJKLMNOPQRSUVWXYZ1234567890

Here are the logs from the rest api,

2021-04-28 16:34:41,603 [7 ] DEBUG TestRestAPI.Controllers.ApiController    Echo: start
2021-04-28 16:34:41,615 [7 ] DEBUG TestRestAPI.Controllers.ApiController    Echo: Payload=abcdefghijklmnopqrsuvwxyz1234567890

Any suggestion on what could be wrong is appreciated.


EDIT 2021-05-25

I found that for this to work, the endpoint needs to end with .aspx and the content type of the request must be "x-www-form-urlencoded".

Questions:

  • Can someone explain why it has to be a certain content type and extension?
  • Is it possible to make this work for endpoints not ending in .aspx?

EDIT 2021-05-27

After adding the request filter i added the following line,

    var _ = context.Request.Form;

to trigger the evaluation of the input stream. Then if the content type is "x-www-form-urlencoded" the request filter will be applied. If you any other kind of data like json, xml, raw text then the request content wont change for some reason. Maybe request filters only works on form data?

Jesper Lundin
  • 168
  • 1
  • 8
  • HTTP use HTML format. The easiest way is to use parse the html into an html document. Then edit the document and finally write the html document to a string and send the modified string. See following github library for parsing html : https://github.com/zzzprojects/html-agility-pack?force_isolation=true – jdweng Apr 28 '21 at 15:08
  • The parsing and modification of the request body is not a problem. The problem is to put it back so that the RestAPI gets the modified request body. At the moment, the RestAPI still reads the unmodified content, hence the httpModule has no effect. – Jesper Lundin Apr 28 '21 at 19:50
  • The modified body needs to be added to following : Request.CreateResponse(HttpStatusCode.OK, asString); – jdweng Apr 29 '21 at 01:25
  • Maybe my post was unclear, a httpModule wraps an IIS site and intercepts the request and responses to the site. In my case I want to intercept the request before it enters the site and change the request body. – Jesper Lundin Apr 29 '21 at 07:25
  • How can you modify something you haven't received (before entering the site)? A controller has a deserialize method and the best you can do is capture at the beginning of the deserialize. That looks like what you have done. – jdweng Apr 29 '21 at 09:28
  • The HttpModule is added as a module to the IIS site and will intercept the incoming request. I mean that it will modify the payload before the rest api reads it. But the logs from the restapi above shows that the rest api gets the unmodified request body (which is in lower case) an not the modified one (which is in uppercase). – Jesper Lundin Apr 29 '21 at 13:58
  • The controller uses a deserialize method to parse the receive data and outputs a The rest of the application looks at the . Your echo is just sending a response back the the client. You have to change the – jdweng Apr 29 '21 at 14:12
  • I want to be able to change the request in the httpModule. I have no control over the RestAPI. The RestAPI above is just for testing to see if the HttpModule is able to change the request body before the RestAPI reads it. – Jesper Lundin Apr 29 '21 at 19:25
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/231759/discussion-between-jesper-lundin-and-jdweng). – Jesper Lundin Apr 29 '21 at 19:27
  • Then you have a two port server. You receive a message from client. App is a client to the API over a new request on second connection. Then waits for a response from API Server. Finally sends response back to the original clinet on original connection. – jdweng Apr 29 '21 at 19:31

0 Answers0