# To change this template, choose Tools | Templates # and open the template in the editor. require 'mu/maker' class Har HTTP_CONTENT_LENGTH_HEADER = 'Content-Length' HTTP_CONTENT_TYPE_HEADER = 'Content-Type' HTTP_CONTENT_ENCODING_HEADER = 'Content-Encoding' HTTP_TRANSFER_ENCODING_HEADER = 'Transfer-Encoding' HTTP_CONTENT_TRANSFER_ENCODING_HEADER = 'Content-Transfer-Encoding' HTTP_FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded' HTTP_TEXT_PLAIN_CONTENT_TYPE = 'text/plain' HTTP_TEXT_HTML_CONTENT_TYPE = 'text/html' HTTP_TEXT_XML_CONTENT_TYPE = 'text/xml' HTTP_APPLICATION_XML_CONTENT_TYPE = 'application/xml' HTTP_TEXT_JSON_CONTENT_TYPE = 'text/json' HTTP_APPLICATION_JSON_CONTENT_TYPE = 'application/json' HTTP_TEXT_JAVASCRIPT_CONTENT_TYPE = 'text/javascript' HTTP_APPLICATION_JAVASCRIPT_CONTENT_TYPE = 'application/x-javascript' HTTP_TEXT_CSS_CONTENT_TYPE = 'text/css' HTTP_TEXT_CONTENT_TYPES = [ HTTP_FORM_CONTENT_TYPE, HTTP_TEXT_PLAIN_CONTENT_TYPE, HTTP_TEXT_HTML_CONTENT_TYPE, HTTP_TEXT_XML_CONTENT_TYPE, HTTP_APPLICATION_XML_CONTENT_TYPE, HTTP_TEXT_JSON_CONTENT_TYPE, HTTP_APPLICATION_JSON_CONTENT_TYPE, HTTP_TEXT_JAVASCRIPT_CONTENT_TYPE, HTTP_APPLICATION_JAVASCRIPT_CONTENT_TYPE, HTTP_TEXT_CSS_CONTENT_TYPE ] HTTP_GZIP_CONTENT_ENCODING = 'gzip' HTTP_DEFLATE_CONTENT_ENCODING = 'deflate' HTTP_EMPTY_GZIP_BODY = "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00" HTTP_CHUNKED_TRANSFER_ENCODING = 'chunked' HTTP_VIDEO_X_MS_WMV_CONTENT_TYPE = 'video/x-ms-wmv' HTTP_VIDEO_X_MS_WMA_CONTENT_TYPE = 'audio/x-ms-wma' HTTP_STREAMING_CONTENT_TYPES = [ HTTP_VIDEO_X_MS_WMV_CONTENT_TYPE, HTTP_VIDEO_X_MS_WMA_CONTENT_TYPE ] HTTP_CONTENT_SLICE_SIZE = 1024 attr_reader :har_file attr_accessor :entries,:hosts,:har,:har_hosts def initialize har_file, options, ignores @har_file = har_file @hosts = {} @options = options @ignores = ignores begin @har = JSON File.read(@har_file) rescue Exception=>e puts "There was an error reading the JSON har file, probably a parsing problem" raise e end end # Return a list of entries from the har file def get_entries @entries = @har['log']['entries'] end def generate ios # Attempt to create the scenario generated = MuSL::Maker.create do |scenario| # First declare all the hosts scenario.hosts do |host| self.build_hosts host end # Then build all the steps scenario.steps do |step| self.build_steps step, scenario end # scenario.steps end # Maker.create end # Return a list of hosts from the har file def get_hosts ignores, options # Regex to check if certain kinds of entries should be ignored, like css, js, images based on command line options ignores_regex = ignores.join('|') # Iterate through each har entry and parse out the important url pieces begin hosts_seen = Hash.new(0) host_count = 0 @har["log"]["entries"].each_with_index do |entry, index| # Check our command line exclusions if(options.ignore) next if (entry['response']['content']['mimeType'] =~ /#{ignores_regex}/o ) end # Build the hosts list host = nil m = entry["musl"] = {} m["url_object"] = URI.parse entry["request"]["url"] m["url"] = {} m['url']['port'] = m["url_object"].port || (m["url_object"].scheme === 'http' ? 80 : 443) m['url']['pathname'] = m['url_object'].path || '/' m["url"]["search"] = m["url_object"].query || '' m["url"]["hash"] = m["url_object"].fragment || '' # Create the host entry hash with underscores instead of dots for the host value entry['request']['headers'].each do |header| if (!host && header['name'].downcase === 'host') host = header['value'] # Set the hosts_seen value to host_count and increment the host_count unless we have already seen the host hosts_seen[host] += host_count && host_count += 1 unless hosts_seen.has_key?(host) # OLD WAY - Substitute the dots with underscores for the musl host # OLD WAY entry['musl'][host] = host.gsub(/[^a-zA-Z0-9_]/, '_') entry['musl'][host] = hosts_seen[host].to_s if entry['musl'][host].match /^[0-9]/ entry['musl'][host] = 'host_' + entry['musl'][host] end end end @hosts[host] = entry['musl'][host] entry['musl']['host'] = entry['musl'][host] host = nil if entry['response']['cookies'] #p "Entry-response-cookies: #{entry['response']['cookies']}" entry['response']['cookies'].each do |cookie| # TODO: Need to mark the index of this entry against # this cookie so we can search for it easily. end end end rescue Exception => e puts e.message puts e.backtrace.inspect raise e end return @hosts end # get_hosts # Finds a specific cookie from the response cookies def find_cookie name, step if step === 0 return end (step-1).downto(0) {|i| entry = @entries[i] res = entry['response'] if (res['cookies'] && res['cookies'].length > 0) for j in 0..res['cookies'].length do cookie = res['cookies'][j] unless cookie.nil? if cookie['name'] === name return i end end end end } return end # find_cookie # Takes the headers def build_headers cs, headers, cookies, i headers.each do |header| # If this is a Cookie header, try and map the cookie # value to a step variable preceding this step if ('cookie' === header['name'].downcase) value = self.escape(header['value']) for j in 0..cookies.length cookie = cookies[j] if defined? cookie['name'] step = self.find_cookie cookie['name'], i # THE FOLLOWING LOGIC PROBABLY NEEDS FIXING FOR COOKIES if (step != nil) regex = /"(#{cookie['name']})" + "=([^;]*)"/ if value.match regex raise "I KNOW THERE IS A PROBLE WITH THE code below" # TODO: FIX THIS LOGIC value = value.replace(regex, cookie['name'] + '=#{@cr' + step + '.' + cookie['name'].gsub(/[^a-zA-Z0-9_]/, '_') + '}') end end end end cs.line(header['name'] + ': ' + value) else cs.line(header['name'] + ': ' + header['value']) end end #end headers.each end # end build_headers def build_postdata cs, req, entry_count, scenario if req['postData']['params'].length > 0 cs.header('Content-Length') do cs.length_string({'of' => "body_#{entry_count}"}) end cs.line() cs.block('body_' + "#{entry_count}" + ' = struct [', ']') do #assert_equal(req['postData']['mimeType'], 'application/x-www-form-urlencoded', 'unsupported mime type') cs.block('dsv(delimiter: "&") [',']') do req['postData']['params'].each do |param| cs.block('struct [',']') do cs.string(param['name'] + '=') cs.block('uri_percent_encode [',']') do cs.string param['value'] end end end end end else unless(@options.ignore_payload) cs.line() self.build_payload(cs, entry_count, req, scenario) end end end # end build_postdata def build_hosts hosts # TODO: Right now v4 is hard-coded, need to change this hosts.create 'host_0', 'v4', 'browser' har_hosts = self.get_hosts @ignores, @options # Build the host entries into the scenario har_hosts.each do |hhost,har_host_value| hosts.create(har_hosts[hhost], 'v4', hhost) end end def build_steps steps, scenario # Create the steps entries = self.get_entries entry_count = 0 for i in 0..entries.length entry = entries[i] next unless defined? entry['musl'] m = entry['musl'] req = entry['request'] res = entry['response'] next if m.nil? xopts = { 'src' => '&host_0', 'dst' => '&' + "#{m['host']}" } xklass = m['url_object'].scheme === 'http' ? 'tcp' : 'ssl' xopts['dst_port'] = m['url']['port'] # Create the xport options in the scenario steps.xport("xport#{entry_count}", xklass, xopts) # client_send comments for each entry steps.comment(req["method"] + ' ' + m['url']['pathname'] + ' ' + req['httpVersion']) # main client_send lines for each entry steps.client_send("cs#{entry_count}","xport#{entry_count}") do |cs| cs.line(req['method'] + ' ' + m['url']['pathname'] + m['url']['search'] + m['url']['hash'] + ' ' + req['httpVersion']) # For each header self.build_headers cs, req['headers'], req['cookies'], i # For building the form post params block if req.has_key?('postData') self.build_postdata cs, req, entry_count, scenario else cs.line('Content-Length: 0') end # end if req.has_key('postData') cs.line() # For building the client content payload block into the scenario if req.has_key?('content') self.build_payload(cs, entry_count, req, scenario) end unless @options.ignore_payload end # end steps.client_send # Skip the server side if the command line option included --endpint #unless @options.endpoint steps.server_receive("sr#{entry_count}", "cs#{entry_count}") do || #return nil end # For adding comment headers for the server side steps.comment(res['httpVersion'] + ' ' + "#{res['status']}" + ' ' + res['statusText']) # Build server_send portion steps.server_send("ss#{entry_count}", "xport#{entry_count}") do |ss| ss.line(res['httpVersion'] + ' ' + "#{res['status']}" + ' ' + res['statusText']) res['headers'].each do |header| ss.line(header['name'] + ': ' + header['value']) end ss.line() # For building the server content payload block into the scenario if res.has_key?('content') build_payload ss, entry_count, res, scenario end unless @options.ignore_payload end # end server_send #end # If the endpoint option is chosen only include cs 'client send' values instead of ss 'server send' values receive_side = @options.endpoint ? 'cs' : 'ss' # Build client_receive portion steps.client_receive("cr#{entry_count}", "#{receive_side}#{entry_count}") do |cr| cr.assertions do |as| as.create('/^HTTP\\/1\\.1 (\\d+)/ == ' + "#{res['status']}") end if (res['cookies'] && res['cookies'].length > 0) cr.variables do |vs| res['cookies'].each do |cookie| vs.create('@' + cookie['name'].gsub(/[^a-zA-Z0-9_]/, '_') + ' = ' + '/' + cookie['name'] + '=([^;]*)' + '/:1') end end end end entry_count += 1 end end # If the payload is to be included, handle the inclusion logic here def build_payload(cs_send, entry_count, req_res, scenario) # If it is coming from the client side it will be part of the postData payload = req_res.has_key?('content') ? req_res['content']['text'] : req_res['postData']['text'] #raise "req_res #{req_res} PAYLOAD #{payload}" return if payload.nil? content_encoding = false transfer_encoding = false # In case we need encoding types different than gzip, and chunked later on req_res['headers'].each do |header| if(header['name'] === 'Transfer-Encoding') # We can do chunked if header['value'] == HTTP_CHUNKED_TRANSFER_ENCODING transfer_encoding = true else raise NotImplementedError, "Transfer-Encoding: #{header['value']}" end elsif(header['name'] === 'Content-Encoding') # Check out content encoding # We can do GZIP if header['value'] == HTTP_GZIP_CONTENT_ENCODING content_encoding = true else raise NotImplementedError, "Content-Encoding: #{header['value']}" end end end # http chunked if transfer_encoding cs_send.literal_no_format("http_chunk_encode(chunk_size: #{req_res['content']['size']}) [") end # gzip only for now if content_encoding cs_send.literal_no_format("gzip_compress[") end # If we need to replace the content with a special repeated field if @options.strip_large_content body = [] if req_res['content']['size'] > @options.large_content_size # Take first 1K bytes from the content and repeat body << payload[0, HTTP_CONTENT_SLICE_SIZE] else # Set response body body << payload end # Yes calculate the number of 1K chunks we need to inject count = req_res['content']['size'] / HTTP_CONTENT_SLICE_SIZE remainder = req_res['content']['size'] % HTTP_CONTENT_SLICE_SIZE # TODO: If there is a reminder add one more repeat count for now if remainder > 0 count += 1 end # Open the field cs_send.literal_no_format("repeat(count: %d) [" % [count]) # No, write the content as binary string cs_send.literal_no_format("\"0h") # Write all blocks body.each do |block| # Write each byte in the block block.each_byte do |byte| cs_send.literal_no_format("%02x" % byte) end end cs_send.literal_no_format("\""); else cs_send.literal_no_format("\"" + self.escape(payload) + "\"") end # Close repeat field if @options.strip_large_content cs_send.literal_no_format("]") end # Close content encoding, if needed if content_encoding cs_send.literal_no_format("]") end # Close transfer encoding, if needed if transfer_encoding cs_send.literal_no_format("]") end end # build_payload() # TO-DO: Implement this check def text_body? HTTP_TEXT_CONTENT_TYPES.include?(@content_type) and not @content_transfer_encoding.to_s == 'binary' and not @mu_content_transfer_encoding.to_s == 'binary' end ESCAPES= Array.new 256 do |i| case i when 9; "\\t".freeze when 13; "\\r".freeze when 92; "\\\\".freeze when 10; "\\n".freeze when 32..126; i.chr.freeze else ; ('\x%02x' % i).freeze end end ESCAPES['"'.ord] = %q{\"} ESCAPES["'".ord] = %q{\'} ESCAPES.freeze # Takes input and a table that maps ascii codes to their representation def escape input, escapes=nil escapes ||= ESCAPES output = [] input.each_byte do |i| output << escapes[i] end output.join end end