On Mon, 14 Dec 2015 18:24:21 +0100
Nenad Merdanovic <ni...@nimzo.info> wrote:

> Hello,
> 
> Sorry for top posting, but has there been any progress in getting the
> ability to rewrite response body with Lua in HAproxy (easy way)? I would
> assume AppletHTTP could be used for this, but I see that http-response
> doesn't support use-service.


Hi Nenad,

The use-service allow HAProxy/Lua to have a HTTP server behaviour. The
Lua executed in a directive use-service receives an http resquest and
sends an http response. In the "use-service" mode, we quit the proxy
mode.

Obvisouly, this mode have no sense with responses.

So, the use-service cannot allow to rewrite the response.

The best way is the non-keepalive requests and the TCP mode. If the
http request uses the connexion close mode, the content-length can be
deleted, and the transfer-enconding chunked too.

In this case, the body can be modified.

In other way, if you want to support the keep-alive, you can try to
write an Lua action which converts received HTTP data from any mode, to
a "transfer-encoding: chunked" mode.

When the data is transfered with chunks, you can manipulate streams
during the tranfer.

This is a little bit complex to write because you must reenconding an
http parser.

Thierry

> Regards,
> Nenad
> 
> On 10/26/2015 12:00 PM, Thierry FOURNIER wrote:
> > On Sun, 25 Oct 2015 02:09:15 +0100
> > PiBa-NL <piba.nl....@gmail.com> wrote:
> > 
> >> Hi Thierry, haproxy-list,
> >>
> >> Op 19-10-2015 om 11:24 schreef thierry.fourn...@arpalert.org:
> >>> On Mon, 19 Oct 2015 01:31:42 +0200
> >>> PiBa-NL <piba.nl....@gmail.com> wrote:
> >>>
> >>>> Hi Thierry,
> >>>>
> >>>> Op 18-10-2015 om 21:37 schreef thierry.fourn...@arpalert.org:
> >>>>> On Sun, 18 Oct 2015 00:07:13 +0200
> >>>>> PiBa-NL <piba.nl....@gmail.com> wrote:
> >>>>>
> >>>>>> Hi haproxy list,
> >>>>>>
> >>>>>> For testing purposes i am trying to 'modify' a response of a webserver
> >>>>>> but only having limited success. Is this supposed to work?
> >>>>>> As a more usefull goal than the current LAL to TST replacement i 
> >>>>>> imagine
> >>>>>> rewriting absolute links on a webpage could be possible which is
> >>>>>> sometimes problematic with 'dumb' webapplications..
> >>>>>>
> >>>>>> Or is it outside of the current scope of implemented functionality? If
> >>>>>> so, it on the 'lua todo list' ?
> >>>>>>
> >>>>>> I tried for example a configuration like below. And get several
> >>>>>> different results in the browser.
> >>>>>> -Sometimes i get 4 times TSTA
> >>>>>> -Sometimes i see after the 8th TSTA- Connection: keep-alive << this
> >>>>>> happens most of the time..
> >>>>>> -Sometimes i get 9 times TSTA + STOP << this would be the desired
> >>>>>> outcome (only seen very few times..)
> >>>>>>
> >>>>>> Probably due to the response-buffer being filled differently due to
> >>>>>> 'timing'..
> >>>>>>
> >>>>>> The "connection: keep-alive" text is probably from the actual server
> >>>>>> reply which is 'appended' behind the response generated by my lua
> >>>>>> script.?. However shouldn't the .done() prevent that from being send to
> >>>>>> the client?
> >>>>>>
> >>>>>> Ive tried putting a loop into the lua script to call res:get() multiple
> >>>>>> times but that didnt seem to work..
> >>>>>>
> >>>>>> Also to properly modify a page i would need to know all changes before
> >>>>>> sending the headers with changed content-length back to the client..
> >>>>>>
> >>>>>> Can someone confirm this is or isn't (reliably) possible? Or how this
> >>>>>> can be scripted in lua differently?
> >>>>> Hello,
> >>>>>
> >>>>> Your script replace 3 bytes by 3 bytes, this must run with HTTP, but if
> >>>>> your replacement change the length of the response, you can have some
> >>>>> difficulties with clients, or with keepalive.
> >>>> Yes i started with replacing with the same number of bytes to avoid some
> >>>> of the possible troubles caused by changing the length.. And as seen in
> >>>> the haproxy.cfg it is configured with 'mode http'.
> >>>>> The res:get(), returns the current content of the response buffer.
> >>>>> Maybe it not contains the full response. You must execute a loop with
> >>>>> regular "core.yield()" to get back the hand to HAProxy and wait for new
> >>>> Calling yield does allow to 'wait' for more data to come in.. No
> >>>> guarantee that it only takes 1 yield for data to 'grow'..
> >>>>
> >>>> [info] 278/055943 (77431) : luahttpresponse Content-Length XYZ: 14115
> >>>> [info] 278/055943 (77431) : luahttpresponse SIZE: 2477
> >>>> [info] 278/055943 (77431) : luahttpresponse LOOP
> >>>> [info] 278/055943 (77431) : luahttpresponse SIZE: 6221
> >>>> [info] 278/055943 (77431) : luahttpresponse LOOP
> >>>> [info] 278/055943 (77431) : luahttpresponse SIZE: 7469
> >>>> [info] 278/055943 (77431) : luahttpresponse LOOP
> >>>> [info] 278/055943 (77431) : luahttpresponse SIZE: 7469
> >>>> [info] 278/055943 (77431) : luahttpresponse LOOP
> >>>> [info] 278/055943 (77431) : luahttpresponse SIZE: 7469
> >>>> [info] 278/055943 (77431) : luahttpresponse LOOP
> >>>> [info] 278/055943 (77431) : luahttpresponse SIZE: 7469
> >>>> [info] 278/055943 (77431) : luahttpresponse LOOP
> >>>> [info] 278/055943 (77431) : luahttpresponse SIZE: 7469
> >>>> [info] 278/055943 (77431) : luahttpresponse LOOP
> >>>> [info] 278/055943 (77431) : luahttpresponse SIZE: 7469
> >>>> [info] 278/055943 (77431) : luahttpresponse LOOP
> >>>> [info] 278/055943 (77431) : luahttpresponse SIZE: 7469
> >>>> [info] 278/055943 (77431) : luahttpresponse LOOP
> >>>> [info] 278/055943 (77431) : luahttpresponse SIZE: 8717
> >>>> [info] 278/055943 (77431) : luahttpresponse LOOP
> >>>> [info] 278/055943 (77431) : luahttpresponse SIZE: 14337
> >>>> [info] 278/055943 (77431) : luahttpresponse DONE?: 14337
> >>>>
> >>>>> data. When all the data are read, res:get() returns an error.
> >>>> Not sure when/how this error would happen.? The result of res:get only
> >>>> seems to get bigger while the webserver is sending the response..
> >>>>> The res:send() is dangerous because it send data directly to the client
> >>>>> without the end of haproxy analysis. Maybe it is the cause o your
> >>>>> problem.
> >>>>>
> >>>>> Try to use res:set().
> >>>> Ok tried that, new try with function below.
> >>>>> The difficulty is that another "res:get()" returns the same data that
> >>>>> these you put.
> >>>>>
> >>>>> I don't known if you can modify an http response greater than one
> >>>>> buffer.
> >>>> Would be nice if that was somehow possible. But my current lua script
> >>>> cannot..
> >>>>> The function res:close() closes the connection even if HAProxy want to
> >>>>> keep the connection alive. I suggest that you don't use this function.
> >>>> It seems txn.res:close() does not exist? txn:done()
> >>>>> I reproduce the error message using curl. By default curl tries
> >>>>> to transfer data with keepalive, and it is not happy if the all the
> >>>>> announced data are not transfered.
> >>>>>
> >>>>>      Connection: keep-alive curl: (18) transfer closed with outstanding
> >>>>>      read data remaining
> >>>>>
> >>>>> It seems that i reproduce a bug. I'm looking for.
> >>>> Ok if you can create a patch, let me know. Happy to test if it solves
> >>>> some of the issues i see.
> >>>
> >>> Hi,
> >>>
> >>> I catch the error. While we execute http actions, the content of the
> >>> buffer must contains the http headers, otherwise, the request is
> >>> invalid.
> >>>
> >>> It is because the http action are applied on the content and not on
> >>> the  stream. So this is th behaviour:
> >>>
> >>>   -> The function res:get() remove the data from the haproxy input buffer
> >>>
> >>>   -> The function res:send() send the modified data directly to the
> >>>      client (throught the output buffer), this data is not yet avalaible
> >>>      in the input buffer.
> >>>
> >>>   -> Now HAProxy is locked in the processing, because it wait for a
> >>>      valid reponse header for continuing the Lua processing.
> >>>
> >>> This behaviour is not really a bug, it is the process who garantee that
> >>> a valid http response is present before continuing the processing.
> >>>
> >>> Maybe, a patch will send an error in the case of we enter in an action
> >>> with a valid request/response and we out from the processing without a
> >>> valid request/response.
> >>>
> >>> So, regarding the HAProxy global behaviour, the action are designed for
> >>> manipulating http header, and not the body.
> >>>
> >>> If you want to manipulates the body, you can use a tcp action.
> >> Ok the script below works, even for larger responses and sets the new 
> >> content-length.. That is.. for my single test page i tried it with.
> >>
> >> It could be troublesome that the lua script must take into account every 
> >> variation that might happen like chunked/compressed bodies, and parsing 
> >> the headers. Was kinda hoping that haproxy in http mode would be able to 
> >> take care of at least some of these kind of things. All advantages of 
> >> running a haproxy frontent in 'mode http' are now nolonger available so 
> >> thats a downside in my opinion..
> > 
> > 
> > You're absolutely right. The http action allow only the header
> > manipulation and not the body manipulation. So a new Lua binding is
> > currently discussed. This will be pluged between the receive buffer and
> > the send buffer. It will be dedicated for protocol transformation.
> > 
> > This enhancement requires heavy modification in HAProxy. Luckyly, these
> > modification will be the common with http/2.
> > 
> > 
> >> Also the script below buffers the whole response (which must have a 
> >> content-length header) before forwarding it to the client. This could 
> >> significantly increase the memoryusage of a single connection through 
> >> haproxy..
> > 
> > 
> > I agree again. It increase also the latency :(.
> > 
> > 
> >> But hey, it does open up a world of new 'rewriting' 
> >> possibilities for those backends that really really really need it..
> > 
> > 
> > I have no doubts about this.
> > 
> > 
> >> Anyway i wanted to share this for those people that might be in need of 
> >> doing something similar..
> >>
> >> And if anyone knows a way to simplify any part of the lua script please 
> >> let me know :)
> >> Yes searching the start and end of the content-length header twice isnt 
> >> needed, and the txn:Info() statements should be removed.. but im asking 
> >> more for other 'major' improvements.
> > 
> > 
> > Thank you for sharing.
> > 
> > Thierry
> > 
> > 
> >> Best regards,
> >> PiBa-NL
> >>
> >> listen proxytcpresponse
> >>         bind :10009
> >>         mode tcp
> >>         tcp-response content lua.luatcpresponse
> >>         server x 192.168.0.40:302
> >>
> >>      function luatcpresponse(txn)
> >>          txn:Info("#### TCP RESPONSE ENTERING LUA FUNCTION ###\n")
> >>          local responsecomplete = ""
> >>          local headersparsed = false
> >>          local headerlength = 0
> >>          local contentsize = 0
> >>          local response2 = txn.res:get()
> >>          local headers = ""
> >>          while (response2) and (string.len(response2) > 0)  do
> >>              txn:Info("lua tcp response LOOP")
> >>              txn:Info("lua tcp response SIZE: " .. string.len(response2))
> >>              response2 = string.gsub(response2,"LAL","TST")
> >>              responsecomplete = responsecomplete .. response2
> >>
> >>              if (not headersparsed) and 
> >> (string.find(responsecomplete,"\r\n\r\n") > 0) then
> >>                  txn:Info("headers complete")
> >>
> >>                  headersparsed = true
> >>                  _ , headerlength = 
> >> string.find(responsecomplete,"\r\n\r\n")
> >>                  headers = string.sub(responsecomplete,0,headerlength)
> >>                  local cl,cle = string.find(headers, "Content%-Length: ")
> >>                  local sizeend = string.find(responsecomplete,"\r\n", cle)
> >>                  contentsize = tonumber(string.sub(headers, cle+1, 
> >> sizeend-1))
> >>
> >>                  txn:Info("Headersize: " .. headerlength)
> >>                  txn:Info("Content-Length: " .. contentsize)
> >>                  responsecomplete = string.sub(responsecomplete, 
> >> headerlength+1)
> >>              end
> >>              txn:Info("RES size: " .. string.len(responsecomplete) .. " 
> >> H:" .. headerlength .. " S:" .. contentsize)
> >>              if (string.len(responsecomplete) == contentsize) then
> >>                  txn:Info("RESPONSE COMPLETE")
> >>                  local i = 0
> >>
> >>                  -- remove the scheme and host part from response href 
> >> links.. (nothing checked..)
> >>                  responsecomplete = 
> >> string.gsub(responsecomplete,"href=\"http://localhost:302/","href=\"/";)
> >>
> >>                  local ressize = string.len(responsecomplete)
> >>                  local cl,cle = string.find(headers, "Content%-Length: ")
> >>                  local sizeend = string.find(headers,"\r\n", cle)
> >>                  local firstheaders = string.sub(headers, 0, cl-1)
> >>                  local lastheaders = string.sub(headers, sizeend)
> >>                  headers = firstheaders .. "Content-Length: " .. ressize 
> >> .. lastheaders
> >>                  txn.res:send(headers .. responsecomplete)
> >>                  responsecomplete = ""
> >>                  headersparsed = false
> >>              end
> >>
> >>              response2 = txn.res:get()
> >>          end
> >>          txn:Info("TCP RESPONSE LUA FUNCTION, EXIT\n")
> >>      end
> >>
> >> core.register_action("luatcpresponse" , { "tcp-res" }, luatcpresponse);
> >>
> >>
> >>
> >>>
> >>> You can use also an http action, but the buffer must contains a valid
> >>> http request each time when HAProxy give back the hand. In this case,
> >>> you cannot modify requests greater than an haproxy buffer.
> >>>
> >>> res:get() -> get all the data and remove it from the input buffer
> >>> res:dup() -> just duplicates data
> >>> res:set() -> set data in the input buffer, HAProxy will continue the
> >>>               analysis.
> >>> res:send()-> send data in the output buffer, HAProy cannot analyse
> >>>               these data.
> >>>
> >>> Thierry
> >>>
> >>>>> Thierry
> >>>>>
> >>>> This function seems to work for responses up to +-15KB.
> >>>> Sometimes the number of loops it runs is different, and it seems kinda
> >>>> in-efficient to just run loops until the response is 'complete', another
> >>>> strange observation is that the res:set inside the loop is required,
> >>>> even though it doesn't set a modified response, eventually the complete
> >>>> response is modified in the browser result. Second request over a
> >>>> keep-alive connection also fails. Adding http-server-close also closes
> >>>> the client connection, but does avoid the problem with the second
> >>>> request.. In the lua script I dont account for headers size yet when
> >>>> checking if the response is completely read, but i dont think thats
> >>>> affected the test..
> >>>>
> >>>> Is there anything i can do to improve this function? (Besides removing
> >>>> the txn:Info() lines.)
> >>>>
> >>>>     function luahttpresponse(txn)
> >>>>       local resheaders = txn.http:res_get_headers()
> >>>>       local contentlength = tonumber(resheaders["content-length"][0])
> >>>>       local response2 = txn.res:get()
> >>>>       txn:Info("luahttpresponse Content-Length XYZ: " .. contentlength)
> >>>>       txn:Info("luahttpresponse SIZE: " .. string.len(response2))
> >>>>       while string.len(response2) < contentlength  do
> >>>>           txn:Info("luahttpresponse LOOP")
> >>>>           txn.res:set(response2)
> >>>>           core.yield()
> >>>>           response2 = txn.res:get()
> >>>>           txn:Info("luahttpresponse SIZE: " .. string.len(response2))
> >>>>       end
> >>>>       response2 = string.gsub(response2,"LAL","TST")
> >>>>       txn.res:set(response2)
> >>>>       txn:Info("luahttpresponse DONE?: " .. string.len(response2))
> >>>>     end
> >>>>
> >>>> Regards,
> >>>> PiBa-NL
> >>>>>> Thanks in advance,
> >>>>>> PiBa-NL
> >>>>>>
> >>>>>> ## haproxy.cfg
> >>>>>> listen proxyresponse
> >>>>>>        bind :10006
> >>>>>>        mode http
> >>>>>>        http-response lua.luahttpresponse
> >>>>>>        server x 192.168.0.40:302
> >>>>>>
> >>>>>> ## script.lua
> >>>>>>      function luahttpresponse(txn)
> >>>>>>        local response2 = txn.res:get()
> >>>>>>        response2 = string.gsub(response2,"LAL","TST")
> >>>>>>        txn.res:send(response2)
> >>>>>>        txn.done()
> >>>>>>      end
> >>>>>> core.register_action("luahttpresponse" , { "http-res" }, 
> >>>>>> luahttpresponse);
> >>>>>>
> >>>>>> ## webpage.aspx , with 2.7KB output.
> >>>>>> ------------------
> >>>>>> <%@ Page Language="C#" %>
> >>>>>> <html><body>START
> >>>>>> <% var x = new String('x', 250);
> >>>>>> x = "<input type=\"hidden\" value=\""+x+"\" />"; %>
> >>>>>> 1 -LALA- <% Response.Write(x); %>
> >>>>>> 2 -LALA- <% Response.Write(x); %>
> >>>>>> 3 -LALA- <% Response.Write(x); %>
> >>>>>> 4 -LALA- <% Response.Write(x); %>
> >>>>>> 5 -LALA- <% Response.Write(x); %>
> >>>>>> 6 -LALA- <% Response.Write(x); %>
> >>>>>> 7 -LALA- <% Response.Write(x); %>
> >>>>>> 8 -LALA- <% Response.Write(x); %>
> >>>>>> 9 -LALA- <% Response.Write(x); %>
> >>>>>> STOP
> >>>>>> --------------------
> >>>>>>
> >>>>>>
> >>>>
> >>
> >>
> > 
> 

Reply via email to