Skip to content

Commit 4a76538

Browse files
committed
.
1 parent 2f24136 commit 4a76538

File tree

7 files changed

+67
-68
lines changed

7 files changed

+67
-68
lines changed

app/api/v1/feeds.rb

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# frozen_string_literal: true
22

3+
require 'time'
4+
35
require_relative '../../auth'
46
require_relative '../../auto_source'
57
require_relative '../../feeds'
6-
require_relative '../../xml_builder'
78
require_relative '../../exceptions'
89
require_relative '../../feed_token'
910

@@ -21,7 +22,7 @@ def index(_request) # rubocop:disable Metrics/MethodLength
2122
id: feed[:name],
2223
name: feed[:name],
2324
description: feed[:description],
24-
url: "/api/v1/feeds/#{feed[:name]}",
25+
public_url: "/#{feed[:name]}",
2526
created_at: nil,
2627
updated_at: nil
2728
}
@@ -35,6 +36,9 @@ def show(request, token)
3536
end
3637

3738
def create(request)
39+
raise ForbiddenError, 'Auto source feature is disabled' unless AutoSource.enabled?
40+
raise ForbiddenError, 'Request origin not allowed' unless AutoSource.allowed_origin?(request)
41+
3842
account = authenticate_request(request)
3943
params = extract_create_params(request)
4044
validate_create_params(params, account)
@@ -45,43 +49,10 @@ def create(request)
4549
build_create_response(request, feed_data)
4650
end
4751

48-
def json_request?(request)
49-
accept_header = request.env['HTTP_ACCEPT'].to_s
50-
accept_header.include?('application/json') && !accept_header.include?('application/xml')
51-
end
52-
53-
def show_feed_metadata(feed_id) # rubocop:disable Metrics/MethodLength
54-
config = LocalConfig.find(feed_id)
55-
raise NotFoundError, 'Feed not found' unless config
56-
57-
{
58-
success: true,
59-
data: {
60-
feed: {
61-
id: feed_id,
62-
name: feed_id,
63-
description: "RSS feed for #{feed_id}",
64-
url: "/api/v1/feeds/#{feed_id}",
65-
strategy: config[:strategy] || 'ssrf_filter',
66-
created_at: nil,
67-
updated_at: nil
68-
}
69-
}
70-
}
71-
end
72-
73-
def generate_feed_content(request, feed_id)
74-
rss_content = Html2rss::Web::Feeds.generate_feed(feed_id, request.params)
75-
config = LocalConfig.find(feed_id)
76-
ttl = config&.dig(:channel, :ttl) || 3600
77-
78-
request.response['Content-Type'] = 'application/xml'
79-
request.response['Cache-Control'] = "public, max-age=#{ttl}"
80-
81-
rss_content.to_s
82-
end
83-
8452
def handle_token_based_feed(request, token)
53+
raise ForbiddenError, 'Auto source feature is disabled' unless AutoSource.enabled?
54+
raise ForbiddenError, 'Request origin not allowed' unless AutoSource.allowed_origin?(request)
55+
8556
feed_token = validate_feed_token(token)
8657
account = get_account_for_token(feed_token)
8758
validate_account_access(account, feed_token.url)

app/api/v1/health.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require 'time'
4+
35
require_relative '../../auth'
46
require_relative '../../health_check'
57
require_relative '../../exceptions'

app/auto_source.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def allowed_origins
4444
else
4545
origins = ENV.fetch('AUTO_SOURCE_ALLOWED_ORIGINS', '')
4646
end
47-
origins.split(',').map(&:strip)
47+
origins.split(',').map(&:strip).reject(&:empty?)
4848
end
4949

5050
# @param token_data [Hash]

app/security_logger.rb

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'logger'
44
require 'json'
55
require 'digest'
6+
require 'time'
67

78
module Html2rss
89
module Web
@@ -45,7 +46,7 @@ def log_auth_failure(ip, user_agent, reason)
4546
ip: ip,
4647
user_agent: user_agent,
4748
reason: reason
48-
})
49+
}, severity: :warn)
4950
end
5051

5152
##
@@ -56,7 +57,7 @@ def log_auth_success(username, ip)
5657
log_event('auth_success', {
5758
username: username,
5859
ip: ip
59-
})
60+
}, severity: :info)
6061
end
6162

6263
##
@@ -69,7 +70,7 @@ def log_rate_limit_exceeded(ip, endpoint, limit)
6970
ip: ip,
7071
endpoint: endpoint,
7172
limit: limit
72-
})
73+
}, severity: :warn)
7374
end
7475

