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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d4bcbb3c1002def0f437335a19871b535ce7764369d43fed1c4a1548367988f
4
- data.tar.gz: 03dea17ed3566ea8cce9ea8511f741c92487113b6ba0a4cdb899ab1835f07009
3
+ metadata.gz: b1e00c5cf603e6f7db3b7ac63478c587d4d3f0dd54d40ee11cbcc730a9ea4bb9
4
+ data.tar.gz: c58a1210d23ae79c1ac86cd6340c7b6f30c6f6f575eb82b9c33caff1c5306eb3
5
5
  SHA512:
6
- metadata.gz: 35c43a782a9e1ba431f972d18ec2c248a6141e96f79c082d09a2a48f973aee449d684011e4b70e3f95b4c488ff71fb1537c8da716e0186669e9ca974ff267792
7
- data.tar.gz: 1b82e5bdea29824445c2097f21009f57a0f71cebbd80799386aed785e2247de8a4c87bd65136e198fea1150485c6909264f21cfed3970a2365c47c61ca7b7ce7
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module HeaderKit
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  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.2.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-29 00:00:00.000000000 Z
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 Link HTTP headers. Includes content negotiation
15
- for selecting the best response type or language.
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://github.com/philiprehberger/rb-header-kit
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://github.com/philiprehberger/rb-header-kit
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