diff --git a/priv/www/hijack.css b/priv/www/hijack.css index 3eeb14e7d17995ce5cce7861495941c07c46ca35..170e10e7cab5c2b9b80933ed6cb17c36690564de 100644 --- a/priv/www/hijack.css +++ b/priv/www/hijack.css @@ -1,9 +1,9 @@ body { - padding: 1rem; + padding: 1rem 0; } th, td { - padding: 0.6rem 0.75rem; + padding: 0.4rem 0.5rem; } tr.active { @@ -11,13 +11,46 @@ tr.active { } code { - padding: 1rem; + max-height: 66rem; +} + +pre { + border: 1px solid #eee; + background: #fff; +} + +#response_body, #request_body { + clear: both; } #error, #request { display: none; } +#response_image { + box-shadow: 1px 1px 3px #cccccc; +} + +button.clear { + background-color: #d9534f; + border: 0.1rem solid #d43f3a; +} + +button.header { + background-color: #204d74; + border: 0.1rem solid #122b40; +} + +button.body { + background-color: #204d74; + border: 0.1rem solid #122b40; +} + +button.replay { + background-color: #f0ad4e; + border: 0.1rem solid #eea236; +} + .button { margin-right: 1rem; height: 2.4rem; @@ -29,3 +62,11 @@ code { text-overflow: ellipsis; white-space: nowrap; } + +tr.status4, tr.status5 { + color: #d43f3a; +} + +tr.replay { + color: #eea236; +} diff --git a/priv/www/hijack.js b/priv/www/hijack.js index 05f128add862532541f88d65ddd87b2f9296418c..c4403624db60f08ab8eae779819d567fd65f42ee 100644 --- a/priv/www/hijack.js +++ b/priv/www/hijack.js @@ -6,12 +6,15 @@ function poll() { var item = items[i]; var row = document.createElement("tr"); $(row).data("item", JSON.stringify(item)); - var fields = '<td>' + item.request.host + '</td><td>' + item.request.method + '</td><td>' + item.request.path + '</td>'; + var fields = '<td>' + item.request.method + '</td><td>' + item.request.path + '</td>'; if (typeof item.response !== 'undefined') { - $(row).append(fields + '<td>' + item.response.status + '</td>'); + $(row).append(fields + '<td>' + item.response.status + '</td>').addClass("status" + Math.floor(item.response.status / 100)); } else if (typeof item.error !== 'undefined') { $(row).append(fields + '<td>ERR</td>'); } + if (item.request.from == "replay") { + $(row).addClass("replay"); + } $('#requests').prepend(row); } }); @@ -23,13 +26,31 @@ function display(item) { $("#intro").hide(); if (typeof item.response !== 'undefined') { $("#error").hide(); - $("#request_status").html(item.request.method + " " + item.request.path + " → " + item.response.status); + $("#request_status").html(item.request.method + " " + item.request.path + " " + item.request.version + " → " + item.response.status); displayHeaders("#request_headers > tbody", item.request.headers); displayHeaders("#response_headers > tbody", item.response.headers); $("#request_headers").show(); $("#response_headers").show(); - $("#request_body").hide().children("code").text(item.request.body); - $("#response_body").hide().children("code").text(item.response.body); + if (item.request.body.length > 0) { + $("#request_body_button").show(); + $("#request_body").hide().children("code").text(item.request.body); + } else { + $("#request_body_button").hide(); + $("#request_body").hide(); + } + if (item.response.body.length > 0) { + $("#response_body").hide().children("code").text(item.response.body); + if (item.response.headers['content-type'].lastIndexOf('image/', 0) === 0) { + $("#response_body_button").hide(); + $("#response_image").attr('src', item.response.body).show(); + } else { + $("#response_body_button").show(); + $("#response_image").hide(); + } + } else { + $("#response_body_button").hide(); + $("#response_body").hide(); + } $("code").each(function (i, block) { hljs.highlightBlock(block); }); diff --git a/priv/www/index.html b/priv/www/index.html index e2ab6a2a37e262cb167e21675ba2abd2f0a81bb9..4f366b60b04db78d9981c7b35266eb6149e1d5e5 100644 --- a/priv/www/index.html +++ b/priv/www/index.html @@ -19,13 +19,12 @@ <div class="row"> <div class="column"> - <h2 class="float-left">All Requests</h2> - <button id="clear_button" class="button float-right">Clear</button> + <h3 class="float-left">All Requests</h3> + <button id="clear_button" class="button float-right clear">Clear</button> <table> <thead> <tr> - <th>Host</th> <th>Method</th> <th>Path</th> <th>Status</th> @@ -39,26 +38,27 @@ <div class="column"> <div id="intro"> - Welcome to Hijack </div> <div id="error"> - <h2 id="error_status"></h2> + <h3 id="error_status"></h3> - <h3 id="error_reason"></h3> + <h4 id="error_reason"></h4> </div> <div id="request"> - <h2 id="request_status"></h2> + <h3 id="request_status"></h3> - <h3>Request</h3> + <img id="response_image" src=""/> - <button id="request_header_button" class="button float-left">Headers</button> - <button id="request_body_button" class="button float-left">Body</button> - <button id="request_replay_button" class="button float-right">Replay</button> + <h4>Request</h4> + + <button id="request_header_button" class="button float-left header">Headers</button> + <button id="request_body_button" class="button float-left body">Body</button> + <button id="request_replay_button" class="button float-right replay">Replay</button> <table id="request_headers"> <thead> @@ -71,13 +71,12 @@ </tbody> </table> - <br/> <pre id="request_body"><code></code></pre> - <h3>Response</h3> + <h4>Response</h4> - <button id="response_header_button" class="button float-left">Headers</button> - <button id="response_body_button" class="button float-left">Body</button> + <button id="response_header_button" class="button float-left header">Headers</button> + <button id="response_body_button" class="button float-left body">Body</button> <table id="response_headers"> <thead> @@ -90,7 +89,6 @@ </tbody> </table> - <br/> <pre id="response_body"><code></code></pre> </div> diff --git a/src/hijack.erl b/src/hijack.erl index 1518a2803cf94b30d4847fe555e230d9d1a40342..6baa6519856d2e75a8a3b6e969c64e62f12954fc 100644 --- a/src/hijack.erl +++ b/src/hijack.erl @@ -5,6 +5,7 @@ {consul, $c, "consul", {string, "localhost:8083"}, "Consul host and port"}, {target, $t, "target", {string, "localhost:8080"}, "Target host and port"}, {snoop, $s, "snoop", {boolean, false}, "Ignore response from target"}, + {host_rewrite, $h, "host", {boolean, false}, "Rewrite host header"}, {port, $w, "port", {integer, 4040}, "Port for hijack webserver"}, {username, $u, "username", string, "Username"}, {password, $p, "password", string, "Password"}, @@ -128,6 +129,7 @@ execute(State = #state{opts = Opts}) -> {ConsulHost, ConsulPort} = parse_host_and_port(proplists:get_value(consul, Opts), 8083), case Transport:connect(ConsulHost, ConsulPort, []) of {ok, Socket} -> + ok = Transport:setopts(Socket, [binary, {active, false}, {packet, 4}]), {TargetHost, TargetPort} = parse_host_and_port(proplists:get_value(target, Opts), 80), NewState = State#state{transport = Transport, consul_socket = Socket, target_host = TargetHost, target_port = TargetPort}, Parent = self(), @@ -255,12 +257,20 @@ handle(State = #state{transport = Transport, consul_socket = Socket}, #{<<"actio Transport:close(Socket), State#state{consul_socket = undefined}; -handle(State = #state{target_host = Host, target_port = Port}, Request = #{<<"from">> := _From}) -> +handle(State = #state{opts = Opts, target_host = Host, target_port = Port}, Request = #{<<"from">> := _From, <<"headers">> := Headers}) -> Parent = self(), + Request1 = case proplists:get_value(host_rewrite, Opts) of + true -> + #{<<"host">> := RealHost} = Headers, + Headers1 = Headers#{<<"x-hijack-host">> => RealHost, <<"host">> => list_to_binary(Host)}, + Request#{<<"headers">> => Headers1}; + _ -> + Request + end, spawn_link( fun() -> {ok, Pid} = gun:open(Host, Port), - gun_loop(Parent, Request, Pid, undefined), + gun_loop(Parent, Request1, Pid, undefined), gun:shutdown(Pid) end), State; diff --git a/src/hijack_www.erl b/src/hijack_www.erl index 1d3f475315093720d4c0c6154f7f583457fb0751..a5bf9f6cfef377ecbf6173327c208419c437a38b 100644 --- a/src/hijack_www.erl +++ b/src/hijack_www.erl @@ -16,7 +16,9 @@ replay(SessionID, _Env, Input) -> requests(SessionID, _Env, _Input) -> Requests = get_requests(true), - BinaryJSON = jsx:encode(Requests), + UnzippedRequests = [gunzip(Request) || Request <- Requests], + EncodedRequests = [encode(Request) || Request <- UnzippedRequests], + BinaryJSON = jsx:encode(EncodedRequests), StringJSON = binary_to_list(BinaryJSON), mod_esi:deliver(SessionID, "Content-Type:application/json\r\n\r\n"), mod_esi:deliver(SessionID, StringJSON). @@ -24,3 +26,22 @@ requests(SessionID, _Env, _Input) -> get_requests(Purge) -> www_state ! {get, self(), Purge}, receive {requests, Requests} -> Requests end. + +gunzip(Request = #{<<"response">> := #{<<"headers">> := #{<<"content-encoding">> := <<"gzip">>}}}) -> + #{<<"response">> := Response} = Request, + #{<<"body">> := Body} = Response, + NewBody = zlib:gunzip(Body), + NewResponse = Response#{<<"body">> => NewBody}, + Request#{<<"response">> => NewResponse}; +gunzip(Request) -> + Request. + +encode(Request = #{<<"response">> := #{<<"headers">> := #{<<"content-type">> := <<"image/", ImageType/binary>>}}}) -> + #{<<"response">> := Response} = Request, + #{<<"body">> := Body} = Response, + B64 = base64:encode(Body), + NewBody = <<"data:image/", ImageType/binary, ";base64,", B64/binary>>, + NewResponse = Response#{<<"body">> => NewBody}, + Request#{<<"response">> => NewResponse}; +encode(Request) -> + Request.