diff --git a/Gemfile b/Gemfile
index b4a0967b..d5b42d83 100644
--- a/Gemfile
+++ b/Gemfile
@@ -23,6 +23,7 @@ gem 'pg'
gem 'pundit'
gem 'pundit_extra'
gem 'rack-cors'
+gem 'rack-attack'
gem 'redis'
gem 'slack-notifier'
gem 'snorlax'
diff --git a/Gemfile.lock b/Gemfile.lock
index 7e2590c1..23d4c827 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -177,6 +177,8 @@ GEM
activesupport (>= 3.0.0)
pundit_extra (0.3.0)
rack (2.0.1)
+ rack-attack (5.0.1)
+ rack
rack-cors (0.4.0)
rack-test (0.6.3)
rack (>= 1.0)
@@ -316,6 +318,7 @@ DEPENDENCIES
pry-rails
pundit
pundit_extra
+ rack-attack
rack-cors
rails (~> 5.0.0)
rails3-jquery-autocomplete
diff --git a/app/controllers/hacks_controller.rb b/app/controllers/hacks_controller.rb
new file mode 100644
index 00000000..1abe3e60
--- /dev/null
+++ b/app/controllers/hacks_controller.rb
@@ -0,0 +1,37 @@
+# bad code that should be checked over before entering one of the
+# nice files from the right side of this repo
+class HacksController < ApplicationController
+ include ActionView::Helpers::TextHelper # string truncate method
+
+ # rate limited by rack-attack - currently 5r/s
+ # TODO: what else can we do to make get_with_redirects safer?
+ def load_url_title
+ authorize :Hack
+ url = params[:url]
+ response, url = get_with_redirects(url)
+ title = get_encoded_title(response)
+ render json: { success: true, title: title, url: url }
+ rescue StandardError => e
+ render json: { success: false }
+ end
+
+ private
+
+ def get_with_redirects(url)
+ uri = URI.parse(url)
+ response = Net::HTTP.get_response(uri)
+ while response.code == '301'
+ uri = URI.parse(response['location'])
+ response = Net::HTTP.get_response(uri)
+ end
+ [response, uri.to_s]
+ end
+
+ def get_encoded_title(http_response)
+ title = http_response.body.sub(/.*
(.*)<\/title>.*/m, '\1')
+ charset = http_response['content-type'].sub(/.*charset=(.*);?.*/, '\1')
+ charset = nil if charset == 'text/html'
+ title = title.force_encoding(charset) if charset
+ truncate(title, length: 140)
+ end
+end
diff --git a/app/policies/hack_policy.rb b/app/policies/hack_policy.rb
new file mode 100644
index 00000000..b6fbf6ce
--- /dev/null
+++ b/app/policies/hack_policy.rb
@@ -0,0 +1,5 @@
+class HackPolicy < ApplicationPolicy
+ def load_url_title?
+ true
+ end
+end
diff --git a/config/application.rb b/config/application.rb
index b629682a..96505b32 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -26,6 +26,8 @@ module Metamaps
Doorkeeper::ApplicationController.helper ApplicationHelper
end
+ config.middleware.use Rack::Attack
+
# Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters += [:password]
diff --git a/config/initializers/rack-attack.rb b/config/initializers/rack-attack.rb
new file mode 100644
index 00000000..9dfe3746
--- /dev/null
+++ b/config/initializers/rack-attack.rb
@@ -0,0 +1,59 @@
+class Rack::Attack
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
+
+ # Throttle all requests by IP (60rpm)
+ #
+ # Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}"
+ throttle('req/ip', :limit => 300, :period => 5.minutes) do |req|
+ req.ip # unless req.path.start_with?('/assets')
+ end
+
+ # Throttle POST requests to /login by IP address
+ #
+ # Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}"
+ throttle('logins/ip', :limit => 5, :period => 20.seconds) do |req|
+ if req.path == '/login' && req.post?
+ req.ip
+ end
+ end
+
+ # Throttle POST requests to /login by email param
+ #
+ # Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{req.email}"
+ #
+ # Note: This creates a problem where a malicious user could intentionally
+ # throttle logins for another user and force their login requests to be
+ # denied, but that's not very common and shouldn't happen to you. (Knock
+ # on wood!)
+ throttle("logins/email", :limit => 5, :period => 20.seconds) do |req|
+ if req.path == '/login' && req.post?
+ # return the email if present, nil otherwise
+ req.params['email'].presence
+ end
+ end
+
+ throttle('load_url_title/req/ip', :limit => 5, :period => 1.second) do |req|
+ # If the return value is truthy, the cache key for the return value
+ # is incremented and compared with the limit. In this case:
+ # "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}"
+ #
+ # If falsy, the cache key is neither incremented nor checked.
+
+ req.ip if req.path == 'hacks/load_url_title'
+ end
+
+ self.throttled_response = lambda do |env|
+ now = Time.now
+ match_data = env['rack.attack.match_data']
+ period = match_data[:period]
+ limit = match_data[:limit]
+
+ headers = {
+ 'X-RateLimit-Limit' => limit.to_s,
+ 'X-RateLimit-Remaining' => '0',
+ 'X-RateLimit-Reset' => (now + (period - now.to_i % period)).to_s
+ }
+
+ [429, headers, ['']]
+ end
+end
diff --git a/config/routes.rb b/config/routes.rb
index fe48b6ba..84112d23 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -80,4 +80,8 @@ Metamaps::Application.routes.draw do
get 'users/:id/details', to: 'users#details', as: :details
post 'user/updatemetacodes', to: 'users#updatemetacodes', as: :updatemetacodes
resources :users, except: [:index, :destroy]
+
+ namespace :hacks do
+ get 'load_url_title'
+ end
end
diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js
index d14a0cf4..13258857 100644
--- a/frontend/src/Metamaps/PasteInput.js
+++ b/frontend/src/Metamaps/PasteInput.js
@@ -88,6 +88,18 @@ const PasteInput = {
import_id,
{
success: function(topic) {
+ $.get('/hacks/load_url_title', {
+ url: text
+ }, function success(data, textStatus) {
+ var selector = '#showcard #topic_' + topic.get('id') + ' .best_in_place'
+ if ($(selector).find('form').length > 0) {
+ $(selector).find('textarea, input').val(data.title)
+ } else {
+ $(selector).html(data.title)
+ }
+ topic.set('name', data.title)
+ topic.save()
+ })
TopicCard.showCard(topic.get('node'), function() {
$('#showcard #titleActivator').click()
.find('textarea, input').focus()