Skip to content

Commit 8b5ec69

Browse files
committed
Document integration stages and optimize spoof checks
1 parent 2c8c7a9 commit 8b5ec69

File tree

5 files changed

+46
-25
lines changed

5 files changed

+46
-25
lines changed

INTEGRATIONS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
**High‑Level Flow**
66

77
- Add a typed log structure under `lib/log_struct/log/` (so the docs generator picks it up).
8-
- Add a configuration toggle in `ConfigStruct::Integrations` and wire it into `Integrations.setup_integrations`.
8+
- Add a configuration toggle in `ConfigStruct::Integrations` and wire it into `Integrations.setup_integrations` via the appropriate stage (`:non_middleware` for instrumentation hooks, `:middleware` when the integration inserts Rack middleware).
99
- Implement the integration under `lib/log_struct/integrations/…` to produce that log type.
1010
- Add the dev dependency for the third‑party gem and generate RBIs with Tapioca.
1111
- Add tests (unit + behavior) under `test/log_struct/integrations` and (if needed) `test/log_struct/log`.
@@ -74,7 +74,7 @@
7474
- `prop :enable_<name>, T::Boolean, default: true`
7575
- In `lib/log_struct/integrations.rb`:
7676
- `require_relative "integrations/<name>"`
77-
- Call `Integrations::<Name>.setup(config)` inside `setup_integrations` behind the toggle.
77+
- Call `Integrations::<Name>.setup(config)` inside `setup_integrations`, selecting the stage that matches your integration (`:non_middleware` for subscribers/hooks, `:middleware` for Rack additions). Most integrations belong in the default `:non_middleware` stage; only code that mutates the middleware stack should go in `:middleware`.
7878

7979
4. Implement the integration
8080
- File: `lib/log_struct/integrations/<name>.rb`

docs/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,20 @@ This is the documentation website for the LogStruct gem, built with Next.js and
1414
### Prerequisites
1515

1616
- Node.js (version 18 or higher)
17-
- npm (version 8 or higher)
17+
- pnpm (version 9 or higher)
1818

1919
### Getting Started
2020

2121
1. Install dependencies:
2222

2323
```bash
24-
npm install
24+
pnpm install
2525
```
2626

2727
2. Start the development server:
2828

2929
```bash
30-
npm run dev
30+
pnpm dev
3131
```
3232