7576
##
@@ -78,11 +79,13 @@ def log_rate_limit_exceeded(ip, endpoint, limit)
7879
# @param url [String] URL being accessed
7980
# @param success [Boolean] whether the token was valid
8081
def log_token_usage(feed_token, url, success)
82+
severity = success ? :info : :warn
83+
8184
log_event('token_usage', {
8285
success: success,
8386
url: url,
8487
token_hash: Digest::SHA256.hexdigest(feed_token)[0..7]
85-
})
88+
}, severity: severity)
8689
end
8790

8891
##
@@ -95,7 +98,7 @@ def log_suspicious_activity(ip, activity, details = {})
9598
ip: ip,
9699
activity: activity,
97100
**details
98-
})
101+
}, severity: :warn)
99102
end
100103

101104
##
@@ -108,7 +111,7 @@ def log_blocked_request(ip, reason, endpoint)
108111
ip: ip,
109112
reason: reason,
110113
endpoint: endpoint
111-
})
114+
}, severity: :warn)
112115
end
113116

114117
##
@@ -119,18 +122,20 @@ def log_config_validation_failure(component, details)
119122
log_event('config_validation_failure', {
120123
component: component,
121124
details: details
122-
})
125+
}, severity: :error)
123126
end
124127

125128
##
126129
# Log a security event
127130
# @param event_type [String] type of security event
128131
# @param data [Hash] event data
129-
def log_event(event_type, data)
130-
logger.warn({
132+
def log_event(event_type, data, severity: :warn)
133+
payload = {
131134
security_event: event_type,
132135
**data
133-
}.to_json)
136+
}.to_json
137+
138+
logger.public_send(severity, payload)
134139
rescue StandardError => error
135140
handle_logging_error(error, event_type, data)
136141
end
@@ -141,11 +146,8 @@ def log_event(event_type, data)
141146
# @param event_type [String] type of security event
142147
# @param data [Hash] event data
143148
def handle_logging_error(error, event_type, data)
144-
logger.error("Security logging error: #{error.message}")
145-
logger.warn("Security event: #{event_type} - #{data}")
146-
rescue StandardError
147-
# If even the fallback fails, just print to stderr
148-
warn("Security logging failed: #{error.message}")
149+
Kernel.warn("Security logging error: #{error.message}")
150+
Kernel.warn("Security event: #{event_type} - #{data}")
149151
end
150152
end
151153
end

app/xml_builder.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require 'nokogiri'
44
require 'sanitize'
5+
require 'time'
56

67
module Html2rss
78
module Web

spec/html2rss/web/api/v1_spec.rb

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
RSpec.describe 'api/v1' do # rubocop:disable RSpec/DescribeClass
88
include Rack::Test::Methods
99

10-
def app = described_class
10+
def app = Html2rss::Web::App.freeze.app
11+
12+
before do
13+
allow(Html2rss::Web::AutoSource).to receive_messages(enabled?: true, allowed_origin?: true)
14+
end
1115

1216
describe 'GET /api/v1' do
1317
it 'returns API information', :aggregate_failures do
@@ -43,6 +47,7 @@ def app = described_class
4347
response_data = JSON.parse(last_response.body)
4448
expect(response_data['success']).to be true
4549
expect(response_data['data']).to have_key('feeds')
50+
expect(response_data['data']['feeds'].first).to have_key('public_url')
4651
expect(response_data['meta']).to have_key('total')
4752
end
4853
end
@@ -107,6 +112,17 @@ def app = described_class
107112
expect(last_response.body).to include('Invalid token')
108113
end
109114
end
115+
116+
context 'when auto source disabled' do
117+
it 'returns 403 forbidden', :aggregate_failures do
118+
allow(Html2rss::Web::AutoSource).to receive(:enabled?).and_return(false)
119+
120+
get '/api/v1/feeds/some-token'
121+
122+
expect(last_response.status).to eq(403)
123+
expect(last_response.body).to include('Auto source feature is disabled')
124+
end
125+
end
110126
end
111127

