philiprehberger-header_kit 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +54 -0
- data/lib/philiprehberger/header_kit/cors.rb +29 -0
- data/lib/philiprehberger/header_kit/forwarded.rb +47 -0
- data/lib/philiprehberger/header_kit/security.rb +27 -0
- data/lib/philiprehberger/header_kit/version.rb +1 -1
- data/lib/philiprehberger/header_kit.rb +43 -0
- metadata +9 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b1e00c5cf603e6f7db3b7ac63478c587d4d3f0dd54d40ee11cbcc730a9ea4bb9
|
|
4
|
+
data.tar.gz: c58a1210d23ae79c1ac86cd6340c7b6f30c6f6f575eb82b9c33caff1c5306eb3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dbc951e5c1cee317be92464e8c4639cbb65f963f87f61b2050b12ed1c75b2be29d7cd326514ce35472a36bcc53d61d8cc445581cc3e85ff24954345dd3100439
|
|
7
|
+
data.tar.gz: eda78dd329146c43b11f44594c9fdc9fa5acf31352a77af7cc1e610f4ff80a6d21f4feb52119efc36a51058ba40aad15b89f0635cff908e7c39098ac3c5dfe4a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-03-31
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `parse_cors` and `build_cors` for CORS header handling
|
|
15
|
+
- `security_headers` for generating recommended security headers
|
|
16
|
+
- `parse_forwarded` for RFC 7239 Forwarded header parsing
|
|
17
|
+
- `parse_via` for Via header parsing
|
|
18
|
+
|
|
10
19
|
## [0.2.0] - 2026-03-28
|
|
11
20
|
|
|
12
21
|
### Added
|
data/README.md
CHANGED
|
@@ -137,6 +137,55 @@ Philiprehberger::HeaderKit.negotiate("text/html;q=0.9, application/json", ["text
|
|
|
137
137
|
# => "application/json"
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
### Parse CORS
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
headers = {
|
|
144
|
+
'Origin' => 'https://example.com',
|
|
145
|
+
'Access-Control-Request-Method' => 'POST',
|
|
146
|
+
'Access-Control-Request-Headers' => 'Content-Type, Authorization'
|
|
147
|
+
}
|
|
148
|
+
Philiprehberger::HeaderKit.parse_cors(headers)
|
|
149
|
+
# => {origin: "https://example.com", method: "POST", headers: ["Content-Type", "Authorization"]}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Build CORS
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
Philiprehberger::HeaderKit.build_cors(
|
|
156
|
+
origin: "https://example.com",
|
|
157
|
+
methods: ["GET", "POST"],
|
|
158
|
+
headers: ["Content-Type"],
|
|
159
|
+
max_age: 3600,
|
|
160
|
+
credentials: true
|
|
161
|
+
)
|
|
162
|
+
# => {"Access-Control-Allow-Origin" => "https://example.com", ...}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Security Headers
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
Philiprehberger::HeaderKit.security_headers
|
|
169
|
+
# => {"X-Content-Type-Options" => "nosniff", "X-Frame-Options" => "DENY", ...}
|
|
170
|
+
|
|
171
|
+
Philiprehberger::HeaderKit.security_headers(hsts: "max-age=31536000", csp: "default-src 'self'")
|
|
172
|
+
# => includes Strict-Transport-Security and Content-Security-Policy
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Parse Forwarded
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
Philiprehberger::HeaderKit.parse_forwarded('for=192.0.2.60;proto=http;by=203.0.113.43')
|
|
179
|
+
# => [{for: "192.0.2.60", proto: "http", by: "203.0.113.43"}]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Parse Via
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
Philiprehberger::HeaderKit.parse_via('1.1 vegur, 1.0 fred')
|
|
186
|
+
# => [{protocol: "1.1", host: "vegur"}, {protocol: "1.0", host: "fred"}]
|
|
187
|
+
```
|
|
188
|
+
|
|
140
189
|
## API
|
|
141
190
|
|
|
142
191
|
| Method | Description |
|
|
@@ -155,6 +204,11 @@ Philiprehberger::HeaderKit.negotiate("text/html;q=0.9, application/json", ["text
|
|
|
155
204
|
| `HeaderKit.parse_link(header)` | Parse Link header into entry array |
|
|
156
205
|
| `HeaderKit.build_link(links)` | Build Link header from array of hashes |
|
|
157
206
|
| `HeaderKit.negotiate(accept_header, available)` | Content negotiation, returns best match or nil |
|
|
207
|
+
| `HeaderKit.parse_cors(headers)` | Parse CORS-related request headers |
|
|
208
|
+
| `HeaderKit.build_cors(**options)` | Build CORS response headers |
|
|
209
|
+
| `HeaderKit.security_headers(**options)` | Generate recommended security headers |
|
|
210
|
+
| `HeaderKit.parse_forwarded(header)` | Parse RFC 7239 Forwarded header |
|
|
211
|
+
| `HeaderKit.parse_via(header)` | Parse Via header into structured entries |
|
|
158
212
|
|
|
159
213
|
## Development
|
|
160
214
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module HeaderKit
|
|
5
|
+
module Cors
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def parse(headers)
|
|
9
|
+
result = {}
|
|
10
|
+
result[:origin] = headers['Origin'] || headers['origin']
|
|
11
|
+
result[:method] = headers['Access-Control-Request-Method'] || headers['access-control-request-method']
|
|
12
|
+
request_headers = headers['Access-Control-Request-Headers'] || headers['access-control-request-headers']
|
|
13
|
+
result[:headers] = request_headers&.split(',')&.map(&:strip) || []
|
|
14
|
+
result
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build(origin:, methods: ['GET'], headers: [], max_age: nil, credentials: false, expose: [])
|
|
18
|
+
result = {}
|
|
19
|
+
result['Access-Control-Allow-Origin'] = origin
|
|
20
|
+
result['Access-Control-Allow-Methods'] = Array(methods).join(', ')
|
|
21
|
+
result['Access-Control-Allow-Headers'] = Array(headers).join(', ') unless headers.empty?
|
|
22
|
+
result['Access-Control-Max-Age'] = max_age.to_s if max_age
|
|
23
|
+
result['Access-Control-Allow-Credentials'] = 'true' if credentials
|
|
24
|
+
result['Access-Control-Expose-Headers'] = Array(expose).join(', ') unless expose.empty?
|
|
25
|
+
result
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module HeaderKit
|
|
5
|
+
module Forwarded
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def parse(header)
|
|
9
|
+
return [] if header.nil? || header.empty?
|
|
10
|
+
|
|
11
|
+
header.split(',').map do |part|
|
|
12
|
+
entry = {}
|
|
13
|
+
part.strip.split(';').each do |pair|
|
|
14
|
+
key, value = pair.strip.split('=', 2)
|
|
15
|
+
next unless key && value
|
|
16
|
+
|
|
17
|
+
entry[key.strip.downcase.to_sym] = unquote(value.strip)
|
|
18
|
+
end
|
|
19
|
+
entry
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parse_via(header)
|
|
24
|
+
return [] if header.nil? || header.empty?
|
|
25
|
+
|
|
26
|
+
header.split(',').map do |entry|
|
|
27
|
+
parts = entry.strip.split(/\s+/, 3)
|
|
28
|
+
result = {}
|
|
29
|
+
if parts.length >= 2
|
|
30
|
+
result[:protocol] = parts[0]
|
|
31
|
+
result[:host] = parts[1]
|
|
32
|
+
result[:comment] = parts[2] if parts[2]
|
|
33
|
+
else
|
|
34
|
+
result[:host] = parts[0]
|
|
35
|
+
end
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def unquote(value)
|
|
41
|
+
value.start_with?('"') && value.end_with?('"') ? value[1..-2] : value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private_class_method :unquote
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module HeaderKit
|
|
5
|
+
module Security
|
|
6
|
+
DEFAULTS = {
|
|
7
|
+
content_type_options: 'nosniff',
|
|
8
|
+
frame_options: 'DENY',
|
|
9
|
+
xss_protection: '0',
|
|
10
|
+
referrer_policy: 'strict-origin-when-cross-origin'
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def headers(hsts: nil, csp: nil, frame: nil, content_type_options: nil, referrer_policy: nil)
|
|
16
|
+
result = {}
|
|
17
|
+
result['X-Content-Type-Options'] = content_type_options || DEFAULTS[:content_type_options]
|
|
18
|
+
result['X-Frame-Options'] = frame || DEFAULTS[:frame_options]
|
|
19
|
+
result['X-XSS-Protection'] = DEFAULTS[:xss_protection]
|
|
20
|
+
result['Referrer-Policy'] = referrer_policy || DEFAULTS[:referrer_policy]
|
|
21
|
+
result['Strict-Transport-Security'] = hsts if hsts
|
|
22
|
+
result['Content-Security-Policy'] = csp if csp
|
|
23
|
+
result
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -11,6 +11,9 @@ require_relative 'header_kit/content_type'
|
|
|
11
11
|
require_relative 'header_kit/cookie'
|
|
12
12
|
require_relative 'header_kit/link'
|
|
13
13
|
require_relative 'header_kit/negotiation'
|
|
14
|
+
require_relative 'header_kit/cors'
|
|
15
|
+
require_relative 'header_kit/security'
|
|
16
|
+
require_relative 'header_kit/forwarded'
|
|
14
17
|
|
|
15
18
|
module Philiprehberger
|
|
16
19
|
module HeaderKit
|
|
@@ -131,5 +134,45 @@ module Philiprehberger
|
|
|
131
134
|
def self.negotiate(accept_header, available)
|
|
132
135
|
Negotiation.negotiate(accept_header, available)
|
|
133
136
|
end
|
|
137
|
+
|
|
138
|
+
# Parse CORS-related headers from a request.
|
|
139
|
+
#
|
|
140
|
+
# @param headers [Hash] request headers hash
|
|
141
|
+
# @return [Hash] with :origin, :method, :headers keys
|
|
142
|
+
def self.parse_cors(headers)
|
|
143
|
+
Cors.parse(headers)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Build CORS response headers.
|
|
147
|
+
#
|
|
148
|
+
# @param options [Hash] CORS options (origin:, methods:, headers:, max_age:, credentials:, expose:)
|
|
149
|
+
# @return [Hash{String => String}] response headers
|
|
150
|
+
def self.build_cors(**options)
|
|
151
|
+
Cors.build(**options)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Generate recommended security response headers.
|
|
155
|
+
#
|
|
156
|
+
# @param options [Hash] overrides (hsts:, csp:, frame:, content_type_options:, referrer_policy:)
|
|
157
|
+
# @return [Hash{String => String}] security headers
|
|
158
|
+
def self.security_headers(**options)
|
|
159
|
+
Security.headers(**options)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Parse an RFC 7239 Forwarded header.
|
|
163
|
+
#
|
|
164
|
+
# @param header [String] the Forwarded header value
|
|
165
|
+
# @return [Array<Hash>] parsed entries with symbol keys
|
|
166
|
+
def self.parse_forwarded(header)
|
|
167
|
+
Forwarded.parse(header)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Parse a Via header into structured entries.
|
|
171
|
+
#
|
|
172
|
+
# @param header [String] the Via header value
|
|
173
|
+
# @return [Array<Hash>] entries with :protocol, :host, :comment keys
|
|
174
|
+
def self.parse_via(header)
|
|
175
|
+
Forwarded.parse_via(header)
|
|
176
|
+
end
|
|
134
177
|
end
|
|
135
178
|
end
|
metadata
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-header_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Parse and build Accept, Accept-Language, Accept-Encoding, Authorization,
|
|
14
|
-
Cache-Control, Content-Type, Cookie, and
|
|
15
|
-
|
|
14
|
+
Cache-Control, Content-Type, Cookie, Link, CORS, Forwarded, and Via HTTP headers.
|
|
15
|
+
Includes content negotiation and security header generation.
|
|
16
16
|
email:
|
|
17
17
|
- me@philiprehberger.com
|
|
18
18
|
executables: []
|
|
@@ -31,14 +31,17 @@ files:
|
|
|
31
31
|
- lib/philiprehberger/header_kit/cache_control.rb
|
|
32
32
|
- lib/philiprehberger/header_kit/content_type.rb
|
|
33
33
|
- lib/philiprehberger/header_kit/cookie.rb
|
|
34
|
+
- lib/philiprehberger/header_kit/cors.rb
|
|
35
|
+
- lib/philiprehberger/header_kit/forwarded.rb
|
|
34
36
|
- lib/philiprehberger/header_kit/link.rb
|
|
35
37
|
- lib/philiprehberger/header_kit/negotiation.rb
|
|
38
|
+
- lib/philiprehberger/header_kit/security.rb
|
|
36
39
|
- lib/philiprehberger/header_kit/version.rb
|
|
37
|
-
homepage: https://
|
|
40
|
+
homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-header_kit
|
|
38
41
|
licenses:
|
|
39
42
|
- MIT
|
|
40
43
|
metadata:
|
|
41
|
-
homepage_uri: https://
|
|
44
|
+
homepage_uri: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-header_kit
|
|
42
45
|
source_code_uri: https://github.com/philiprehberger/rb-header-kit
|
|
43
46
|
changelog_uri: https://github.com/philiprehberger/rb-header-kit/blob/main/CHANGELOG.md
|
|
44
47
|
bug_tracker_uri: https://github.com/philiprehberger/rb-header-kit/issues
|