From 861d6a7cc2fd65b95e21a74e6ab43911061ee87a Mon Sep 17 00:00:00 2001 From: Tomohiko Mimura Date: Sun, 29 Sep 2024 23:12:16 +0900 Subject: [PATCH] v1.0.0 --- .github/workflows/main.yml | 20 +++++-- .gitignore | 7 +++ .rubocop.yml | 19 ++++++- CHANGELOG.md | 2 +- Gemfile | 20 +++++-- README.md | 54 +++++++++++++++---- actionpack-cloudfront_viewer_address.gemspec | 28 +++++----- lib/action_pack/cloudfront_viewer_address.rb | 11 ++++ .../cloudfront_viewer_address/railtie.rb | 13 +++++ .../cloudfront_viewer_address/remote_ip.rb | 27 ++++++++++ .../cloudfront_viewer_address/version.rb | 4 +- lib/actionpack/cloudfront_viewer_address.rb | 9 +--- sig/actionpack/cloudfront_viewer_address.rbs | 6 --- .../remote_ip_spec.rb | 33 ++++++++++++ .../cloudfront_viewer_address_spec.rb | 7 +++ .../cloudfront_viewer_address_spec.rb | 11 ---- .../app/controllers/remote_ip_controller.rb | 7 +++ spec/dummy/config.ru | 6 +++ spec/dummy/config/application.rb | 14 +++++ spec/dummy/config/boot.rb | 5 ++ spec/dummy/config/environment.rb | 5 ++ spec/dummy/config/routes.rb | 5 ++ spec/dummy/log/.keep | 0 spec/dummy/tmp/.keep | 0 spec/rails_integration_spec.rb | 11 ++++ spec/spec_helper.rb | 12 +++-- 26 files changed, 272 insertions(+), 64 deletions(-) create mode 100644 lib/action_pack/cloudfront_viewer_address.rb create mode 100644 lib/action_pack/cloudfront_viewer_address/railtie.rb create mode 100644 lib/action_pack/cloudfront_viewer_address/remote_ip.rb rename lib/{actionpack => action_pack}/cloudfront_viewer_address/version.rb (64%) delete mode 100644 sig/actionpack/cloudfront_viewer_address.rbs create mode 100644 spec/action_pack/cloudfront_viewer_address/remote_ip_spec.rb create mode 100644 spec/action_pack/cloudfront_viewer_address_spec.rb delete mode 100644 spec/actionpack/cloudfront_viewer_address_spec.rb create mode 100644 spec/dummy/app/controllers/remote_ip_controller.rb create mode 100644 spec/dummy/config.ru create mode 100644 spec/dummy/config/application.rb create mode 100644 spec/dummy/config/boot.rb create mode 100644 spec/dummy/config/environment.rb create mode 100644 spec/dummy/config/routes.rb create mode 100644 spec/dummy/log/.keep create mode 100644 spec/dummy/tmp/.keep create mode 100644 spec/rails_integration_spec.rb diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 17d4e87..c11e83f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,17 +4,29 @@ on: push: branches: - main - pull_request: + schedule: + - cron: '0 4 * * *' jobs: build: runs-on: ubuntu-latest - name: Ruby ${{ matrix.ruby }} + name: Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} strategy: matrix: ruby: - - '3.3.4' + - '3.3' + - '3.2' + - '3.1' + rails: + - '7.2' + - '7.1' + - '7.0' + include: + - ruby: ruby-head + rails: edge + env: + RAILS_VERSION: ${{ matrix.rails }} steps: - uses: actions/checkout@v4 @@ -23,5 +35,7 @@ jobs: with: ruby-version: ${{ matrix.ruby }} bundler-cache: true + continue-on-error: ${{ (matrix.ruby == 'ruby-head')}} - name: Run the default task run: bundle exec rake + continue-on-error: ${{ (matrix.ruby == 'ruby-head')}} diff --git a/.gitignore b/.gitignore index b04a8c8..7d2fc92 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,12 @@ /spec/reports/ /tmp/ +/Gemfile.lock + +/spec/dummy/log/* +/spec/dummy/tmp/* +!/spec/dummy/log/.keep +!/spec/dummy/tmp/.keep + # rspec failure tracking .rspec_status diff --git a/.rubocop.yml b/.rubocop.yml index 762eebb..5aed568 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,22 @@ +require: + - rubocop-performance + - rubocop-rails + - rubocop-rake + - rubocop-rspec + - rubocop-rspec_rails + AllCops: - TargetRubyVersion: 3.0 + TargetRubyVersion: 3.1 + NewCops: enable + +Layout/LineLength: + Enabled: false + +RSpec/MultipleExpectations: + Enabled: false + +Style/Documentation: + Enabled: false Style/StringLiterals: EnforcedStyle: double_quotes diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f48510..82a44ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## [Unreleased] -## [0.1.0] - 2024-09-29 +## [1.0.0] - 2024-09-29 - Initial release diff --git a/Gemfile b/Gemfile index d766d0b..0b06cf0 100644 --- a/Gemfile +++ b/Gemfile @@ -2,11 +2,23 @@ source "https://rubygems.org" -# Specify your gem's dependencies in actionpack-cloudfront_viewer_address.gemspec gemspec -gem "rake", "~> 13.0" +if ENV["RAILS_VERSION"] == "edge" + gem "rails", github: "rails/rails", branch: "main" +elsif ENV["RAILS_VERSION"] + gem "rails", "~> #{ENV["RAILS_VERSION"]}.0" +else + gem "rails" +end -gem "rspec", "~> 3.0" +gem "rake" -gem "rubocop", "~> 1.21" +gem "rspec", require: false +gem "rspec-rails", require: false +gem "rubocop", require: false +gem "rubocop-performance", require: false +gem "rubocop-rails", require: false +gem "rubocop-rake", require: false +gem "rubocop-rspec", require: false +gem "rubocop-rspec_rails", require: false diff --git a/README.md b/README.md index b43cec6..dcd7b17 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,60 @@ -# Actionpack::CloudfrontViewerAddress +# ActionPack::CloudfrontViewerAddress -TODO: Delete this and the text below, and describe your gem +Calculate the remote IP ( `request.remote_ip` ) using `CloudFront-Viewer-Address`. -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/actionpack/cloudfront_viewer_address`. To experiment with that code, run `bin/console` for an interactive prompt. +> [!CAUTION] +> Be sure to configure security groups and other settings so that HTTP requests go through Amazon CloudFront. +> Otherwise, the `CloudFront-Viewer-Address` will be freely modified and IP spoofing. -## Installation +## Why do you need this Gem? -TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org. +If you use 'Amazon CloudFront' as the Proxy for Rails App, the remote IP ( `request.remote_ip` ) will be set to the CloudFront IP. +To work around this problem, it was necessary to calculate the remote IP address using a combination of `X-Forwarded-For` and the 'IP address of a trusted Proxy'. +Like these Gem... + +* https://github.com/dinks/cloudfront-rails +* https://github.com/customink/actionpack-cloudfront + +However, CloudFront provides a more concise solution to this problem with `CloudFront-Viewer-Address`. +This Gem is intended to make this functionality more concise for use in Rails applications. + +## Usage + +### Step1. Amazon CloudFront Setup + +Before using this Gem, modify your Amazon CloudFront configuration to enable `CloudFront-Viewer-Address`. +Please refer to the following for details on how to set up. + +* https://aws.amazon.com/about-aws/whats-new/2021/10/amazon-cloudfront-client-ip-address-connection-port-header/ +* https://dev.classmethod.jp/articles/amazon-cloudfront-client-ip-address-connection-port-header/ (written in Japanese) + +### Step2. This `ActionPack::CloudfrontViewerAddress` Gem Install Install the gem and add to the application's Gemfile by executing: - $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG + $ bundle add actionpack-cloudfront_viewer_address If bundler is not being used to manage dependencies, install the gem by executing: - $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG + $ gem install actionpack-cloudfront_viewer_address -## Usage +### Step3. Rack middleware + +**If you are using Rails, omit this as it will be set automatically.** +Otherwise, set the middleware as `middleware.insert_after ActionDispatch::RemoteIp, ActionPack::CloudfrontViewerAddress::RemoteIp`. + +### Step4. Use `request.remote_ip` + +Otherwise, no special processing is required. +The client's IP address can be obtained by referring to `request.remote_ip` as usual. + +## About IP spoofing + +This Gem only references the `CloudFront-Viewer-Address` request header. +The `CloudFront-Viewer-Address` header is not improved if the request is made to a Rails application via Amazon CloudFront. +However, if the request is made to a Rails application without going through Amazon CloudFront, the `CloudFront-Viewer-Address` can be freely rewritten and there is a risk of IP spoofing. -TODO: Write usage instructions here +**Please keep this in mind.** ## Development diff --git a/actionpack-cloudfront_viewer_address.gemspec b/actionpack-cloudfront_viewer_address.gemspec index 3182639..c48c026 100644 --- a/actionpack-cloudfront_viewer_address.gemspec +++ b/actionpack-cloudfront_viewer_address.gemspec @@ -1,27 +1,27 @@ # frozen_string_literal: true -require_relative "lib/actionpack/cloudfront_viewer_address/version" +require_relative "lib/action_pack/cloudfront_viewer_address/version" Gem::Specification.new do |spec| spec.name = "actionpack-cloudfront_viewer_address" - spec.version = Actionpack::CloudfrontViewerAddress::VERSION + spec.version = ActionPack::CloudfrontViewerAddress::VERSION spec.authors = ["Tomohiko Mimura"] spec.email = ["mito.5525@gmail.com"] - spec.summary = "TODO: Write a short summary, because RubyGems requires one." - spec.description = "TODO: Write a longer description or delete this line." - spec.homepage = "TODO: Put your gem's website or public repo URL here." + spec.summary = "Calculate RemoteIp based on `CloudFront-Viewer-Address` Header" + spec.description = "Utilize `CloudFront-Viewer-Address`, a custom CloudFront header, to calculate RemoteIp more simply" + spec.homepage = "https://github.com/tmimura39/actionpack-cloudfront_viewer_address" spec.license = "MIT" - spec.required_ruby_version = ">= 3.0.0" + spec.required_ruby_version = ">= 3.1.0" - spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'" + spec.metadata["allowed_push_host"] = "https://rubygems.org" spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." - spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." + spec.metadata["source_code_uri"] = spec.homepage + spec.metadata["changelog_uri"] = "#{spec.homepage}/tree/main/CHANGELOG.md" + + spec.metadata["rubygems_mfa_required"] = "true" - # Specify which files should be added to the gem when it is released. - # The `git ls-files -z` loads the files in the RubyGem that have been added into git. gemspec = File.basename(__FILE__) spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| ls.readlines("\x0", chomp: true).reject do |f| @@ -33,9 +33,5 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - # Uncomment to register a new dependency of your gem - # spec.add_dependency "example-gem", "~> 1.0" - - # For more information and examples about making a new gem, check out our - # guide at: https://bundler.io/guides/creating_gem.html + spec.add_dependency "actionpack" end diff --git a/lib/action_pack/cloudfront_viewer_address.rb b/lib/action_pack/cloudfront_viewer_address.rb new file mode 100644 index 0000000..58f28a6 --- /dev/null +++ b/lib/action_pack/cloudfront_viewer_address.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require_relative "cloudfront_viewer_address/version" +require_relative "cloudfront_viewer_address/remote_ip" + +require_relative "cloudfront_viewer_address/railtie" if defined?(Rails::Railtie) + +module ActionPack + module CloudfrontViewerAddress + end +end diff --git a/lib/action_pack/cloudfront_viewer_address/railtie.rb b/lib/action_pack/cloudfront_viewer_address/railtie.rb new file mode 100644 index 0000000..ded5ec1 --- /dev/null +++ b/lib/action_pack/cloudfront_viewer_address/railtie.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "remote_ip" + +module ActionPack + module CloudfrontViewerAddress + class Railtie < ::Rails::Railtie + initializer "actionpack_cloudfront_viewer_address.configure_rails_initialization" do |app| + app.config.middleware.insert_after ActionDispatch::RemoteIp, ActionPack::CloudfrontViewerAddress::RemoteIp + end + end + end +end diff --git a/lib/action_pack/cloudfront_viewer_address/remote_ip.rb b/lib/action_pack/cloudfront_viewer_address/remote_ip.rb new file mode 100644 index 0000000..3af1993 --- /dev/null +++ b/lib/action_pack/cloudfront_viewer_address/remote_ip.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "action_dispatch" + +module ActionPack + module CloudfrontViewerAddress + class RemoteIp + def initialize(app) + @app = app + end + + def call(env) + req = ::ActionDispatch::Request.new(env) + if env["HTTP_CLOUDFRONT_VIEWER_ADDRESS"].present? + # IPv4 "HTTP_CLOUDFRONT_VIEWER_ADDRESS" = "1.1.1.1:3000" + # IPV6 "HTTP_CLOUDFRONT_VIEWER_ADDRESS" = "0000:0000:0000:0000:0000:0000:0000:0000:3000" + req.remote_ip = req.env["HTTP_CLOUDFRONT_VIEWER_ADDRESS"].sub(/:\d+\z/, "") # remove `port(:0000)` part + end + app.call(req.env) + end + + private + + attr_reader :app + end + end +end diff --git a/lib/actionpack/cloudfront_viewer_address/version.rb b/lib/action_pack/cloudfront_viewer_address/version.rb similarity index 64% rename from lib/actionpack/cloudfront_viewer_address/version.rb rename to lib/action_pack/cloudfront_viewer_address/version.rb index 22d0d91..e984aa4 100644 --- a/lib/actionpack/cloudfront_viewer_address/version.rb +++ b/lib/action_pack/cloudfront_viewer_address/version.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -module Actionpack +module ActionPack module CloudfrontViewerAddress - VERSION = "0.1.0" + VERSION = "1.0.0" end end diff --git a/lib/actionpack/cloudfront_viewer_address.rb b/lib/actionpack/cloudfront_viewer_address.rb index a28422a..f632d68 100644 --- a/lib/actionpack/cloudfront_viewer_address.rb +++ b/lib/actionpack/cloudfront_viewer_address.rb @@ -1,10 +1,3 @@ # frozen_string_literal: true -require_relative "cloudfront_viewer_address/version" - -module Actionpack - module CloudfrontViewerAddress - class Error < StandardError; end - # Your code goes here... - end -end +require "action_pack/cloudfront_viewer_address" diff --git a/sig/actionpack/cloudfront_viewer_address.rbs b/sig/actionpack/cloudfront_viewer_address.rbs deleted file mode 100644 index 51dd73d..0000000 --- a/sig/actionpack/cloudfront_viewer_address.rbs +++ /dev/null @@ -1,6 +0,0 @@ -module Actionpack - module CloudfrontViewerAddress - VERSION: String - # See the writing guide of rbs: https://github.com/ruby/rbs#guides - end -end diff --git a/spec/action_pack/cloudfront_viewer_address/remote_ip_spec.rb b/spec/action_pack/cloudfront_viewer_address/remote_ip_spec.rb new file mode 100644 index 0000000..ea444cc --- /dev/null +++ b/spec/action_pack/cloudfront_viewer_address/remote_ip_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.describe ActionPack::CloudfrontViewerAddress::RemoteIp do + context "when CloudFront-Viewer-Address(IPv4 Address + Port) specified" do + it "action_dispatch.remote_ip = IPv4 Address" do + remote_ip = nil + described_class + .new(->(env) { remote_ip = env["action_dispatch.remote_ip"] }) + .call({ "action_dispatch.remote_ip" => "0.0.0.0", "HTTP_CLOUDFRONT_VIEWER_ADDRESS" => "1.1.1.1:3000" }) + expect(remote_ip).to eq "1.1.1.1" + end + end + + context "when CloudFront-Viewer-Address(IPv6 Address + Port) specified" do + it "action_dispatch.remote_ip = IPv6 Address" do + remote_ip = nil + described_class + .new(->(env) { remote_ip = env["action_dispatch.remote_ip"] }) + .call({ "action_dispatch.remote_ip" => "0.0.0.0", "HTTP_CLOUDFRONT_VIEWER_ADDRESS" => "1111:1111:1111:1111:1111:1111:1111:1111:3000" }) + expect(remote_ip).to eq "1111:1111:1111:1111:1111:1111:1111:1111" + end + end + + context "when CloudFront-Viewer-Address unspecified" do + it "action_dispatch.remote_ip is unchanged" do + remote_ip = nil + described_class + .new(->(env) { remote_ip = env["action_dispatch.remote_ip"] }) + .call({ "action_dispatch.remote_ip" => "0.0.0.0" }) + expect(remote_ip).to eq "0.0.0.0" + end + end +end diff --git a/spec/action_pack/cloudfront_viewer_address_spec.rb b/spec/action_pack/cloudfront_viewer_address_spec.rb new file mode 100644 index 0000000..15c2986 --- /dev/null +++ b/spec/action_pack/cloudfront_viewer_address_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.describe ActionPack::CloudfrontViewerAddress do + it "has a version number" do + expect(ActionPack::CloudfrontViewerAddress::VERSION).not_to be_nil + end +end diff --git a/spec/actionpack/cloudfront_viewer_address_spec.rb b/spec/actionpack/cloudfront_viewer_address_spec.rb deleted file mode 100644 index af01e2b..0000000 --- a/spec/actionpack/cloudfront_viewer_address_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Actionpack::CloudfrontViewerAddress do - it "has a version number" do - expect(Actionpack::CloudfrontViewerAddress::VERSION).not_to be nil - end - - it "does something useful" do - expect(false).to eq(true) - end -end diff --git a/spec/dummy/app/controllers/remote_ip_controller.rb b/spec/dummy/app/controllers/remote_ip_controller.rb new file mode 100644 index 0000000..68fc83c --- /dev/null +++ b/spec/dummy/app/controllers/remote_ip_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class RemoteIpController < ActionController::API + def show + render plain: request.remote_ip + end +end diff --git a/spec/dummy/config.ru b/spec/dummy/config.ru new file mode 100644 index 0000000..2797095 --- /dev/null +++ b/spec/dummy/config.ru @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb new file mode 100644 index 0000000..f72ce72 --- /dev/null +++ b/spec/dummy/config/application.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require_relative "boot" + +require "rails" +require "action_controller/railtie" + +require "actionpack/cloudfront_viewer_address" + +module Dummy + class Application < Rails::Application + config.eager_load = true + end +end diff --git a/spec/dummy/config/boot.rb b/spec/dummy/config/boot.rb new file mode 100644 index 0000000..7865da2 --- /dev/null +++ b/spec/dummy/config/boot.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__) + +require "bundler/setup" diff --git a/spec/dummy/config/environment.rb b/spec/dummy/config/environment.rb new file mode 100644 index 0000000..e8173e0 --- /dev/null +++ b/spec/dummy/config/environment.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require_relative "application" + +Rails.application.initialize! diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb new file mode 100644 index 0000000..10175dc --- /dev/null +++ b/spec/dummy/config/routes.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Rails.application.routes.draw do + get :remote_ip, to: "remote_ip#show" +end diff --git a/spec/dummy/log/.keep b/spec/dummy/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/dummy/tmp/.keep b/spec/dummy/tmp/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/rails_integration_spec.rb b/spec/rails_integration_spec.rb new file mode 100644 index 0000000..a0044ff --- /dev/null +++ b/spec/rails_integration_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +RSpec.describe "Rails Integration", type: :request do + it "Rack Middleware auto inserted" do + get "/remote_ip", headers: { "CloudFront-Viewer-Address" => "1.1.1.1:3000" } + expect(response.body).to eq "1.1.1.1" + + middlewares = app.middleware.to_a + expect(middlewares.index(ActionDispatch::RemoteIp) - middlewares.index(ActionPack::CloudfrontViewerAddress::RemoteIp)).to eq(-1) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1e559f8..b342ecf 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,15 +1,21 @@ # frozen_string_literal: true -require "actionpack/cloudfront_viewer_address" +ENV["RAILS_ENV"] ||= "test" +require File.expand_path("../spec/dummy/config/environment.rb", __dir__) +ENV["RAILS_ROOT"] ||= "#{File.dirname(__FILE__)}/../../../spec/dummy" +require "rspec/rails" RSpec.configure do |config| - # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" - # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! config.expect_with :rspec do |c| c.syntax = :expect end + + config.filter_run_when_matching :focus + config.warnings = true + config.order = :random + Kernel.srand config.seed end