ProxyPass and RewriteHeader coop request

Topics: Developer Forum
Dec 30, 2009 at 3:19 AM

The ProxyPass feature would be a lot more useful if it honoured the header rules set by RewriteHeader statements. For example, I want to put IIS in front of Plone, to have IIS handle the NTLM/Kerberos authentication and then pass the username found on to Plone in an HTTP header. IIRF is the best rewriter for IIS I've found so far, and it can act as a reverse proxy, which is brilliant, but the proxy request has a fixed set of headers.

Coordinator
Dec 31, 2009 at 9:41 AM

I don't understand what the problem is.

Can you explain in more detail?  Show me the request headers you get, and the ones you expect to get.

 

Jan 3, 2010 at 10:45 PM

Well, in this case there is no incoming header, but we're adding a header based on the authentication result. What I want is to set a header that just contains the username.

So using Apache with mod_rewrite and mod_proxy:

 

RewriteEngine On
RequestHeader unset X_REMOTE_USER

RewriteCond %{LA-U:REMOTE_USER} (.+)

RewriteRule ^/(.*) http://localhost:8080/VirtualHostBase/http/%{SERVER_NAME}:80/plone/VirtualHostRoot/$1 [L,P,E=RU:%1]

RequestHeader set X_REMOTE_USER %{RU}e
This works great.
I'm trying to find a way to do this with IIS, but after trying a few different things in the iirf.ini I finally looked at the code, and I see that for a ProxyPass the set of request headers is fixed - there's no way to have a RewriteHeader directive change the headers sent to the proxied server. Unless I'm wrong?

 

Coordinator
Jan 4, 2010 at 8:08 PM
Edited Jan 7, 2010 at 5:33 AM

IIRF supports a RewriteHeader directive.  You should be able to set the header you prefer, using that directive.  You can then do the Proxy operation.  Place RewriteHeader before the ProxyPass directive in the ini file. 

I wouldn't say "the headers are fixed".  In a ProxyPass, the request headers arriving from the original request are relayed to the proxied resource. If you add a request header (with RewriteHeader) you can affect the set of headers relayed remotely by ProxyPass.

Jan 4, 2010 at 9:50 PM

Hmm, that's not what I'm seeing. When I use this config:

RewriteEngine On
RewriteLog C:\rewrite.log
RewriteLogLevel 7
RewriteHeader HTTP_X_REMOTE_USER ^$ guest
ProxyPass ^/(.*)$ http://localhost:8080/VirtualHostBase/http/%{SERVER_NAME}:80/plone/VirtualHostRoot/$1

Even though I do see "EvaluateRules: Setting Header: 'HTTP_X_REMOTE_USER:' = 'guest'" in the log file, it doesn't show up in any of the "ProxyRequest: header" items lower in the log file, and neither does it actually show up in the HTTP request. Obviously, I don't want to hardcode to guest in the real config, I want to do something like:
RewriteCond %{REMOTE_USER} domain\\(.+) [I]
RewriteHeader HTTP_X_REMOTE_USER ^$ *1
RewriteCond %{REMOTE_USER} domain\\(.+) [I]
RewriteHeader HTTP_X_REMOTE_USER ^(?!\*1)$ *1

But first I'd like the header to actually show up. Am I doing something wrong? At the top of the log file it says Ionic ISAPI Rewriting Filter (IIRF) 2.0.1.15 RELEASE.

And when I look in Proxy.c, in the Iirf_ProxyRequest() function I'm not seeing anywhere extra headers added, sHeaders is set to a fixed list, and then just used later without having existing headers appended.

RewriteEngine On
RewriteLog C:\rewrite.log
RewriteLogLevel 7
RewriteHeader HTTP_X_REMOTE_USER ^$ guest
ProxyPass ^/(.*)$ http://syddmichaelb01:8080/VirtualHostBase/http/%{SERVER_NAME}:80/cto/VirtualHostRoot/$1

 

Jan 4, 2010 at 10:14 PM

Just to be sure, I just tried (from the new log file) "Ionic ISAPI Rewriting Filter (IIRF) 2.1.0.7 RELEASE" and I see no change. If this is behaving differently to what you'd expect, have I made a config error? Thanks for your help.

Coordinator
Jan 4, 2010 at 10:51 PM

Ok, I'm clear on what you're seeing.

