diff --git a/spec/std/http/request_spec.cr b/spec/std/http/request_spec.cr index 6c169b7e3b3e..4fea587b55cc 100644 --- a/spec/std/http/request_spec.cr +++ b/spec/std/http/request_spec.cr @@ -1,5 +1,6 @@ require "spec" require "http/request" +require "socket" module HTTP describe Request do @@ -203,5 +204,69 @@ module HTTP io.to_s.should eq("GET /api/v3/some/resource?q=isearchforsomething&locale=de HTTP/1.1\r\n\r\n") end end + + describe "#peer_addr=" do + it "sets the peer_addr" do + request = Request.from_io(StringIO.new("GET / HTTP/1.1\r\nHost: host.example.org\r\n\r\n")).not_nil! + addr = Socket::Addr.new("AF_INET", "12345", "127.0.0.1") + request.peer_addr = addr + request.peer_addr.should eq addr + end + end + + describe "#peer_addr" do + it "returns the peer_addr" do + request = Request.from_io(StringIO.new("GET / HTTP/1.1\r\nHost: host.example.org\r\n\r\n")).not_nil! + addr = Socket::Addr.new("AF_INET", "12345", "127.0.0.1") + request.peer_addr = addr + request.peer_addr.should eq addr + end + + it "raises if peer_addr is nil" do + request = Request.from_io(StringIO.new("GET / HTTP/1.1\r\nHost: host.example.org\r\n\r\n")).not_nil! + + expect_raises do + request.peer_addr + end + end + end + + describe "#remote_ip" do + context "trusting headers" do + it "returns the remote ip from the Client-Ip" do + request = Request.from_io(StringIO.new("GET / HTTP/1.1\r\nHost: host.example.org\r\nClient-Ip: 8.8.8.8\r\n\r\n")).not_nil! + addr = Socket::Addr.new("AF_INET", "12345", "127.0.0.1") + request.peer_addr = addr + + request.remote_ip.should eq "8.8.8.8" + end + + it "returns the remote ip from the X-Forwarded-For header" do + request = Request.from_io(StringIO.new("GET / HTTP/1.1\r\nHost: host.example.org\r\nX-Forwarded-For: 4.4.4.4, 10.0.0.1\r\n\r\n")).not_nil! + addr = Socket::Addr.new("AF_INET", "12345", "127.0.0.1") + request.peer_addr = addr + + request.remote_ip.should eq "4.4.4.4" + end + + it "returns the peer addr if headers are not set" do + request = Request.from_io(StringIO.new("GET / HTTP/1.1\r\nHost: host.example.org\r\n\r\n")).not_nil! + addr = Socket::Addr.new("AF_INET", "12345", "127.0.0.1") + request.peer_addr = addr + + request.remote_ip.should eq "127.0.0.1" + end + end + + context "without trusting headers" do + it "returns the peer_addr ip address" do + request = Request.from_io(StringIO.new("GET / HTTP/1.1\r\nHost: host.example.org\r\n\r\n")).not_nil! + addr = Socket::Addr.new("AF_INET", "12345", "127.0.0.1") + request.peer_addr = addr + + request.remote_ip(false).should eq "127.0.0.1" + end + end + end end end diff --git a/src/http/request.cr b/src/http/request.cr index ec1b8d15f135..41acc5223f7b 100644 --- a/src/http/request.cr +++ b/src/http/request.cr @@ -6,6 +6,7 @@ class HTTP::Request getter headers getter body getter version + property! peer_addr def initialize(@method : String, @resource, @headers = Headers.new : Headers, @body = nil, @version = "HTTP/1.1") if body = @body @@ -33,6 +34,19 @@ class HTTP::Request @method == "HEAD" end + def remote_ip(trust_headers=true) + if trust_headers + client_ips = ips_from("Client-Ip").reverse + forwarded_ips = ips_from("X-Forwarded-For").reverse + + ips = [forwarded_ips, client_ips, peer_addr.ip_address].flatten.compact + + first_remote_ip_address(ips) || peer_addr.ip_address + else + peer_addr.ip_address + end + end + def to_io(io) io << @method << " " << resource << " " << @version << "\r\n" cookies = @cookies @@ -68,4 +82,21 @@ class HTTP::Request private def uri (@uri ||= URI.parse(@resource)).not_nil! end + + private def ips_from(header) + if ips = headers[header]? || headers["Http-#{header}"]? + ips.strip.split(/[,\s]+/) + else + [] of String + end + end + + private def first_remote_ip_address(ip_addresses) + ip_addresses.find { |ip| !trusted_proxy?(ip) } + end + + private def trusted_proxy?(ip) + ip =~ /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i + end + end diff --git a/src/http/server/server.cr b/src/http/server/server.cr index 77903cec1121..0f44a8581aee 100644 --- a/src/http/server/server.cr +++ b/src/http/server/server.cr @@ -139,6 +139,8 @@ class HTTP::Server return end break unless request + request.peer_addr = sock.peeraddr + response = @handler.call(request) response.headers["Connection"] = "keep-alive" if request.keep_alive? response.to_io io