# frozen_string_literal: true require 'json' module ZoomingProxy # Middleware # class Middleware attr_reader :host, :scheme, :forwarded MAX_DEPTH = 10 def initialize(app) @app = app end def call(env) @host = env['SERVER_NAME'] @scheme = env['rack.url_scheme'] @forwarded = env['REMOTE_ADDR'] @encoding = env['HTTP_ACCEPT_ENCODING'] handle_response(*@app.call(env)) end private def handle_response(status, headers, response) proxy_headers = headers.dup proxy_headers['X-Forwarded-For'] = forwarded proxy_headers['X-HAL-Zooming'] = '0' proxy_headers.delete('Accept-Encoding') compress embed(status, proxy_headers, response) end def compress(response) return response unless @encoding.to_s.include?('gzip') response[2][0] = string_to_gzip(response[2][0]) response[1]['Content-Encoding'] = 'gzip' response[1]['Content-Length'] = response[2][0].bytesize response end def string_to_gzip(source) output = StringIO.new gz = Zlib::GzipWriter.new(output) gz.write(source) gz.close output.string end def embed(status, headers, response, depth: 1) body = response_to_hash(response) return zoomed_response(status, headers, body, depth) unless zoomable?(headers, body, depth) body['_embedded'] ||= {} headers['X-HAL-Zooming'] = depth requested_links(body, headers).each { |link| body = zoom_rel(headers, body, depth, link) } zoomed_response(status, headers, body, depth) end def zoom_rel(headers, body, depth, link) relation, hrefs = link array = hrefs.respond_to?(:to_ary) [*hrefs].each do |href| res = request_rel(href, headers) embedded_body = handle_nested_embeds(res, depth) if array body['_embedded'][relation] ||= [] body['_embedded'][relation] << embedded_body else body['_embedded'][relation] = embedded_body end end body end def request_rel(href, headers) uri = URI.parse(href) uri.scheme = scheme uri.host = host Typhoeus.get(uri, headers: headers) end def response_to_hash(response) response = response.to_json if response.respond_to?(:each_key) JSON.parse(body_from_rack_response(response)) rescue JSON::ParserError nil end def handle_nested_embeds(res, depth) _, _, response = embed(res.code, res.headers, res.body, depth: depth + 1) response_to_hash(response) end def body_from_rack_response(response) [*response].join end def zoomed_response(status, headers, body, depth) json = body.to_json headers['Content-Length'] = json.bytesize headers['X-HAL-Zoomed'] = depth.to_s [status, headers, [json]] end def zoomable?(headers, body, depth) zoomable = depth <= MAX_DEPTH zoomable &= headers.include?('x-hal-zoom') zoomable &= headers['content-type'].include?('json') zoomable && !body.nil? end def requested_links(body, headers) links = requested_zooms(headers).select { |rel| body['_links'].key?(rel) } links.map do |rel| link = body['_links'][rel] [rel, link.respond_to?(:to_ary) ? link.map { |l| l['href'] } : link['href']] end end def requested_zooms(headers) headers['x-hal-zoom'].split(/\s+/) end end end