To be honest I never tested RewriteHeader in combination with ProxyPass.   Even so I thought it would just work as I described. 

I'll need to do some tests to figure out what's happening.

Jan 6, 2010 at 1:07 AM

Hmm, should pHeaderInfo be passed into Iirf_ProxyRequest() and the headers it contains added to sHeaders? Could it be that simple?

Coordinator
Jan 7, 2010 at 2:11 AM

It could be. I'll have to look at it. I haven't gotten to it yet.

Jan 7, 2010 at 4:05 AM

Well, I've put together a very custom hack with a hardcoded custom header name to look at - because I can't manage to get the regexs behaving and because of the recursive nature of the matching there seems to be absolutely no way to manipulate an existing header when you don't know the contents.

What I really want is to be able to do the following, because IIS will override any existing value of REMOTE_USER sent from the client with the real value from the authentication process:

RewriteCond %{REMOTE_USER} domain\\(.+) [I]
RewriteHeader X_REMOTE_USER .* *1

In this case, trying to spoof either of these headers will have no effect and be overridden with the real value. But of course, this will always fail to work because IIRF just goes deep on the same pattern and then dies.

Here is the patch of what I'm using, this is insecure because anyone could just add a header containing any username they like and be logged in as that person - acceptable for my situation, but not generally. Again, I'm only doing this because I cannot find anyway to solve this using IIRF yet.

Thanks again for writing this software, I can now actually get Plone working behind IIS and take advantage of the authentication it provides. Cheers.

 

Index: Filter/Proxy.c
===================================================================
--- Filter/Proxy.c	(revision 62941)
+++ Filter/Proxy.c	(working copy)
@@ -40,6 +40,12 @@
 void LogMessage( IirfVdirConfig * cfg, int MsgLevel, char * format, ... );                  // IirfLogging.c
 
 
+/*
+TODO: move this to correct location
+ */
+char * GetHeader_AutoFree(PHTTP_FILTER_CONTEXT pfc,
+                          HTTP_FILTER_AUTH_COMPLETE_INFO * pHeaderInfo,
+                          char * VariableName );
 
 
 char * AllocAndSprintf_AutoFree( HTTP_FILTER_CONTEXT * pfc, char * format, ... )
@@ -309,7 +315,8 @@
 DWORD Iirf_ProxyRequest(HTTP_FILTER_CONTEXT * pfc,
                         LPCTSTR fqUrl,
                         int *pContentChunks,
-                        int *pContentTotalBytes)
+                        int *pContentTotalBytes,
+                        HTTP_FILTER_AUTH_COMPLETE_INFO * pHeaderInfo)
 {
     IirfVdirConfig * cfg               = GetVdirConfigFromFilterContext(pfc);
     CHAR*            varMethod         = GetServerVariable_AutoFree(pfc, "REQUEST_METHOD");
@@ -365,6 +372,13 @@
         DWORD            dwFlags           = 0;
         URL_COMPONENTS   urlComponents;
 
+		// Get the specific header we want
+        char * remote_user = GetHeader_AutoFree(pfc, pHeaderInfo, "CUSTOM_HEADER:");
+        CHAR * hdrRemoteUser;
+        if (strlen(remote_user) > 0)
+        {
+        	hdrRemoteUser = AllocAndSprintf_AutoFree(pfc, "CUSTOM_HEADER: %s\r\n", remote_user);
+        }
 
         hdrVia = AllocAndSprintf_AutoFree(pfc, "Via: 1.1 %s (IIRF 2.0)\r\n", varLocalAddr);
         pfc->AddResponseHeaders( pfc, hdrVia, 0);
@@ -471,13 +485,14 @@
             hdrForwardedFor = AllocAndSprintf_AutoFree(pfc,"X-Forwarded-For: %s, %s\r\n", varForwardedFor, varRemoteAddr);
         }
 
