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.
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 >>>>>> -------------------- >>>>>> >>>>>> >>>> >> >> >