3333
3. Open [http://localhost:3001](http://localhost:3001) to view the docs in your browser.
@@ -37,7 +37,7 @@ npm run dev
3737
To build the static docs for production:
3838

3939
```bash
40-
npm run build
40+
pnpm build
4141
```
4242

4343
This will generate static HTML files in the `out` directory that can be deployed to any static web hosting service.
@@ -51,7 +51,7 @@ To manually deploy:
5151
1. Build the docs:
5252

5353
```bash
54-
npm run build
54+
pnpm build
5555
```
5656

5757
2. The static files will be in the `out` directory, which can be deployed to any static hosting service.

docs/biome.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"files": {
99
"ignoreUnknown": false,
1010
"includes": [
11-
"**/*.{js,ts,jsx,tsx}",
12-
"*.{js,ts,jsx,tsx}",
11+
"**/*.{js,ts,jsx,tsx,json}",
12+
"*.{js,ts,jsx,tsx,json}",
1313
"!generated/**/*.ts",
1414
"!scripts/**/*.js",
1515
"!public/coverage/**/*"

docs/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
"format": "biome format --write .",
1414
"test": "jest",
1515
"typecheck": "tsc -p tsconfig.json --noEmit",
16-
"check": "npm run typecheck && npm run lint && npm run build && npm run test:integration && npm run e2e",
17-
"test:integration": "node scripts/prepare-integration-fixtures.js && npm run build && jest -c jest.integration.config.ts",
16+
"check": "pnpm run typecheck && pnpm run lint && pnpm run build && pnpm run test:integration && pnpm run e2e",
17+
"test:integration": "node scripts/prepare-integration-fixtures.js && pnpm run build && jest -c jest.integration.config.ts",
1818
"e2e": "playwright test",
1919
"test:watch": "jest --watch",
2020
"generate-favicons": "node scripts/generate-favicons.js"

lib/log_struct/integrations/rack_error_handler/middleware.rb

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ def call(env)
8383
[FORBIDDEN_STATUS, IP_SPOOF_HEADERS.dup, [IP_SPOOF_HTML]]
8484
rescue ::ActionController::InvalidAuthenticityToken => invalid_auth_token_error
8585
# Create a security log for CSRF error
86-
request = ::ActionDispatch::Request.new(env)
8786
security_log = Log::Security::CSRFViolation.new(
8887
path: request.path,
8988
http_method: request.method,
@@ -97,15 +96,15 @@ def call(env)
9796
LogStruct.error(security_log)
9897

9998
# Report to error reporting service and/or re-raise
100-
context = extract_request_context(env)
99+
context = extract_request_context(env, request)
101100
LogStruct.handle_exception(invalid_auth_token_error, source: Source::Security, context: context)
102101

103102
# If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
104103
# If we are only logging or reporting these security errors, then return a default response
105104
[FORBIDDEN_STATUS, CSRF_HEADERS.dup, [CSRF_HTML]]
106105
rescue => error
107106
# Extract request context for error reporting
108-
context = extract_request_context(env)
107+
context = extract_request_context(env, request)
109108

110109
# Create and log a structured exception with request context
111110
exception_log = Log.from_exception(Source::Rails, error, context)
@@ -122,23 +121,19 @@ def call(env)
122121
sig { params(request: ::ActionDispatch::Request).void }
123122
def perform_remote_ip_check!(request)
124123
action_dispatch_config = ::Rails.application.config.action_dispatch
124+
check_ip = action_dispatch_config.ip_spoofing_check
125+
return unless check_ip
125126

126-
remote_ip_middleware = ::ActionDispatch::RemoteIp.new(
127-
->(_env) { [200, {}, []] },
128-
action_dispatch_config.ip_spoofing_check,
129-
action_dispatch_config.trusted_proxies
130-
)
131-
132-
return unless remote_ip_middleware.check_ip
127+
proxies = normalized_trusted_proxies(action_dispatch_config.trusted_proxies)
133128

134129
::ActionDispatch::RemoteIp::GetIp
135-
.new(request, remote_ip_middleware.check_ip, remote_ip_middleware.proxies)
130+
.new(request, check_ip, proxies)
136131
.to_s
137132
end
138133

139-
sig { params(env: T::Hash[String, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
140-
def extract_request_context(env)
141-
request = ::ActionDispatch::Request.new(env)
134+
sig { params(env: T::Hash[String, T.untyped], request: T.nilable(::ActionDispatch::Request)).returns(T::Hash[Symbol, T.untyped]) }
135+
def extract_request_context(env, request = nil)
136+
request ||= ::ActionDispatch::Request.new(env)
142137
{
143138
request_id: request.request_id,
144139
path: request.path,
@@ -150,6 +145,32 @@ def extract_request_context(env)
150145
# If we can't extract request context, return minimal info
151146
{error_extracting_context: error.message}
152147
end
148+
149+
sig { params(configured_proxies: T.untyped).returns(T.untyped) }
150+
def normalized_trusted_proxies(configured_proxies)
151+
if configured_proxies.nil? || (configured_proxies.respond_to?(:empty?) && configured_proxies.empty?)
152+
return ::ActionDispatch::RemoteIp::TRUSTED_PROXIES
153+
end
154+
155+
return configured_proxies if configured_proxies.respond_to?(:any?)
156+
157+
raise(
158+
ArgumentError,
159+
<<~EOM
160+
Setting config.action_dispatch.trusted_proxies to a single value isn't
161+
supported. Please set this to an enumerable instead. For
162+
example, instead of:
163+
164+
config.action_dispatch.trusted_proxies = IPAddr.new("10.0.0.0/8")
165+
166+
Wrap the value in an Array:
167+
168+
config.action_dispatch.trusted_proxies = [IPAddr.new("10.0.0.0/8")]
169+
170+
Note that passing an enumerable will *replace* the default set of trusted proxies.
171+
EOM
172+
)
173+
end
153174
end
154175
end
155176
end

0 commit comments

Comments
 (0)