-        sHeaders = AllocAndSprintf_AutoFree(pfc, "%s" "%s" "%s" "%s" "%s" "%s",
+        sHeaders = AllocAndSprintf_AutoFree(pfc, "%s" "%s" "%s" "%s" "%s" "%s" "%s",
                                             hdrContentType,
                                             hdrHost,
                                             hdrForwardedFor,
                                             hdrAcceptLanguage,
                                             hdrAcceptEncoding,
-                                            hdrCookies
+                                            hdrCookies,
+                                            hdrRemoteUser
             );
 
         wHeaders= AsciiToWideChar(sHeaders);
Index: Filter/Rewriter.c
===================================================================
--- Filter/Rewriter.c	(revision 62941)
+++ Filter/Rewriter.c	(working copy)
@@ -254,7 +254,8 @@
 extern DWORD Iirf_ProxyRequest(HTTP_FILTER_CONTEXT * pfc,
                         LPCTSTR fqUrl,
                         int *pContentChunks,
-                        int *pContentTotalBytes);                                    // Proxy.c
+                        int *pContentTotalBytes,
+                        HTTP_FILTER_AUTH_COMPLETE_INFO * pHeaderInfo);                                    // Proxy.c
 extern int ExceptionFilter(EXCEPTION_POINTERS *pExp, IirfVdirConfig * cfg);          // ExceptionHandler.cpp
 
 
@@ -862,7 +863,7 @@
             int chunks;
             int totalBytes;
             LogMessage(cfg, 2,"DoRewrites: Proxy to: '%s'", resultString);
