Skip to content

Commit 0d17964

Browse files
authored
Merge pull request #1 from Mocksi/add-eye-candy
create UI
2 parents b96744f + 9624ec7 commit 0d17964

File tree

9 files changed

+255
-16
lines changed

9 files changed

+255
-16
lines changed

Gemfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
source 'https://rubygems.org'
44

5-
gem 'rack'
6-
gem 'puma'
7-
gem 'thor'
85
gem 'json'
96
gem 'httpx', '~> 1.3'
7+
gem "puma", ">= 6.4.3"
8+
gem 'rack'
9+
gem 'thor'
1010

1111
group :development, :test do
1212
gem 'rspec', '~> 3.13'

Gemfile.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ GEM
66
httpx (1.3.0)
77
http-2 (>= 1.0.0)
88
json (2.7.1)
9-
nio4r (2.7.1)
10-
puma (6.4.2)
9+
nio4r (2.7.3)
10+
puma (6.4.3)
1111
nio4r (~> 2.0)
1212
rack (2.2.9)
1313
rack-test (1.1.0)
@@ -34,7 +34,7 @@ PLATFORMS
3434
DEPENDENCIES
3535
httpx (~> 1.3)
3636
json
37-
puma
37+
puma (>= 6.4.3)
3838
rack
3939
rack-test (~> 1.1)
4040
rspec (~> 3.13)

lib/command_executor.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44

55
class CommandExecutor
66
attr_reader :logger, :client_uuid, :endpoint_url
7-
REACTOR_URL = ENV['MOCKSI_REACTOR_URL'] || "https://crowllectordb.onrender.com/api/v1/reactor"
87

98
def initialize(logger, client_uuid)
109
@logger = logger
1110
@client_uuid = client_uuid
12-
@endpoint_url = REACTOR_URL
11+
@endpoint_url = Hawksi.configuration.reactor_url
1312
end
1413

1514
def execute_command(command, params)

lib/file_uploader.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33

44
class FileUploader
55
# FIXME: use a base URL for the upload and process URLs
6-
UPLOAD_URL = ENV['MOCKSI_UPLOAD_URL'] || "https://crowllectordb.onrender.com/api/v1/upload"
7-
PROCESS_URL = ENV['MOCKSI_PROCESS_URL'] || "https://crowllectordb.onrender.com/api/v1/process"
8-
96
def initialize(logger, client_uuid)
107
@logger = logger
118
@client_uuid = client_uuid
@@ -19,10 +16,15 @@ def upload_files(tar_gz_files)
1916

2017
def process_files
2118
HTTPX.wrap do |client|
22-
response = client.post(PROCESS_URL, headers: { "x-client-id" => @client_uuid })
19+
response = begin
20+
client.post(Hawksi.configuration.process_url, headers: { "x-client-id" => @client_uuid })
21+
rescue => e
22+
@logger.error "Failed to process files. Error: #{e.message}"
23+
end
24+
2325
if response.is_a?(HTTPX::Response)
2426
@logger.info "Processing uploaded files. Status: #{response.status}"
25-
else
27+
elsif response.is_a?(HTTPX::ErrorResponse)
2628
@logger.error "Failed to process files. Error: #{response.error}"
2729
end
2830
end
@@ -58,7 +60,7 @@ def upload_file(tar_gz_file)
5860

