rails_ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec_status +96 -0
- data/AGENT_GUIDE.md +513 -0
- data/Appraisals +49 -0
- data/COMMERCIAL_LICENSE_TEMPLATE.md +92 -0
- data/FEATURES.md +204 -0
- data/LEGAL_PROTECTION_GUIDE.md +222 -0
- data/LICENSE +62 -0
- data/LICENSE_SUMMARY.md +74 -0
- data/MIT-LICENSE +62 -0
- data/PERFORMANCE.md +300 -0
- data/PROVIDERS.md +495 -0
- data/README.md +454 -0
- data/Rakefile +11 -0
- data/SPEED_OPTIMIZATIONS.md +217 -0
- data/STRUCTURE.md +139 -0
- data/USAGE_GUIDE.md +288 -0
- data/app/channels/ai_stream_channel.rb +33 -0
- data/app/components/ai/prompt_component.rb +25 -0
- data/app/controllers/concerns/ai/context_aware.rb +77 -0
- data/app/controllers/concerns/ai/streaming.rb +41 -0
- data/app/helpers/ai_helper.rb +164 -0
- data/app/jobs/ai/generate_embedding_job.rb +25 -0
- data/app/jobs/ai/generate_summary_job.rb +25 -0
- data/app/models/concerns/ai/embeddable.rb +38 -0
- data/app/views/rails_ai/dashboard/index.html.erb +51 -0
- data/config/routes.rb +19 -0
- data/lib/generators/rails_ai/install/install_generator.rb +38 -0
- data/lib/rails_ai/agents/agent_manager.rb +258 -0
- data/lib/rails_ai/agents/agent_team.rb +243 -0
- data/lib/rails_ai/agents/base_agent.rb +331 -0
- data/lib/rails_ai/agents/collaboration.rb +238 -0
- data/lib/rails_ai/agents/memory.rb +116 -0
- data/lib/rails_ai/agents/message_bus.rb +95 -0
- data/lib/rails_ai/agents/specialized_agents.rb +391 -0
- data/lib/rails_ai/agents/task_queue.rb +111 -0
- data/lib/rails_ai/cache.rb +14 -0
- data/lib/rails_ai/config.rb +40 -0
- data/lib/rails_ai/context.rb +7 -0
- data/lib/rails_ai/context_analyzer.rb +86 -0
- data/lib/rails_ai/engine.rb +48 -0
- data/lib/rails_ai/events.rb +9 -0
- data/lib/rails_ai/image_context.rb +110 -0
- data/lib/rails_ai/performance.rb +231 -0
- data/lib/rails_ai/provider.rb +8 -0
- data/lib/rails_ai/providers/anthropic_adapter.rb +256 -0
- data/lib/rails_ai/providers/base.rb +60 -0
- data/lib/rails_ai/providers/dummy_adapter.rb +29 -0
- data/lib/rails_ai/providers/gemini_adapter.rb +509 -0
- data/lib/rails_ai/providers/openai_adapter.rb +535 -0
- data/lib/rails_ai/providers/secure_anthropic_adapter.rb +206 -0
- data/lib/rails_ai/providers/secure_openai_adapter.rb +284 -0
- data/lib/rails_ai/railtie.rb +48 -0
- data/lib/rails_ai/redactor.rb +12 -0
- data/lib/rails_ai/security/api_key_manager.rb +82 -0
- data/lib/rails_ai/security/audit_logger.rb +46 -0
- data/lib/rails_ai/security/error_handler.rb +62 -0
- data/lib/rails_ai/security/input_validator.rb +176 -0
- data/lib/rails_ai/security/secure_file_handler.rb +45 -0
- data/lib/rails_ai/security/secure_http_client.rb +177 -0
- data/lib/rails_ai/security.rb +0 -0
- data/lib/rails_ai/version.rb +5 -0
- data/lib/rails_ai/window_context.rb +103 -0
- data/lib/rails_ai.rb +502 -0
- data/monitoring/ci_setup_guide.md +214 -0
- data/monitoring/enhanced_monitoring_script.rb +237 -0
- data/monitoring/google_alerts_setup.md +42 -0
- data/monitoring_log_20250921.txt +0 -0
- data/monitoring_script.rb +161 -0
- data/rails_ai.gemspec +54 -0
- data/scripts/security_scanner.rb +353 -0
- data/setup_monitoring.sh +163 -0
- data/wiki/API-Documentation.md +734 -0
- data/wiki/Architecture-Overview.md +672 -0
- data/wiki/Contributing-Guide.md +407 -0
- data/wiki/Development-Setup.md +532 -0
- data/wiki/Home.md +278 -0
- data/wiki/Installation-Guide.md +527 -0
- data/wiki/Quick-Start.md +186 -0
- data/wiki/README.md +135 -0
- data/wiki/Release-Process.md +467 -0
- metadata +385 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsAi
|
4
|
+
module Security
|
5
|
+
class ErrorHandler
|
6
|
+
def self.handle_security_error(error, context = {})
|
7
|
+
# Log security error
|
8
|
+
AuditLogger.log_security_event(:security_error, {
|
9
|
+
error_class: error.class.name,
|
10
|
+
error_message: error.message,
|
11
|
+
context: context,
|
12
|
+
timestamp: Time.now
|
13
|
+
})
|
14
|
+
|
15
|
+
# Return sanitized error message
|
16
|
+
sanitized_message = sanitize_error_message(error)
|
17
|
+
|
18
|
+
# In production, don't expose internal details
|
19
|
+
if Rails.env.production?
|
20
|
+
case error
|
21
|
+
when ValidationError
|
22
|
+
"Invalid input provided"
|
23
|
+
when RateLimitError
|
24
|
+
"Rate limit exceeded. Please try again later"
|
25
|
+
when SecurityError
|
26
|
+
"Security error occurred"
|
27
|
+
else
|
28
|
+
"An error occurred while processing your request"
|
29
|
+
end
|
30
|
+
else
|
31
|
+
sanitized_message
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.sanitize_error_message(error)
|
36
|
+
message = error.message.to_s
|
37
|
+
|
38
|
+
# Remove sensitive information
|
39
|
+
message = message.gsub(/api[_-]?key/i, 'API_KEY')
|
40
|
+
message = message.gsub(/password/i, 'PASSWORD')
|
41
|
+
message = message.gsub(/secret/i, 'SECRET')
|
42
|
+
message = message.gsub(/token/i, 'TOKEN')
|
43
|
+
|
44
|
+
# Remove file paths
|
45
|
+
message = message.gsub(%r{/[a-zA-Z0-9_/.-]+}, '[PATH]')
|
46
|
+
|
47
|
+
# Remove IP addresses
|
48
|
+
message = message.gsub(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, '[IP]')
|
49
|
+
|
50
|
+
# Remove URLs
|
51
|
+
message = message.gsub(%r{https?://[^\s]+}, '[URL]')
|
52
|
+
|
53
|
+
message
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.log_and_raise(error, context = {})
|
57
|
+
handle_security_error(error, context)
|
58
|
+
raise error
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'ipaddr'
|
5
|
+
|
6
|
+
module RailsAi
|
7
|
+
module Security
|
8
|
+
class InputValidator
|
9
|
+
MAX_FILE_SIZE = 50.megabytes
|
10
|
+
ALLOWED_IMAGE_TYPES = %w[image/jpeg image/png image/gif image/webp].freeze
|
11
|
+
ALLOWED_VIDEO_TYPES = %w[video/mp4 video/webm video/quicktime].freeze
|
12
|
+
ALLOWED_AUDIO_TYPES = %w[audio/mpeg audio/wav audio/ogg audio/mp4].freeze
|
13
|
+
|
14
|
+
ALLOWED_SCHEMES = %w[http https].freeze
|
15
|
+
BLOCKED_HOSTS = %w[
|
16
|
+
localhost 127.0.0.1 0.0.0.0 ::1
|
17
|
+
10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
18
|
+
metadata.google.internal
|
19
|
+
instance-data.ec2.internal
|
20
|
+
].freeze
|
21
|
+
|
22
|
+
def self.validate_text_input(text, max_length: 10000)
|
23
|
+
raise ValidationError, "Text cannot be nil" if text.nil?
|
24
|
+
|
25
|
+
text = text.to_s.strip
|
26
|
+
raise ValidationError, "Text cannot be empty" if text.empty?
|
27
|
+
raise ValidationError, "Text too long (max: #{max_length})" if text.length > max_length
|
28
|
+
|
29
|
+
if text.match?(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/)
|
30
|
+
raise ValidationError, "Text contains invalid characters"
|
31
|
+
end
|
32
|
+
|
33
|
+
text
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.validate_file_path(path)
|
37
|
+
raise ValidationError, "File path cannot be nil" if path.nil?
|
38
|
+
|
39
|
+
path = File.expand_path(path)
|
40
|
+
|
41
|
+
if path.include?('..') || path.include?('~')
|
42
|
+
raise ValidationError, "Invalid file path: directory traversal detected"
|
43
|
+
end
|
44
|
+
|
45
|
+
unless File.exist?(path) && File.readable?(path)
|
46
|
+
raise ValidationError, "File not found or not readable: #{path}"
|
47
|
+
end
|
48
|
+
|
49
|
+
if File.size(path) > MAX_FILE_SIZE
|
50
|
+
raise ValidationError, "File too large (max: #{MAX_FILE_SIZE} bytes)"
|
51
|
+
end
|
52
|
+
|
53
|
+
path
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.validate_base64_data(data, expected_type: nil)
|
57
|
+
raise ValidationError, "Base64 data cannot be nil" if data.nil?
|
58
|
+
|
59
|
+
unless data.match?(/\A[A-Za-z0-9+\/]*={0,2}\z/)
|
60
|
+
raise ValidationError, "Invalid base64 format"
|
61
|
+
end
|
62
|
+
|
63
|
+
begin
|
64
|
+
decoded = Base64.strict_decode64(data)
|
65
|
+
if decoded.size > MAX_FILE_SIZE
|
66
|
+
raise ValidationError, "Base64 data too large (max: #{MAX_FILE_SIZE} bytes)"
|
67
|
+
end
|
68
|
+
rescue ArgumentError
|
69
|
+
raise ValidationError, "Invalid base64 data"
|
70
|
+
end
|
71
|
+
|
72
|
+
if expected_type && data.start_with?("data:")
|
73
|
+
mime_type = data.split(';')[0].split(':')[1]
|
74
|
+
unless valid_mime_type?(mime_type, expected_type)
|
75
|
+
raise ValidationError, "Invalid MIME type: #{mime_type}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
data
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.validate_url(url)
|
83
|
+
raise ValidationError, "URL cannot be nil" if url.nil?
|
84
|
+
|
85
|
+
begin
|
86
|
+
uri = URI.parse(url)
|
87
|
+
rescue URI::InvalidURIError
|
88
|
+
raise ValidationError, "Invalid URL format"
|
89
|
+
end
|
90
|
+
|
91
|
+
unless ALLOWED_SCHEMES.include?(uri.scheme)
|
92
|
+
raise ValidationError, "Invalid URL scheme: #{uri.scheme}"
|
93
|
+
end
|
94
|
+
|
95
|
+
if blocked_host?(uri.host)
|
96
|
+
raise ValidationError, "URL blocked for security reasons"
|
97
|
+
end
|
98
|
+
|
99
|
+
if private_ip?(uri.host)
|
100
|
+
raise ValidationError, "Private IP addresses not allowed"
|
101
|
+
end
|
102
|
+
|
103
|
+
url
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.validate_messages(messages)
|
107
|
+
raise ValidationError, "Messages cannot be nil" if messages.nil?
|
108
|
+
raise ValidationError, "Messages must be an array" unless messages.is_a?(Array)
|
109
|
+
raise ValidationError, "Messages cannot be empty" if messages.empty?
|
110
|
+
raise ValidationError, "Too many messages (max: 100)" if messages.length > 100
|
111
|
+
|
112
|
+
messages.each_with_index do |message, index|
|
113
|
+
validate_message(message, index)
|
114
|
+
end
|
115
|
+
|
116
|
+
messages
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def self.validate_message(message, index)
|
122
|
+
raise ValidationError, "Message #{index} must be a hash" unless message.is_a?(Hash)
|
123
|
+
raise ValidationError, "Message #{index} missing role" unless message[:role]
|
124
|
+
raise ValidationError, "Message #{index} missing content" unless message[:content]
|
125
|
+
|
126
|
+
valid_roles = %w[system user assistant]
|
127
|
+
unless valid_roles.include?(message[:role])
|
128
|
+
raise ValidationError, "Message #{index} has invalid role: #{message[:role]}"
|
129
|
+
end
|
130
|
+
|
131
|
+
validate_text_input(message[:content], max_length: 50000)
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.valid_mime_type?(mime_type, expected_type)
|
135
|
+
case expected_type
|
136
|
+
when :image
|
137
|
+
ALLOWED_IMAGE_TYPES.include?(mime_type)
|
138
|
+
when :video
|
139
|
+
ALLOWED_VIDEO_TYPES.include?(mime_type)
|
140
|
+
when :audio
|
141
|
+
ALLOWED_AUDIO_TYPES.include?(mime_type)
|
142
|
+
else
|
143
|
+
true
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.blocked_host?(host)
|
148
|
+
return true if host.nil?
|
149
|
+
|
150
|
+
BLOCKED_HOSTS.any? do |blocked|
|
151
|
+
if blocked.include?('/')
|
152
|
+
begin
|
153
|
+
blocked_network = IPAddr.new(blocked)
|
154
|
+
blocked_network.include?(host)
|
155
|
+
rescue IPAddr::InvalidAddressError
|
156
|
+
false
|
157
|
+
end
|
158
|
+
else
|
159
|
+
host == blocked || host.end_with?(".#{blocked}")
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.private_ip?(host)
|
165
|
+
return false if host.nil?
|
166
|
+
|
167
|
+
begin
|
168
|
+
ip = IPAddr.new(host)
|
169
|
+
ip.private? || ip.loopback?
|
170
|
+
rescue IPAddr::InvalidAddressError
|
171
|
+
false
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
module RailsAi
|
6
|
+
module Security
|
7
|
+
class SecureFileHandler
|
8
|
+
def self.safe_file_read(path)
|
9
|
+
validate_path = InputValidator.validate_file_path(path)
|
10
|
+
|
11
|
+
File.open(validate_path, 'rb') do |file|
|
12
|
+
content = file.read(InputValidator::MAX_FILE_SIZE)
|
13
|
+
if file.eof?
|
14
|
+
content
|
15
|
+
else
|
16
|
+
raise ValidationError, "File too large"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.safe_temp_file(content, extension = '.tmp')
|
22
|
+
temp_file = Tempfile.new(['rails_ai', extension])
|
23
|
+
temp_file.binmode
|
24
|
+
temp_file.write(content)
|
25
|
+
temp_file.rewind
|
26
|
+
temp_file
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.safe_file_size(path)
|
30
|
+
File.size(InputValidator.validate_file_path(path))
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.safe_file_exists?(path)
|
34
|
+
return false if path.nil?
|
35
|
+
|
36
|
+
begin
|
37
|
+
InputValidator.validate_file_path(path)
|
38
|
+
true
|
39
|
+
rescue ValidationError
|
40
|
+
false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'timeout'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
module RailsAi
|
8
|
+
module Security
|
9
|
+
class SecureHTTPClient
|
10
|
+
DEFAULT_TIMEOUT = 30
|
11
|
+
MAX_REDIRECTS = 5
|
12
|
+
|
13
|
+
def self.make_request(url, options = {})
|
14
|
+
uri = URI.parse(url)
|
15
|
+
|
16
|
+
# Security validations
|
17
|
+
validate_url(uri)
|
18
|
+
|
19
|
+
http = create_secure_http_client(uri)
|
20
|
+
request = create_secure_request(uri, options)
|
21
|
+
|
22
|
+
# Set security headers
|
23
|
+
set_security_headers(request)
|
24
|
+
|
25
|
+
# Execute request with timeout
|
26
|
+
execute_with_timeout(http, request, options[:timeout] || DEFAULT_TIMEOUT)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.make_streaming_request(url, options = {}, &block)
|
30
|
+
uri = URI.parse(url)
|
31
|
+
|
32
|
+
# Security validations
|
33
|
+
validate_url(uri)
|
34
|
+
|
35
|
+
http = create_secure_http_client(uri)
|
36
|
+
request = create_secure_request(uri, options)
|
37
|
+
|
38
|
+
# Set security headers
|
39
|
+
set_security_headers(request)
|
40
|
+
|
41
|
+
# Execute streaming request
|
42
|
+
execute_streaming_request(http, request, options[:timeout] || DEFAULT_TIMEOUT, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def self.validate_url(uri)
|
48
|
+
unless %w[http https].include?(uri.scheme)
|
49
|
+
raise SecurityError, "Invalid URL scheme: #{uri.scheme}"
|
50
|
+
end
|
51
|
+
|
52
|
+
if blocked_host?(uri.host)
|
53
|
+
raise SecurityError, "URL blocked for security reasons: #{uri.host}"
|
54
|
+
end
|
55
|
+
|
56
|
+
if private_ip?(uri.host)
|
57
|
+
raise SecurityError, "Private IP addresses not allowed: #{uri.host}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.create_secure_http_client(uri)
|
62
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
63
|
+
http.use_ssl = (uri.scheme == 'https')
|
64
|
+
|
65
|
+
# Security settings
|
66
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
67
|
+
http.verify_depth = 5
|
68
|
+
|
69
|
+
# Timeout settings
|
70
|
+
http.read_timeout = DEFAULT_TIMEOUT
|
71
|
+
http.open_timeout = 10
|
72
|
+
http.ssl_timeout = 10
|
73
|
+
|
74
|
+
# Disable SSL compression
|
75
|
+
http.ssl_version = :TLSv1_2
|
76
|
+
|
77
|
+
http
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.create_secure_request(uri, options)
|
81
|
+
case options[:method]&.upcase || 'GET'
|
82
|
+
when 'GET'
|
83
|
+
Net::HTTP::Get.new(uri)
|
84
|
+
when 'POST'
|
85
|
+
request = Net::HTTP::Post.new(uri)
|
86
|
+
request.body = options[:body] if options[:body]
|
87
|
+
request
|
88
|
+
when 'PUT'
|
89
|
+
request = Net::HTTP::Put.new(uri)
|
90
|
+
request.body = options[:body] if options[:body]
|
91
|
+
request
|
92
|
+
else
|
93
|
+
raise SecurityError, "Unsupported HTTP method: #{options[:method]}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.set_security_headers(request)
|
98
|
+
request['User-Agent'] = 'RailsAI/1.0'
|
99
|
+
request['Accept'] = 'application/json'
|
100
|
+
request['Connection'] = 'close'
|
101
|
+
request['Cache-Control'] = 'no-cache'
|
102
|
+
|
103
|
+
# Security headers
|
104
|
+
request['X-Requested-With'] = 'RailsAI'
|
105
|
+
request['X-Content-Type-Options'] = 'nosniff'
|
106
|
+
request['X-Frame-Options'] = 'DENY'
|
107
|
+
request['X-XSS-Protection'] = '1; mode=block'
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.execute_with_timeout(http, request, timeout)
|
111
|
+
Timeout.timeout(timeout) do
|
112
|
+
response = http.request(request)
|
113
|
+
validate_response(response)
|
114
|
+
response
|
115
|
+
end
|
116
|
+
rescue Timeout::Error
|
117
|
+
raise SecurityError, "Request timeout after #{timeout} seconds"
|
118
|
+
rescue => e
|
119
|
+
raise SecurityError, "HTTP request failed: #{e.message}"
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.execute_streaming_request(http, request, timeout, &block)
|
123
|
+
Timeout.timeout(timeout) do
|
124
|
+
http.request(request) do |response|
|
125
|
+
validate_response(response)
|
126
|
+
|
127
|
+
response.read_body do |chunk|
|
128
|
+
block.call(chunk) if block
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
rescue Timeout::Error
|
133
|
+
raise SecurityError, "Streaming request timeout after #{timeout} seconds"
|
134
|
+
rescue => e
|
135
|
+
raise SecurityError, "Streaming request failed: #{e.message}"
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.validate_response(response)
|
139
|
+
case response.code.to_i
|
140
|
+
when 200..299
|
141
|
+
response
|
142
|
+
when 400..499
|
143
|
+
raise SecurityError, "Client error: #{response.code} #{response.message}"
|
144
|
+
when 500..599
|
145
|
+
raise SecurityError, "Server error: #{response.code} #{response.message}"
|
146
|
+
else
|
147
|
+
raise SecurityError, "Unexpected response: #{response.code} #{response.message}"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.blocked_host?(host)
|
152
|
+
return true if host.nil?
|
153
|
+
|
154
|
+
blocked_hosts = %w[
|
155
|
+
localhost 127.0.0.1 0.0.0.0 ::1
|
156
|
+
metadata.google.internal
|
157
|
+
instance-data.ec2.internal
|
158
|
+
169.254.169.254
|
159
|
+
]
|
160
|
+
|
161
|
+
blocked_hosts.any? { |blocked| host == blocked || host.end_with?(".#{blocked}") }
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.private_ip?(host)
|
165
|
+
return false if host.nil?
|
166
|
+
|
167
|
+
require 'ipaddr'
|
168
|
+
begin
|
169
|
+
ip = IPAddr.new(host)
|
170
|
+
ip.private? || ip.loopback? || ip.link_local?
|
171
|
+
rescue IPAddr::InvalidAddressError
|
172
|
+
false
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
Binary file
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsAi
|
4
|
+
class WindowContext
|
5
|
+
attr_reader :controller, :action, :params, :request, :session, :cookies
|
6
|
+
|
7
|
+
def initialize(controller: nil, action: nil, params: {}, request: nil, session: {}, cookies: {})
|
8
|
+
@controller = controller
|
9
|
+
@action = action
|
10
|
+
@params = params || {}
|
11
|
+
@request = request
|
12
|
+
@session = session || {}
|
13
|
+
@cookies = cookies || {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_h
|
17
|
+
{
|
18
|
+
controller: controller_name,
|
19
|
+
action: action_name,
|
20
|
+
route: route_info,
|
21
|
+
params: sanitized_params,
|
22
|
+
user_agent: user_agent,
|
23
|
+
referer: referer,
|
24
|
+
ip_address: ip_address,
|
25
|
+
session_data: sanitized_session,
|
26
|
+
cookies: sanitized_cookies,
|
27
|
+
timestamp: current_time.iso8601
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.from_controller(controller)
|
32
|
+
new(
|
33
|
+
controller: controller.class.name,
|
34
|
+
action: controller.action_name,
|
35
|
+
params: extract_params(controller),
|
36
|
+
request: controller.request,
|
37
|
+
session: controller.session.to_h,
|
38
|
+
cookies: controller.cookies.to_h
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def current_time
|
45
|
+
if defined?(Time.current)
|
46
|
+
Time.current
|
47
|
+
else
|
48
|
+
Time.now
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.extract_params(controller)
|
53
|
+
if controller.params.respond_to?(:to_unsafe_h)
|
54
|
+
controller.params.to_unsafe_h
|
55
|
+
elsif controller.params.respond_to?(:to_h)
|
56
|
+
controller.params.to_h
|
57
|
+
else
|
58
|
+
controller.params
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def controller_name
|
63
|
+
controller&.class&.name || 'Unknown'
|
64
|
+
end
|
65
|
+
|
66
|
+
def action_name
|
67
|
+
action || 'Unknown'
|
68
|
+
end
|
69
|
+
|
70
|
+
def route_info
|
71
|
+
return 'Unknown' unless request
|
72
|
+
|
73
|
+
"#{request.method} #{request.path}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def sanitized_params
|
77
|
+
# Remove sensitive parameters
|
78
|
+
params.except('password', 'password_confirmation', 'token', 'secret', 'key')
|
79
|
+
end
|
80
|
+
|
81
|
+
def user_agent
|
82
|
+
request&.user_agent || 'Unknown'
|
83
|
+
end
|
84
|
+
|
85
|
+
def referer
|
86
|
+
request&.referer || 'Direct'
|
87
|
+
end
|
88
|
+
|
89
|
+
def ip_address
|
90
|
+
request&.remote_ip || 'Unknown'
|
91
|
+
end
|
92
|
+
|
93
|
+
def sanitized_session
|
94
|
+
# Remove sensitive session data
|
95
|
+
session.except('password', 'token', 'secret', 'key', 'csrf_token')
|
96
|
+
end
|
97
|
+
|
98
|
+
def sanitized_cookies
|
99
|
+
# Remove sensitive cookies
|
100
|
+
cookies.except('_session_id', 'csrf_token', 'remember_token')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|