-            if (Iirf_ProxyRequest(pfc, resultString, &chunks, &totalBytes))
+            if (Iirf_ProxyRequest(pfc, resultString, &chunks, &totalBytes, pHeaderInfo))
             {
                 // This is a POST, will require a REWRITE to the Extension, then Proxy from there.
                 //
Coordinator
Jan 7, 2010 at 5:33 AM
Edited Jan 7, 2010 at 5:39 AM

Thanks for the patch, I'll have a look.

I'm not clear on exactly what you're trying to do.  You wrote:


RewriteCond %{REMOTE_USER} domain\\(.+) [I]
RewriteHeader X_REMOTE_USER .* *1

In this case, trying to spoof either of these headers will have no effect and be overridden with the real value. But of course, this will always fail to work because IIRF just goes deep on the same pattern and then dies.

I don't know what that means, but... a couple things.  First, you can test the value of the custom header that you set, before applying the rule. That's what the pattern is for. For example:

# Set X_REMOTE_USER only if it is not set already.  
# Set it to the value actually stored in the REMOTE_USER header.
RewriteCond %{REMOTE_USER} domain\\(.+) [I]
RewriteHeader X_REMOTE_USER ^$ *1

The ^$ as the pattern for the RewriteHeader means the RewriteHeader will apply only if the X_REMOTE_USER is not already set  (only if it is empty or non-existent).   That may be what you are trying to do, not sure.   Your RewriteHeader uses  .*  as the pattern, which matches anything.   That means the RewriteHeader rule will always fire.  You may or may not want this.   An example illustrating this and some additional discussion is in the documentation page for the RewriteHeader directive.

Second, there's a [L] modifier you can use on the RewriteHeader directive to make it stop iterating.   So that might help you avoid the "gonig deep on the same pattern and dying" thing, if it is what I think you are talking about.

 

Jan 7, 2010 at 10:20 PM

"First, you can test the value of the custom header that you set, before applying the rule. That's what the pattern is for."

Yes, very useful and seems to work ok although personally I had no luck using the (?!www) style syntax outlined in the documentation, it just failed to match when it looks like it should have.

"Your RewriteHeader uses  .*  as the pattern, which matches anything.   That means the RewriteHeader rule will always fire.  You may or may not want this."

Well, in this case I do want this, but I see no way to make it work. If you put [L] with that rule, then the ProxyPass will not be called. If you don't, this rule will just be recursively called until the entire ruleset dies after hitting the depth limit. So I can't see how you could make a .* pattern work with IIRF at all. And your documentation seems to state this also: "if the pattern was .* rather than ^$, the header would be set always. This would likely produce a cycle or infinite loop in the rules. Which you probably don't want." Which seems to read as, "you can't use .* for the pattern."

I guess the main point in what I'm trying to achieve is to only have this header set by IIS after genuine authentication, so if someone passes a custom REMOTE_USER header to the server, it won't get through to the proxy request. Right now my hack can pass one hardcoded header through to the proxy request, but it picks up the pre-rewrite values, so you can still fake the header if you know or guess the header name. The browser sets a REMOTE_USER header with a fake value, then authenticates to IIS, which overwrites the REMOTE_USER value, but the place in the code where I read the value in the proxy section finds the original value. So for now I'll use a custom header because we're only using this for tracking purposes, so there's no security risk.

Thanks for responding!

Coordinator
Jan 7, 2010 at 11:01 PM
mbaltaks wrote:

"Your RewriteHeader uses  .*  as the pattern, which matches anything.   That means the RewriteHeader rule will always fire.  You may or may not want this."

Well, in this case I do want this, but I see no way to make it work. If you put [L] with that rule, then the ProxyPass will not be called. If you don't, this rule will just be recursively called until the entire ruleset dies after hitting the depth limit. So I can't see how you could make a .* pattern work with IIRF at all. And your documentation seems to state this also: "if the pattern was .* rather than ^$, the header would be set always. This would likely produce a cycle or infinite loop in the rules. Which you probably don't want." Which seems to read as, "you can't use .* for the pattern."

I guess the main point in what I'm trying to achieve is to only have this header set by IIS after genuine authentication, so if someone passes a custom REMOTE_USER header to the server, it won't get through to the proxy request. Right now my hack can pass one hardcoded header through to the proxy request, but it picks up the pre-rewrite values, so you can still fake the header if you know or guess the header name. The browser sets a REMOTE_USER header with a fake value, then authenticates to IIS, which overwrites the REMOTE_USER value, but the place in the code where I read the value in the proxy section finds the original value. So for now I'll use a custom header because we're only using this for tracking purposes, so there's no security risk.

Thanks for responding!

Not true that you can't use .* for the pattern on a RewriteHeader. It's true that if you use .* you will always rewrite the header.  But if you apply a RewriteCond, then the rewrite will be conditional.

I don't know your scenario, but it seems to me there must be a way you can distinguish whether the RewriteHeader has succeeded or not.  Before taht rule fires, the X_REMOTE_USER is something.  After it fires, it is something else.  If you cannot distinguish between the before and after, then there doesn't seem to be much point in having the rule.  If you know what I mean.  It changes the specified header, and I would expect you would be able to tell after it fires, that it has actually changed the header.   If you can codify that difference into a RewriteCond, then you have a way to make the RewriteHeader fire only if it hasn't fired yet. 

Another strategy might be to put the ProxyPass before the RewriteHeader in the ini file, and make ProxyPass conditional (RewriteCond) on the value of X_REMOTE_USER that has been set by your rewriteheader directive. 

The key thing is being able to test in a RewriteCond, whether the header has been set yet.

Jan 7, 2010 at 11:27 PM

Ok, I just tried changing the code to only pass the REMOTE_USER value to the proxy request, first time it crashed with only the ProxyPass rule, so I added:

 

RewriteCond %{REMOTE_USER} citrite\\(.+) [I]
RewriteHeader REMOTE_USER ^$ *1

RewriteCond %{REMOTE_USER} domain\\(.+) [I]
RewriteHeader REMOTE_USER ^$ *1
Before the ProxyPass rule, just a noop to populate the data, but this happily passes whatever the browser sends on to the proxy request, even when IIS has already overwritten this with the value from authentication.
So, back to your suggestion, does this look right then? Do both of the conditions have to be satisfied before the RewriteHeader rule will apply? It doesn't seem to work in my test environment.

 

RewriteCond %{REMOTE_USER} domain\\(.+) [I]
RewriteCond %{X_REMOTE_USER} ^(?!\*1)$
RewriteHeader X_REMOTE_USER .* *1
And should that second line match when the value of the header is not the previous matched value?

 

 

Coordinator
Jan 8, 2010 at 8:59 AM

I'm not really following.  When you say "but this happily passes whatever the browser sends on to the proxy request, even when IIS has already overwritten this with the value from authentication."  I don't know what that means.

When you use RewriteCond, if there are multiple RewriteCond's they are AND'd together.

I also don't know what this means:  "And should that second line match when the value of the header is not the previous matched value?" 

I think you get it though.  You get how it's supposed to work.

 

Feb 7, 2010 at 10:31 PM

Hi,

I'm facing a similar issue.

First some info on my config: IIS 6.0, IIRF 2.1.0.11, Win 2003 Server.

I also want IIS to handle authentication and then pass the request down to my app server, via ProxyPass.

In fact I just want to do the same as the following directives which comes from Helicon ISAPI_Rewrite3:

 

RewriteProxy (.*) http://localhost:8001/$1 [H,A]

 

The magic is the A flag, which adds these headers to the proxied server (see http://www.helicontech.com/isapi_rewrite/doc/RewriteProxy.htm )

X-ISRW-Proxy-AUTH-TYPE,
X-ISRW-Proxy-AUTH-USER,
X-ISRW-Proxy-LOGON-USER,
X-ISRW-Proxy-REMOTE-USER

which comes from SERVER variables:

AUTH_TYPE,
AUTH_USER,
LOGON_USER,
REMOTE_USER

 

So, to achieve this, I tried to add a new header through the RewriteHeader directive, like so:

 

RewriteHeader HTTP_X_REMOTE_USER ^$ %{REMOTE_USER}
ProxyPass ^/(?!home)(.*)$ http://localhost:8001/$2

 

When IIS is authenticating the request I see in the log that the header is set accordingely, BUT this new header is NOT sent to the proxied server (the one at localhost 8001).
I'll call this the first bug. Could someone confirm this? If yes, I may file a bug in the tracker.

Another issue: when IIS doesn't authenticate the request (anonymous access), then there is no REMOTE_USER header and the previous directive will loop until max depth reaches. This is because the REMOTE_USER is blank and the pattern to match the HTTP_X_REMOTE_USER is also blank => ^$. So what came to my mind is to protect this RewriteHeader directive with a RewriteCond, like this:

 

RewriteCond %{HTTP_X_REMOTE_USER} ^$
RewriteHeader HTTP_X_REMOTE_USER ^$ %{REMOTE_USER}
ProxyPass ^/(?!home)(.*)$ http://localhost:8001/$2

 

Looking at the log shows a weird behaviour, here is the interesting part:

Sun Feb 07 23:18:20 -  2324 - EvaluateRules: depth=0
Sun Feb 07 23:18:20 -  2324 - GetHeader_AutoFree: getting 'HTTP_X_REMOTE_USER:'
Sun Feb 07 23:18:20 -  2324 - GetHeader_AutoFree: 128 bytes   ptr:0x000C9AA8
Sun Feb 07 23:18:20 -  2324 - GetHeader_AutoFree: 'HTTP_X_REMOTE_USER:' = ''
Sun Feb 07 23:18:20 -  2324 - EvaluateRules: Rule 1 : 1 matches
Sun Feb 07 23:18:20 -  2324 - GetServerVariable: getting 'HTTP_X_REMOTE_USER'
Sun Feb 07 23:18:20 -  2324 - GetServerVariable: cannot find that variable
Sun Feb 07 23:18:20 -  2324 - GetServerVariable: 128 bytes
Sun Feb 07 23:18:20 -  2324 - GetServerVariable: result 'HTTP_X_REMOTE_USER'
Sun Feb 07 23:18:20 -  2324 - ReplaceServerVariables: VariableName='HTTP_X_REMOTE_USER' Value='HTTP_X_REMOTE_USER'
Sun Feb 07 23:18:20 -  2324 - ReplaceServerVariables: in='%{HTTP_X_REMOTE_USER}' out='HTTP_X_REMOTE_USER'
Sun Feb 07 23:18:20 -  2324 - EvalCondition: ts1 'HTTP_X_REMOTE_USER'
Sun Feb 07 23:18:20 -  2324 - GenerateReplacementString: result 'HTTP_X_REMOTE_USER'
Sun Feb 07 23:18:20 -  2324 - EvalCondition: checking 'HTTP_X_REMOTE_USER' against pattern '^$'
Sun Feb 07 23:18:20 -  2324 - EvalCondition: match result: -1 (No match)
Sun Feb 07 23:18:20 -  2324 - EvalCondition: Cond %{HTTP_X_REMOTE_USER} ^$ => FALSE
Sun Feb 07 23:18:20 -  2324 - EvalConditionList: rule 1, FALSE, Rule does not apply

because the HTTP_X_REMOTE_USER header doesn't exist, it is created with a VALUE of HTTP_X_REMOTE_USER which is the header name. Of course at this point we can't rely on the fact that this header is blank, which it is no more. (see the next line of the log, EvalCondition)

I'll call this the second bug. IMHO a non existant header should be considered as having a blank value, not the name of itself.

 

Thanx for listening, best regards,

Pascal

Coordinator
Feb 7, 2010 at 10:54 PM

Pascal, can you please open a new thread?

This thread is a month old;  you should open a new one, rather than resuscitating an old one.