Skip to content

Commit 6e3cab8

Browse files
committed
fix: use MD5 hashing used for image cache keys
MD5 hashing used for image cache keys Disable SSL in production because SSL is enforced at the Traefik layer
1 parent fdab2d8 commit 6e3cab8

2 files changed

Lines changed: 72 additions & 51 deletions

File tree

app/controllers/api/v1/images_controller.rb

Lines changed: 68 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -21,81 +21,98 @@ class ImagesController < BaseController
2121
# @return [Binary] The image data with appropriate content-type
2222
def proxy
2323
url = params[:url]
24+
return render_invalid_url unless valid_image_url?(url)
2425

25-
# Validate URL
26-
unless valid_image_url?(url)
27-
render json: { error: 'Invalid or unauthorized URL' }, status: :bad_request
28-
return
29-
end
30-
31-
# Try to get from cache first
32-
cache_key = "image_proxy:#{Digest::MD5.hexdigest(url)}"
33-
cached_data = Rails.cache.fetch(cache_key, expires_in: 7.days) do
34-
fetch_external_image(url)
35-
end
36-
37-
if cached_data[:error]
38-
render json: { error: cached_data[:error] }, status: :bad_gateway
39-
return
40-
end
26+
cached_data = fetch_cached_image(url)
27+
return render_fetch_error(cached_data[:error]) if cached_data[:error]
4128

42-
# Send the cached image
43-
send_data cached_data[:body],
44-
type: cached_data[:content_type],
45-
disposition: 'inline',
46-
filename: File.basename(URI.parse(url).path)
29+
send_image_data(cached_data, url)
4730
rescue StandardError => e
48-
Rails.logger.error("Image proxy error: #{e.message}")
49-
render json: { error: 'Failed to fetch image' }, status: :internal_server_error
31+
handle_proxy_error(e)
5032
end
5133

5234
private
5335

36+
ALLOWED_DOMAINS = [
37+
'upload.wikimedia.org',
38+
'ddragon.leagueoflegends.com',
39+
'raw.communitydragon.org',
40+
'static.wikia.nocookie.net',
41+
'commons.wikimedia.org'
42+
].freeze
43+
44+
HTTP_TIMEOUT_OPTIONS = { open_timeout: 5, read_timeout: 10 }.freeze
45+
5446
# Validates if the URL is from an allowed domain
5547
def valid_image_url?(url)
5648
return false if url.blank?
5749

5850
uri = URI.parse(url)
59-
allowed_domains = [
60-
'upload.wikimedia.org',
61-
'ddragon.leagueoflegends.com',
62-
'raw.communitydragon.org',
63-
'static.wikia.nocookie.net',
64-
'commons.wikimedia.org'
65-
]
66-
67-
allowed_domains.any? { |domain| uri.host&.include?(domain) }
51+
ALLOWED_DOMAINS.any? { |domain| uri.host&.include?(domain) }
6852
rescue URI::InvalidURIError
6953
false
7054
end
7155

56+
# Fetches image from cache or external source
57+
def fetch_cached_image(url)
58+
cache_key = "image_proxy:#{Digest::SHA256.hexdigest(url)}"
59+
Rails.cache.fetch(cache_key, expires_in: 7.days) do
60+
fetch_external_image(url)
61+
end
62+
end
63+
7264
# Fetches image from external URL
7365
def fetch_external_image(url)
7466
uri = URI.parse(url)
67+
response = perform_http_request(uri)
68+
process_http_response(response)
69+
rescue StandardError => e
70+
Rails.logger.error("Failed to fetch image from #{url}: #{e.message}")
71+
{ error: e.message }
72+
end
7573

76-
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
77-
open_timeout: 5, read_timeout: 10) do |http|
74+
# Performs HTTP request to fetch image
75+
def perform_http_request(uri)
76+
Net::HTTP.start(uri.host, uri.port,
77+
use_ssl: uri.scheme == 'https',
78+
**HTTP_TIMEOUT_OPTIONS) do |http|
7879
request = Net::HTTP::Get.new(uri.request_uri)
7980
request['User-Agent'] = 'ProStaff-API/1.0 (Image Proxy)'
81+
http.request(request)
82+
end
83+
end
8084

81-
response = http.request(request)
82-
83-
if response.is_a?(Net::HTTPSuccess)
84-
{
85-
body: response.body,
86-
content_type: response['content-type'] || 'image/png'
87-
}
88-
else
89-
{
90-
error: "External service returned #{response.code}",
91-
content_type: 'text/plain',
92-
body: ''
93-
}
94-
end
85+
# Processes HTTP response
86+
def process_http_response(response)
87+
if response.is_a?(Net::HTTPSuccess)
88+
{ body: response.body, content_type: response['content-type'] || 'image/png' }
89+
else
90+
{ error: "External service returned #{response.code}", content_type: 'text/plain', body: '' }
9591
end
96-
rescue StandardError => e
97-
Rails.logger.error("Failed to fetch image from #{url}: #{e.message}")
98-
{ error: e.message }
92+
end
93+
94+
# Renders invalid URL error
95+
def render_invalid_url
96+
render json: { error: 'Invalid or unauthorized URL' }, status: :bad_request
97+
end
98+
99+
# Renders fetch error
100+
def render_fetch_error(error)
101+
render json: { error: error }, status: :bad_gateway
102+
end
103+
104+
# Sends image data to client
105+
def send_image_data(cached_data, url)
106+
send_data cached_data[:body],
107+
type: cached_data[:content_type],
108+
disposition: 'inline',
109+
filename: File.basename(URI.parse(url).path)
110+
end
111+
112+
# Handles proxy errors
113+
def handle_proxy_error(error)
114+
Rails.logger.error("Image proxy error: #{error.message}")
115+
render json: { error: 'Failed to fetch image' }, status: :internal_server_error
99116
end
100117
end
101118
end

config/environments/production.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
config.active_storage.variant_processor = :mini_magick
2828

2929
# SSL Configuration - Traefik terminates SSL, Rails receives HTTP
30+
# Note: SSL is enforced at the Traefik layer (reverse proxy), not at the Rails layer.
31+
# This is secure because: (1) Traefik handles HTTPS/TLS termination, (2) internal
32+
# communication between Traefik and Rails is over a private Docker network.
33+
# Setting force_ssl = true would cause redirect loops.
3034
config.force_ssl = false
3135

3236

0 commit comments

Comments
 (0)