GeoIP / IP name & location resolution in unicorn logs? - ruby-on-rails

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"

Related

Using Rethinkdb's change feed in a webapp

How can I use Rethinkdb's change feed in a webapp, see http://www.rethinkdb.com/docs/changefeeds/ruby/ ? I currently use Ruby on Rails. I've tried Googling 'rethinkdb "change feed" rails' and 'rethinkdb "change feed" websocket'
I would like to display updates on a webpage to a RethinkDB table with lowest latency as possible.
RethinkDB is meant to be used from the server (from Rails) and not from the client. It's really important to understand this! If you have a listener on your data (a changefeed), then hose changes will get routed to your Rails app.
If you want to add to query RethinkDB from the front-end (from the browser), you might be interested in these two projects:
https://github.com/mikemintz/rethinkdb-websocket-client
https://github.com/mikemintz/rethinkdb-websocket-server
Once these changes are routed to your application, then you can do with them as you wish. If what you want to do is route those changes to the front-end to just show the users these changes, you can just send them through a web socket. Faye is a really good library to do this.
This is how this would look like. In your ruby code, you would add something like this:
# Add Faye
App = Faye::RackAdapter.new MessageApp, mount: "/faye"
# Changefeed listener
r.table("messages").changes.em_run(Conn) do |err, change|
App.get_client.publish('/message/new', change["new_val"])
end
Basically, whenever there's a change in the messages table, send the new value over the web socket. You can take a look at the full example (with front-end code) here:
https://github.com/thejsj/ruby-and-rethinkdb/
And here is the Ruby file:
https://github.com/thejsj/ruby-and-rethinkdb/blob/master/server/main.rb
RethinkDB seems to not support complex client authentication (auth token is shared amongst all clients), so you can't do that client-side from Javascript.
But you can create a pipeline: run websocket on your server, which will fetch records from RethinkDB and pass it to clients. Using em-websocket it will look something like this:
require 'em-websocket'
require 'rethinkdb'
include RethinkDB::Shortcuts
EventMachine.run do
#clients = []
#cursor = r.table("authors").changes.run
EM::WebSocket.start(:host => '0.0.0.0', :port => '3001') do |ws|
ws.onopen do |handshake|
#clients << ws
end
ws.onclose do
#clients.delete ws
end
#cursor.each do |document|
#clients.each{|ws| ws.send document}
end
end
end

Ruby 1.8.7 / Rails 2.3.4 - How does request.host look like in an IPv6 environment?

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.

Correct DNS setup for custom domain with subdomains and CNAMES pointing at them

I have a multi-tenant Rails app, hosted with Heroku at http://myapp.herokuapp.com. The tenants/accounts are separated via subdomains (and Postgresql schemas). I have then added my own domain as custom domain to the Heroku app. Since Heroku really advice against using A records because of "uptime implications" (https://devcenter.heroku.com/articles/avoiding-naked-domains-dns-arecords) I first tried to put a CNAME record from my custom domain to myapp.herokuapp.com. That worked fine also with subdomains like http://clientaccount.mydomain.com.
The problem began when my client wanted her own domain pointing to their account so that http://www.clientdomain.com showed http://clientaccount.mydomain.com, the most obvious solution seemed to be to create a CNAME record in my clients DNS to point to http://clientaccount.mydomain.com. That did not work, when visiting the address an error message stated that "There is no app configured at that hostname", also host www.clientdomain.comgave:
www.clientdomain.com is an alias for clientaccount.mydomain.com
clientaccount.mydomain.com is an alias for myapp.herokuapp.com
myapp.herokuapp.com is an alias for ar.herokuapp.com
After some VERY confused support from Heroku, they advised my to use A records instead pointed to their three apex IPs. So changed it but it still didn't work. They then told me to add my clients domain as a custom domain in my Heroku settings which I have done without good result.
So my current configuration is as follows:
Myapp at Heroku has following custom domains:
*.mydomain.com
www.clientdomain.com
mydomain.com DNS
*.mydomain.com has three A records pointing at Heroku apex IPs
clientdomain.com DNS
In the DNS for clientdomain.com, clientdomain.com (without www) is redirected to www.clientdomain.com (Not sure how they do it but it seems to work.)
For www.clientdomain.com there's a CNAME record pointing at clientaccount.mydomain.com
Current state
www.mydomain.com resolves correctly.
clientaccount.mydomain.com resolves correctly.
www.clientdomain.com goes to www.mydomain.com (without subdomain)
So the problem is obvously either in the DNS somehow, does the subdomain fall away somewhere since my application obviously doesn't receive it? Or do I have to change some code in Rails in order to handle this?
The subdomain is handled as route constraint, Subdomain class:
class Subdomain
def self.matches?(request)
request.subdomain.present? && Account.find_by_subdomain(request.subdomain)
end
end
Any input is appreciated!
EDIT
I have done everything that is suggested, do I have to change my application controller as well?
application_controller.rb
def handle_subdomain
if #account = Account.find_by_subdomain(request.subdomain)
PgTools.set_search_path #account.id
#current_account = #account
else
#current_account = nil
PgTools.restore_default_search_path
end
end
Your best bet here is to first setup your main domain as follows:
*.mydomain.com as a CNAME to heroku-appname.herokuapp.com
And add *.mydomain.com as a domain name to your app:
$ heroku domains:add *.mydomain.com
Next You'll want to setup an easy way for your users/clients to add their custom domains to your multi-tenant app. This requires two things, adding the domain name to your app and having them setup DNS. Here's the best approach:
add www.myclientdomain.com to your app:
$ heroku domains:add www.myclientdomain.com
Then setup DNS to point into Heroku. You're best off taking a lesson from Heroku's setup and having your clients CNAME to a domain name that you own. This will help avoid lock-in and gives you much more control over where you're directing traffic. so:
CNAME www.myclientdomain.com to proxy.mydomain.com
The CNAME will follow proxy.mydomain.com to heroku-appname.herokuapp.com and then resolve to the Heroku IPs.
Then you should be all set.
Ideally you'll be taking on new customers with custom domains faster than you can add domain names manually so you'll want to automate that step. Using the heroku api and client you can manage your custom domains, programmatically, fron within your app.
You need to do something like:
def handle_subdomain
if #account = Account.find_by_subdomain(request.subdomain) || Account.find_by_domain(request.domain)
PgTools.set_search_path #account.id
#current_account = #account
else
#current_account = nil
PgTools.restore_default_search_path
end
end
Notice Account.find_by_domain(request.domain)—although you might need request.host for the full host, not just the domain component.

Heroku Geolocation >> Always Returns "Seattle, WA"

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?

Rails: Get Client IP address

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

Resources