112128
describe 'POST /api/v1/feeds' do
@@ -123,6 +139,19 @@ def app = described_class
123139
expect(response_data['error']['code']).to eq('UNAUTHORIZED')
124140
end
125141
end
142+
143+
context 'when auto source origin not allowed' do
144+
it 'returns 403 forbidden', :aggregate_failures do
145+
allow(Html2rss::Web::AutoSource).to receive(:allowed_origin?).and_return(false)
146+
header 'HTTP_HOST', 'unauthorized.example'
147+
148+
post '/api/v1/feeds', { url: 'https://example.com' }.to_json,
149+
'CONTENT_TYPE' => 'application/json'
150+
151+
expect(last_response.status).to eq(403)
152+
expect(last_response.body).to include('Request origin not allowed')
153+
end
154+
end
126155
end
127156

128157
describe 'GET /api/v1/health/ready' do

spec/html2rss/web/security_logger_spec.rb

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
require_relative '../../../app/security_logger'
55

66
RSpec.describe Html2rss::Web::SecurityLogger do
7-
let(:test_output) { StringIO.new }
87
let(:mock_logger) { instance_double(Logger) }
98

109
before do
1110
allow(Logger).to receive(:new).with($stdout).and_return(mock_logger)
1211
allow(mock_logger).to receive(:formatter=)
12+
allow(mock_logger).to receive(:info)
1313
allow(mock_logger).to receive(:warn)
1414
allow(mock_logger).to receive(:error)
15+
allow(Kernel).to receive(:warn)
1516
described_class.reset_logger!
1617
end
1718

@@ -51,7 +52,7 @@
5152
it 'logs token usage with basic data' do
5253
described_class.log_token_usage('test-token-123', 'https://example.com', true)
5354

54-
expect(mock_logger).to have_received(:warn) do |message|
55+
expect(mock_logger).to have_received(:info) do |message|
5556
data = JSON.parse(message, symbolize_names: true)
5657
data.include?(
5758
security_event: 'token_usage',
@@ -64,7 +65,7 @@
6465
it 'includes hashed token in log data' do
6566
described_class.log_token_usage('test-token-123', 'https://example.com', true)
6667

67-
expect(mock_logger).to have_received(:warn) do |message|
68+
expect(mock_logger).to have_received(:info) do |message|
6869
data = JSON.parse(message, symbolize_names: true)
6970
data[:token_hash].match?(/\A[a-f0-9]{8}\z/)
7071
end
@@ -107,7 +108,7 @@
107108
it 'logs configuration validation failure' do
108109
described_class.log_config_validation_failure('secret_key', 'Invalid secret key')
109110

110-
expect(mock_logger).to have_received(:warn) do |message|
111+
expect(mock_logger).to have_received(:error) do |message|
111112
data = JSON.parse(message, symbolize_names: true)
112113
data.include?(
113114
security_event: 'config_validation_failure',
@@ -122,32 +123,25 @@
122123
it 'does not raise error when logger fails' do
123124
# Mock the logger to raise an error when warn is called
124125
allow(mock_logger).to receive(:warn).and_raise(StandardError, 'Logger error')
125-
allow(mock_logger).to receive(:error)
126126

127127
# Should not raise an error
128128
expect { described_class.log_auth_failure('192.168.1.1', 'Mozilla/5.0', 'invalid_token') }.not_to raise_error
129129
end
130130

131131
it 'logs error when logger fails' do
132-
# Mock the logger to raise an error when warn is called
133132
allow(mock_logger).to receive(:warn).and_raise(StandardError, 'Logger error')
134-
allow(mock_logger).to receive(:error)
135133

136134
described_class.log_auth_failure('192.168.1.1', 'Mozilla/5.0', 'invalid_token')
137135

138-
expect(mock_logger).to have_received(:error).with('Security logging error: Logger error')
136+
expect(Kernel).to have_received(:warn).with('Security logging error: Logger error')
139137
end
140138

141139
it 'logs fallback message when logger fails' do
142-
# Mock the logger to raise an error when warn is called
143140
allow(mock_logger).to receive(:warn).and_raise(StandardError, 'Logger error')
144-
allow(mock_logger).to receive(:error)
145141

146142
described_class.log_auth_failure('192.168.1.1', 'Mozilla/5.0', 'invalid_token')
147143

148-
expected_message = 'Security event: auth_failure - {ip: "192.168.1.1", ' \
149-
'user_agent: "Mozilla/5.0", reason: "invalid_token"}'
150-
expect(mock_logger).to have_received(:warn).with(expected_message)
144+
expect(Kernel).to have_received(:warn).with(a_string_including('Security event: auth_failure'))
151145
end
152146
end
153147
end

0 commit comments

Comments
 (0)