In Rails, what's the best way to get the ip address of the client connecting to the server?
Here are two ways I've found:
request.remote_ip
request.env['HTTP_X_REAL_IP']
I would just use the request.remote_ip that's simple and it works. Any reason you need another method?
See: Get real IP address in local Rails development environment
for some other things you can do with client server ip's.
request.remote_ip is an interpretation of all the available IP address information and it will make a best-guess. If you access the variables directly you assume responsibility for testing them in the correct precedence order. Proxies introduce a number of headers that create environment variables with different names.
Get client ip using command:
request.remote_ip
I found request.env['HTTP_X_FORWARDED_FOR'] very useful too in cases when request.remote_ip returns 127.0.0.1
For anyone interested and using a newer rails and the Devise gem: Devise's "trackable" option includes a column for current/last_sign_in_ip in the users table.
From Mozilla docs:
When a client connects directly to a server, the client's IP address
is sent to the server (and is often written to server access logs).
But if a client connection passes through any forward or reverse
proxies, the server only sees the final proxy's IP address, which is
often of little use. That's especially true if the final proxy is a
load balancer which is part of the same installation as the server.
So, to provide a more-useful client IP address to the server, the
X-Forwarded-For request header is used.
remote_ip will actually check a handful of headers passed in. This includes both REMOTE_ADDR and X-Forwarded-For. X-Forwarded-For is used in case of a proxy. But REMOTE_ADDR is checked first. You can verify this in a rspec test:
describe CustomersController, type: :controller do
describe "GET search" do
it "returns a 200" do
whitelisted_ip = '73.1.193.230' # a comcast ip address
request.headers['REMOTE_ADDR'] = whitelisted_ip
request.headers['X-Forwarded-For'] = whitelisted_ip
get :index
expect(response).to have_http_status(:ok)
end
end
end
Inside the controller:
before_action :verify_source_ip, only: [:index]
def verify_source_ip
head(403) unless request.remote_ip == '73.1.193.230'
end
Related
I find that when I run Rails 5 behind a reverse proxy, the default behaviour of url_for is use the host name of the upstream server, not the proxy itself.
For example, with nginx running at front.com and proxying requests to my upstream Rails server, which it knows as back, root_url returns http://back/.
If I tell nginx to pass the Host header as it proxies, root_url is http://front.com.
So far, so good. But I find using default_url_options in routes.rb (example below) does not have the power to override this. Why not? And if default_url_options can't override the Hostname from the request, what's the point of it?
Thanks!
EDIT
default_url_options example:
RoutingApp.routes.draw do
default_url_options host: 'example.com'
root to: 'pages#index'
end
(from this gist by a Rails core developer)
There are many places to set this, you set it for it to work under console, but not controller.
http://lulalala.logdown.com/posts/5835445-rails-many-default-url-options
I think you want it to work for controller/web view. Try this:
# production.rb
config.action_controller.default_url_options= {host: 'example.com'}
I currently have a single machine, running a stack that includes nginx/Passenger/Rails 3.x. This machine has several IP addresses, one for each TLD we're serving, primarily for SSL certificate purposes.
I'm having an issue with longer TLDs (.co.uk, .com.au, and their ilk.) My first attempt was the following filter in my ApplicationController:
if request.host =~ /co\.uk$|com\.au$/
ActionDispatch::Http::URL.tld_length = 2
else
ActionDispatch::Http::URL.tld_length = 1
end
Unfortunately, this runs after the routes have already been parsed, which means my subdomain handling in the config/routes.rb is already finished (and wrong). But, rather dangerously, I'm permanently affecting this passenger instance for the NEXT request. So it's breaking not only on this request for .com.au, but probably the next request for .com as well.
I have a feeling that what I should be doing is setting tld_length in my environment file, but I'd need to do it based on the IP address of the interface the user has connected to (since I have a different IP address for each TLD).
Is there an easy way to do this? If not, is there a better way to handle multiple TLDs on the same machine?
Looks like middleware is the way to go. Here's the solution that ended up working for me.
# config/application.rb
config.middleware.use TldHandler
And:
# app/lib/tld_handler.rb
class TldHandler
def initialize(app)
#app = app
end
def call(env)
TldHandler.enable_long_tlds(env['SERVER_NAME'])
status, headers, response = #app.call(env)
end
def self.enable_long_tlds(hostname)
if hostname =~ /co\.uk$|com\.au$/
ActionDispatch::Http::URL.tld_length = 2
else
ActionDispatch::Http::URL.tld_length = 1
end
end
end
You could continue to expand the regex as desired.
We are currently doing local IP address detection (using ifconfig, tracepath etc.) in Rails to reach a charting application hosted on the same machine as Rails. The 'self IP address' is detected during Rails start-up and is done only once. Assuming that a user can reach the Apache/Rails server using both hostname and IP address, can I safely use request.host (after user has logged into the application) to create a URL in an <img> tag (which essentially is the image returned by the charting application) in both IPv4 and IPv6 environments? Currently it works for IPv4 addresses as shown below:
class ApplicationController < ActionController::Base
CHARTS_BASE = ''
def set_chart_url
chart_url = MYAPP_CONFIG["chart_base"]
chart_url.gsub!("$chart_host", request.host)
CHARTS_BASE.replace(chart_url + 'charts?')
end
class ReportController < ApplicationController
before_filter :set_chart_url
View:
<div>
<img src="<%=CHARTS_BASE + #opt_graph-%>" alt="Graph"/>
</div>
Will the same work for IPv6? What does request.host look like in an IPv6 environment when connecting to Rails using hostname or IPv6 address in the URL?
I thought anyone already running Rails in an IPv6 environment could validate the above. Thanks.
The IPv6 address is given, in brackets.
I just generated a simple hello world controller and added to its view:
<%= request.host %>
I then started up webrick with rails server -b :: and then visited http://[2001:db8:38c3:1189:c8d3:210f:f297:ba13]:3000/ in my browser. The output was:
Home#index
Find me in app/views/home/index.html.erb
[2001:db8:38c3:1189:c8d3:210f:f297:ba13]
Note that request.host contains the content of the HTTP Host header. So you might see a hostname here, or an IPv4 address. In any case, you should be able to use it as-is to construct a URL.
I'm at a point in my web application where it is useful to see ip addresses resolved down to location (for example with MaxMind) and DNS name via reverse lookup of the IPs. I'm running unicorn on Heroku and other servers. Is there a prebuilt method of doing this or do I need to write some kind of a filter to pass my logs through in order to provide this additional information?
You can use something like http://www.rubygeocoder.com/ to determine the location of the user based of their IP address and then use that within your application. An exact example of what it sounds like you're trying to do is on their homepage
request.ip # => "81.137.210.82"
request.location.city # => "Erith"
request.location.country # => "United Kingdom"
In Rails 3, both the geo_ip and the geo_location gems return accurate results on my local machine, but once uploaded to Heroku, persistently returns "Seattle, WA" (I'm located in Pennsylvania).
I did some digging, and found that the Heroku shared database I'm using is located in Seattle. Can anyone point me in the right direction for how to handle this situation? Again, while running locally the geolocation is working as intended.
Thanks!!
If you are using hostname-based SSL on Heroku, there is currently no way to get the request's original IP. See this thread:
http://groups.google.com/group/heroku/browse_thread/thread/8cd2cba55f9aeb19
On that thread, someone mentioned http://jsonip.com/, which does what you'd expect:
ultramarine:~ jdc$ curl http://jsonip.com/
{"ip":"131.247.152.2"}
My plan is to make an Ajax request to jsonip, then pass the IP back to the server and geolocate it using geoip or geokit. I tried it with geoip thusly:
ruby-1.9.2-p136 :004 > c = GeoIP.new('GeoIP.dat').country('131.247.152.2')
=> ["131.247.152.2", "131.247.152.2", 225, "US", "USA", "United States", "NA"]
(It seems like geokit will be easier to deal with because it doesn't require me to manage a .dat file of IP mappings. I expect to end up using that.)
I don't know how heroku works, but you might be behind a load balancer. You'll need to set your TRUSTED_PROXIES to get request.remote_ip to be the HTTP_X_FORWARDED_FOR address.
You can check to see if this is your problem by adding an action to one of your controllers like this:
def remote_ip
render :text => "REMOTE_ADDR: %s<br/>remote_ip: %s<br/>HTTP_X_FORWARDED_FOR: %s" %
[ request.env['REMOTE_ADDR'],
request.remote_ip,
request.env['HTTP_X_FORWARDED_FOR'] ]
end
If you've got an HTTP_X_FORWARDED_FOR, then you need to tell Rails about trusted proxies. Once you do that, your request.remote_ip and your HTTP_X_FORWARDED_FOR ips will be the same.
In your production.rb, add these lines, where the allowed_ips regex includes your load balancer IPs. Replace the a.b.c. with the load balancer IPs you get from heroku.
# Setup Trusted Proxies
allowed_ips = /^a\.b\.c\.|^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
ActionController::Request.const_set("TRUSTED_PROXIES", allowed_ips)
Interesting. Being a Rails noob, the only thing I can ask is where is it getting the IP from? Is there a way to make sure it is getting it from the user? (Complete rails noob, so you may already be doing this, and I just don't know)
I also found a plugin that seems to be well made called GeoKit. Link: http://geokit.rubyforge.org/ -- Maybe it will work better?