From 4ce801b17940f0c791646335000a42a7d2e3e13e Mon Sep 17 00:00:00 2001 From: Jeremy Woertink Date: Thu, 26 Mar 2020 14:07:46 -0700 Subject: [PATCH] Adding a new remote_ip method (#1059) * Adding a new remote_ip method to get the ip of the client with some fallbacks. Fixes #1000 * moving the logic for the remote_address from Lucky::Action in to a handler so the value is accessible to anything that has access to the context * using the X-Forwarded-For instead of the HTTP_X_FORWARDED_FOR. It seems the HTTP_ was residule from the old CGI days and should no longer be used. --- spec/lucky/remote_ip_handler_spec.cr | 49 ++++++++++++++++++++++++++++ src/lucky/remote_ip_handler.cr | 24 ++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 spec/lucky/remote_ip_handler_spec.cr create mode 100644 src/lucky/remote_ip_handler.cr diff --git a/spec/lucky/remote_ip_handler_spec.cr b/spec/lucky/remote_ip_handler_spec.cr new file mode 100644 index 000000000..37db78209 --- /dev/null +++ b/spec/lucky/remote_ip_handler_spec.cr @@ -0,0 +1,49 @@ +require "../spec_helper" + +include ContextHelper + +describe Lucky::RemoteIpHandler do + describe "getting the remote_address" do + it "returns nil when no remote IP is found" do + context = build_context(path: "/path") + + run_remote_ip_handler(context) + context.request.remote_address.should eq nil + end + + it "returns the X_FORWARDED_FOR address" do + headers = HTTP::Headers.new + headers["X_FORWARDED_FOR"] = "1.2.3.4,127.0.0.1" + request = HTTP::Request.new("GET", "/remote-ip", body: "", headers: headers) + context = build_context(request) + + run_remote_ip_handler(context) + context.request.remote_address.should eq "1.2.3.4" + end + + it "returns nil if the X_FORWARDED_FOR is an empty string, and no default remote_address is found" do + headers = HTTP::Headers.new + headers["X_FORWARDED_FOR"] = "" + request = HTTP::Request.new("GET", "/remote-ip", body: "", headers: headers) + context = build_context(request) + + run_remote_ip_handler(context) + context.request.remote_address.should eq nil + end + + it "returns the original remote_address" do + request = HTTP::Request.new("GET", "/remote-ip", body: "", headers: HTTP::Headers.new) + request.remote_address = "255.255.255.255" + context = build_context(request) + + run_remote_ip_handler(context) + context.request.remote_address.should eq "255.255.255.255" + end + end +end + +private def run_remote_ip_handler(context) + handler = Lucky::RemoteIpHandler.new + handler.next = ->(_ctx : HTTP::Server::Context) {} + handler.call(context) +end diff --git a/src/lucky/remote_ip_handler.cr b/src/lucky/remote_ip_handler.cr new file mode 100644 index 000000000..f5bfe2aca --- /dev/null +++ b/src/lucky/remote_ip_handler.cr @@ -0,0 +1,24 @@ +# Sets the HTTP::Request#remote_address value +# to the value of the first IP in the `X-Forwarded-For` +# header, or fallback to the default `remote_address`. +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For +# +# This Handler does a best guess for the IP which is generally good +# enough. If you require IP based Authentication, then you may want +# to handle this on your own as there will be edge cases when related +# to mobile clients on the go, and potential IP spoofing attacks. +class Lucky::RemoteIpHandler + include HTTP::Handler + + def call(context) + context.request.remote_address = fetch_remote_ip(context) + call_next(context) + end + + private def fetch_remote_ip(context : HTTP::Server::Context) : String? + request = context.request + + x_forwarded = request.headers["X_FORWARDED_FOR"]?.try(&.split(',').first?) + x_forwarded.blank? ? request.remote_address : x_forwarded + end +end