5961
def post_file(client, tar_gz_file)
6062
filename = File.basename(tar_gz_file)
61-
client.post("#{UPLOAD_URL}?filename=#{filename}",
63+
client.post("#{Hawksi.configuration.upload_url}?filename=#{filename}",
6264
headers: { "x-client-id" => @client_uuid },
6365
body: File.read(tar_gz_file))
6466
rescue => e

lib/hawksi.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,16 @@
1+
require 'hawksi/configuration'
12
require 'cli'
2-
require 'request_interceptor'
3+
require 'request_interceptor'
4+
5+
6+
BANNER = <<~BANNER
7+
_ _
8+
/\ /\__ ___ _| | _____(_)
9+
/ /_/ / _` \ \ /\ / / |/ / __| |
10+
/ __ / (_| |\ V V /| <\__ \ |
11+
\/ /_/ \__,_| \_/\_/ |_|\_\___/_|
12+
13+
BANNER
14+
15+
16+
puts BANNER

lib/hawksi/configuration.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module Hawksi
2+
class Configuration
3+
attr_accessor :mocksi_server, :reactor_url, :upload_url, :process_url
4+
5+
def initialize
6+
@mocksi_server = get_env('MOCKSI_SERVER') || "https://app.mocksi.ai"
7+
@reactor_url = get_env('MOCKSI_REACTOR_URL') || "https://api.mocksi.ai/api/v1/reactor"
8+
@upload_url = get_env('MOCKSI_UPLOAD_URL') || "https://api.mocksi.ai/api/v1/upload"
9+
@process_url = get_env('MOCKSI_PROCESS_URL') || "https://api.mocksi.ai/api/v1/process"
10+
end
11+
12+
private
13+
14+
def get_env(key)
15+
env_value = ENV.fetch(key, '').strip
16+
env_value.empty? ? nil : env_value
17+
end
18+
end
19+
20+
class << self
21+
def configuration
22+
@configuration ||= Configuration.new
23+
end
24+
25+
def configure
26+
yield(configuration)
27+
end
28+
end
29+
end

lib/mocksi_handler.rb

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
require 'httpx'
2+
3+
HAWK_SVG = <<~SVG
4+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
5+
<text x="9" y="8" font-size="8" text-anchor="middle" font-family="sans-serif">Hwk</text>
6+
</svg>
7+
SVG
8+
9+
module MocksiHandler
10+
class << self
11+
def handle(request)
12+
if request.path == '/favicon.ico'
13+
return [200, { 'Content-Type' => 'image/svg+xml' }, [HAWK_SVG]]
14+
end
15+
16+
begin
17+
# Get the mocksi server URL from configuration
18+
mocksi_server_url = Hawksi.configuration.mocksi_server
19+
raise "Mocksi server URL not configured" if mocksi_server_url.nil? || mocksi_server_url.empty?
20+
21+
# Prepare the full URL (mocksi_server + request path + query string)
22+
target_uri = URI.join(mocksi_server_url, request.fullpath)
23+
24+
# Prepare headers from the request
25+
headers = {}
26+
request.env.each do |key, value|
27+
if key.start_with?('HTTP_')
28+
header_key = key.sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
29+
headers[header_key] = value
30+
end
31+
## Yay for Rack's weirdness. See https://github.com/rack/rack/issues/1311
32+
if key == 'CONTENT_TYPE'
33+
headers['Content-Type'] = value
34+
end
35+
if key == 'CONTENT_LENGTH'
36+
headers['Content-Length'] = value
37+
end
38+
end
39+
40+
# Forward the cookies
41+
if request.cookies.any?
42+
headers['Cookie'] = request.cookies.map { |k, v| "#{k}=#{v}" }.join('; ')
43+
end
44+
45+
# Initialize httpx with headers
46+
http_client = HTTPX.with(headers: headers)
47+
48+
# Forward the body content if it's a POST or PUT request
49+
body = nil
50+
if %w[POST PUT].include?(request.request_method)
51+
request.body.rewind
52+
body = request.body.read
53+
end
54+
55+
# Make the HTTP request using the appropriate method
56+
response = http_client.request(request.request_method.downcase.to_sym, target_uri, body: body)
57+
58+
# Clone headers to allow modification
59+
response_headers = response.headers.dup
60+
61+
# Check for chunked transfer encoding and remove it
62+
if response_headers["transfer-encoding"]&.include?("chunked")
63+
response_headers.delete("transfer-encoding")
64+
end
65+
66+
# Handle gzip or deflate content-encoding if present
67+
response_body = response.body.to_s
68+
if response_headers["content-encoding"]&.include?("gzip")
69+
response_body = safe_decompress_gzip(response_body)
70+
response_headers.delete("content-encoding") # Remove content-encoding since the content is decompressed
71+
elsif response_headers["content-encoding"]&.include?("deflate")
72+
response_body = decompress_deflate(response_body)
73+
response_headers.delete("content-encoding") # Remove content-encoding since the content is decompressed
74+
end
75+
76+
# Return the response in a format compatible with Rack
77+
[response.status, response_headers, [response_body]]
78+
rescue => e
79+
# Handle any errors that occur during the reverse proxy operation
80+
[500, { 'Content-Type' => 'text/plain' }, ["Error: #{e.message}"]]
81+
end
82+
end
83+
84+
private
85+
86+
# Helper method to safely decompress gzip content, returning the original body if it's not in gzip format
87+
def safe_decompress_gzip(body)
88+
io = StringIO.new(body)
89+
begin
90+
gzip_reader = Zlib::GzipReader.new(io)
91+
decompressed_body = gzip_reader.read
92+
gzip_reader.close
93+
decompressed_body
94+
rescue Zlib::GzipFile::Error
95+
# If the body is not actually gzip, return the original body
96+
body
97+
end
98+
end
99+
100+
# Helper method to decompress deflate content
101+
def decompress_deflate(body)
102+
Zlib::Inflate.inflate(body)
103+
end
104+
end
105+
end

lib/request_interceptor.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
require 'json'
22
require 'logger'
33
require 'digest'
4-
require_relative '../lib/file_storage'
4+
require_relative './file_storage'
5+
require_relative './mocksi_handler'
56

67
module Hawksi
78
class RequestInterceptor
@@ -13,6 +14,14 @@ def initialize(app, logger: Logger.new('hawksi.log'), storage: FileStorage)
1314

1415
def call(env)
1516
request = Rack::Request.new(env)
17+
18+
if request.path.end_with?('/favicon.ico')
19+
return MocksiHandler.handle(request)
20+
end
21+
22+
if request.path.start_with?('/mocksi') || request.path.start_with?('/_') || request.path.start_with?('/api')
23+
return MocksiHandler.handle(request)
24+
end
1625
request_hash = generate_request_hash(request) # Generate a hash of the request
1726
log_request(request, request_hash)
1827

spec/lib/hawksi/configuration_spec.rb

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
2+
require 'spec_helper'
3+
require 'hawksi/configuration'
4+
5+
RSpec.describe Hawksi::Configuration do
6+
describe '.configuration' do
7+
it 'returns a Configuration instance' do
8+
expect(Hawksi.configuration).to be_an_instance_of(Hawksi::Configuration)
9+
end
10+
11+
it 'memoizes the configuration instance' do
12+
config1 = Hawksi.configuration
13+
config2 = Hawksi.configuration
14+
expect(config1).to eq(config2)
15+
end
16+
end
17+
18+
describe '.configure' do
19+
it 'yields the configuration instance' do
20+
expect { |b| Hawksi.configure(&b) }.to yield_with_args(Hawksi.configuration)
21+
end
22+
end
23+
24+
describe '#initialize' do
25+
let(:config) { Hawksi::Configuration.new }
26+
27+
it 'sets default values for URLs' do
28+
expect(config.instance_variable_get(:@mocksi_server)).to eq('https://app.mocksi.ai')
29+
expect(config.instance_variable_get(:@reactor_url)).to eq('https://api.mocksi.ai/api/v1/reactor')
30+
expect(config.instance_variable_get(:@upload_url)).to eq('https://api.mocksi.ai/api/v1/upload')
31+
expect(config.instance_variable_get(:@process_url)).to eq('https://api.mocksi.ai/api/v1/process')
32+
end
33+
34+
context 'when environment variables are set' do
35+
before do
36+
ENV['MOCKSI_SERVER'] = 'https://custom.mocksi.ai'
37+
ENV['MOCKSI_REACTOR_URL'] = 'https://custom.mocksi.ai/reactor'
38+
ENV['MOCKSI_UPLOAD_URL'] = 'https://custom.mocksi.ai/upload'
39+
ENV['MOCKSI_PROCESS_URL'] = 'https://custom.mocksi.ai/process'
40+
end
41+
42+
after do
43+
ENV.delete('MOCKSI_SERVER')
44+
ENV.delete('MOCKSI_REACTOR_URL')
45+
ENV.delete('MOCKSI_UPLOAD_URL')
46+
ENV.delete('MOCKSI_PROCESS_URL')
47+
end
48+
49+
it 'uses environment variables for URLs' do
50+
config = Hawksi::Configuration.new
51+
expect(config.instance_variable_get(:@mocksi_server)).to eq('https://custom.mocksi.ai')
52+
expect(config.instance_variable_get(:@reactor_url)).to eq('https://custom.mocksi.ai/reactor')
53+
expect(config.instance_variable_get(:@upload_url)).to eq('https://custom.mocksi.ai/upload')
54+
expect(config.instance_variable_get(:@process_url)).to eq('https://custom.mocksi.ai/process')
55+
end
56+
end
57+
context 'when a value is set to an empty string' do
58+
before do
59+
ENV['MOCKSI_SERVER'] = ''
60+
ENV['MOCKSI_REACTOR_URL'] = ''
61+
ENV['MOCKSI_UPLOAD_URL'] = ''
62+
ENV['MOCKSI_PROCESS_URL'] = ''
63+
end
64+
65+
after do
66+
ENV.delete('MOCKSI_SERVER')
67+
ENV.delete('MOCKSI_REACTOR_URL')
68+
ENV.delete('MOCKSI_UPLOAD_URL')
69+
ENV.delete('MOCKSI_PROCESS_URL')
70+
end
71+
72+
it 'uses default values for empty string environment variables' do
73+
config = Hawksi::Configuration.new
74+
expect(config.instance_variable_get(:@mocksi_server)).to eq('https://app.mocksi.ai')
75+
expect(config.instance_variable_get(:@reactor_url)).to eq('https://api.mocksi.ai/api/v1/reactor')
76+
expect(config.instance_variable_get(:@upload_url)).to eq('https://api.mocksi.ai/api/v1/upload')
77+
expect(config.instance_variable_get(:@process_url)).to eq('https://api.mocksi.ai/api/v1/process')
78+
end
79+
end
80+
end
81+
end

0 commit comments

Comments
 (0)