Rob Lacey

Brighton, UK - contact@robl.me

Software Engineer specialising in Ruby on Rails. I do occasional freelance work, I love working for SARD JV Ltd, they are awesome and give me my pocket money every month. By all means dangle a carrot.


18xx

If you fancy smashing your brains against a wall. I’m trying to understand this Ruby application. It’s interesting to say the least. https://github.com/tobymao/18xx

1. it’s 18xx, which is classic train game with economic strategy, stocks and share and the like. Pipe smoking (not crack) Socks and Sandals kind of gamer. Kat has 10 of these games, doesn’t wear sandals
2. written in Roda, kinda like Sinatra but different. Much more flexible. But harder learning curve. Nice routing based request flow.
3. Uses Opal, no javascript here. Ruby classes that translate to JS.
4. This feels like it was written by smart people in the most bizarre and almost deliberately convoluted way such that no one ever would want to contribute to their OpenSource project ever. I want to play

Rails 5.2 RemoteIP middleware

We have a broken spec for testing IP Spoofing, turns out it is raising an error.

describe 'IP Spoofing' do
  it 'does not raise an IP Spoofing error' do
    expect do
      get '/', headers: {
        'HTTP_CLIENT_IP' => '10.95.157.143',
        'HTTP_X_FORWARDED_FOR' => '62.172.169.17, 141.101.99.166, 192.168.255.26'
      }
      expect(response.status).to eq(400)
    end.not_to raise_error
  end
end

… we have middleware that captures the Exception and returns a 400 Bad Request status code instead of exploding whilst upgrading to Rails 5.2. Nice 400 errors are better than Exception(s)

module Rack
  class Spoofing < CustomMiddleware
    def call(env)
      @app.call(env)
    rescue ActionDispatch::RemoteIp::IpSpoofAttackError
      [400, {}, []]
    end
  end

  Rails.application.config.middleware.insert_before Rack::Head, Spoofing
end

We insert our Spoofing middleware just ahead of Rack::Head. We can see this in rake middleware

Robs-MacBook-Pro:some rl$ rake middleware
use Webpacker::DevServerProxy
use Rack::Ping
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use RequestStore::Middleware
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Spoofing
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
use Warden::Manager
use Remotipart::Middleware
use Rack::Jayson
use PDFKit::Middleware
use Warden::JWTAuth::Middleware
run Some::Application.routes

Just wondering where that Spoofing exception is raised

Robs-MacBook-Pro:some rl$ bundle show --paths | xargs grep -r IpSpoofAttackError
/Users/rl/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-5.2.6/lib/action_dispatch/middleware/remote_ip.rb:    class IpSpoofAttackError < StandardError; end
/Users/rl/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-5.2.6/lib/action_dispatch/middleware/remote_ip.rb:          raise IpSpoofAttackError, "IP spoofing attack?! " \

There it is, it’s raised by RemoteIP, which we can see from our middleware stack is much further down the middleware stack than Rack::Head and so in Rails 5 at least runs earlier in the chain. No wonder we can’t rescue it now. In fact it makes sense to bomb out early rather than waste effort crunching cookies, etc if we don’t need to.

If we look at the Rails 5.1 middleware stack I was almost expecting our Spoofing middleware to appear before RemoteIp. It clearly doesn’t.

use Webpacker::DevServerProxy
use Rack::Ping
use Rack::Sendfile
use ActionDispatch::Static
use Rack::Lock
use BadMultipartFormDataSanitizer
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x00007f8190611610>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use RequestStore::Middleware
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use Rack::Spoofing
use ActionDispatch::ParamsParser
use Remotipart::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Warden::Manager
use PDFKit::Middleware
use Warden::JWTAuth::Middleware
use Rack::Jayson
run Some::Application.routes

I’m actually wondering if the 400 response even comes from our Spoofing middleware in Rails 5.

Ah, no it doesn’t. Another application error is creating it. Looks like its reaching the application and not raising the spoofing error anyway :S

The RemoteIp middleware initializer looks like this in Rails 4. Turns out check_ip_spoofing is false, something turned it off.

def initialize(app, check_ip_spoofing = true, custom_proxies = nil)
      binding.pry
      @app = app
      @check_ip = check_ip_spoofing
      @proxies = if custom_proxies.blank?
        TRUSTED_PROXIES
      elsif custom_proxies.respond_to?(:any?)
        custom_proxies
      else
        Array(custom_proxies) + TRUSTED_PROXIES
      end
    end

Looks like there’s an environment setting to turn it on in the first place. Rails 5.x probably has a different default..

module Rails
  class Application
    class DefaultMiddlewareStack
      attr_reader :config, :paths, :app

      def initialize(app, config, paths)
        @app = app
        @config = config
        @paths = paths
      end

      def build_stack
          # ...
          middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
          # ... 
      end
  end
end

Well, I’m not turning it on in Rails 4 now. We want rid of it anyway. I am however going to move the middleware to sit just in front of RemoteIp so that it can actually catch the damn thing and make sure our 400 is actually coming from the thing we’re testing.

Rails.application.config.middleware.insert_before ActionDispatch::RemoteIp, Spoofing

Logging Unpermitted Parameters

Found out Unpermitted Parameters has instrumentation. So we can hijack it

*config/initializer/unpermittted.rb

logger = Logger.new(Rails.root.join('log', 'unpermitted.log'))
ActiveSupport::Notifications.subscribe 'unpermitted_parameters.action_controller' do |name, start, finish, id, payload|
  msg = "Unpermitted_parameters: #{payload[:keys].map { |k| ":#{k}" }.join(', ')}"
  puts Rainbow(msg).red.underline
  logger.debug(msg)
end

And now I have STDOUT and logs full of debug with no context whatsoever but hey, thats’s in Rails 7

https://blog.saeloun.com/2021/06/16/rails-7-provides-context-when-logging-unpermitted-parameters.html#after

Robs-MacBook-Pro:sard rl$ tail -f log/unpermitted.log 
# Logfile created on 2021-07-23 16:37:05 +0100 by logger.rb/66358
D, [2021-07-23T16:37:34.610337 #27379] DEBUG -- : Unpermitted_parameters: :id

A case of the Fridays

The Brighton Ruby Meetup by the bins round the back of Greggs, London Road went well. Greggs ran out of Vegan pastry flakes, but Sally brought us up to speed on Class inheritance and calculating the volume of a glazed doughnut under a graph.

Write code with code so you don't have to write code

I’m currently trying to bring in Strong Parameters, replacing attr_accessible with a custom class for each model class we want to sanitize params for. We don’t want to fill controllers with massive blah_params methods if we’re likely to re-use them everywhere. So if we hand this over to a class that can manage it….all the better.

BlahPermit.with(param.fetch(:blah, {})).permit

I’d rather not go through every file and create an accompanying class for what, at this stage, will only require a small tweak to each attr_accessible call.

So here we can cope with both classes we can load or define on the fly if they don’t already exist.

module NotAtAllAttrAccessible
  extend ActiveSupport::Concern

  module ClassMethods
    def attr_accessible(*attrs)
      klass = define_permit_class
      klass.permits += attrs

      define_method :accessible_attributes do
        klass.permits
      end unless method_defined? :accessible_attributes
    end

    private

    def define_permit_class
      name = "#{self.name}Permit"
      Object.const_get(name)

    rescue NameError
      Object.const_set(name, Class.new(Permit))
    end
  end
end

And that will equate to

class Blah
  attr_accessible :thing, :the, :blair, :turnip
end

class Permit
  cattr_accessor :permits, default: []

  class << self
    def klass(klass)
      "#{klass}Permit".constantize
    rescue NameError
      Permit
    end

    def with(params)
      new.with(params)
    end
  end

  def with(params)
    @params = params.is_a?(ActionController::Parameters) ? params : ActionController::Parameters.new(params)
    self
  end

  def permit!
    permits.any? ? params.permit(*permits) : params.permit!
  end

  def permits
    self.class.permits
  end

  def params
    @params || ActionController::Parameters.new
  end
end

class BlahPermit < Permit
  @@permits = [:thing, :the, :blair, :turnip]
end

And we can then crush Parameters like a boss.

def resource_params
  Permit.klass(Blah).with(param.fetch(:blah, {})).permit
end

This Space Between Us

Grouped and Non-Grouped Options for Select

module ActionView
  module Helpers
     module FormOptionsHelper
      def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
        collection.map do |group|
          next options_for_select([group], selected_key) if value_for_collection(group, group_method).is_a?(String)

          option_tags = options_from_collection_for_select(
            value_for_collection(group, group_method), option_key_method, option_value_method, selected_key)
          content_tag("optgroup", option_tags, label: value_for_collection(group, group_label_method))
        end.join.html_safe
      end
    end
  end
end

D3 Colour Wheel

Quick, dirty colour wheel for Uncle Simon easily adapted from this example code.

https://www.essycode.com/posts/create-color-wheel-javascript-d3/

Should probably Stimulusorizificate it.

node-sass moaning like a boss

And again…

/Users/rl/.node-gyp/16.5.0/include/node/v8-internal.h:454:38: error: no template named 'remove_cv_t' in namespace 'std'; did you mean 'remove_cv'?
            !std::is_same<Data, std::remove_cv_t<T>>::value>::Perform(data);
                                ~~~~~^~~~~~~~~~~
                                     remove_cv
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/type_traits:776:50: note: 'remove_cv' declared here
template <class _Tp> struct _LIBCPP_TEMPLATE_VIS remove_cv
                                                 ^
1 error generated.
make: *** [Release/obj.target/binding/src/binding.o] Error 1
gyp ERR! build error 
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/Users/rl/repos/rostering/vendor/engines/sard_core/ui/node_modules/node-gyp/lib/build.js:262:23)
gyp ERR! stack     at ChildProcess.emit (node:events:394:28)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (node:internal/child_process:290:12)
gyp ERR! System Darwin 20.5.0
gyp ERR! command "/usr/local/Cellar/node/16.5.0/bin/node" "/Users/rl/repos/rostering/vendor/engines/sard_core/ui/node_modules/node-gyp/bin/node-gyp.js" "rebuild" "--verbose" "--libsass_ext=" "--libsass_cflags=" "--libsass_ldflags=" "--libsass_library="
gyp ERR! cwd /Users/rl/repos/rostering/vendor/engines/sard_core/ui/node_modules/node-sass
gyp ERR! node -v v16.5.0
gyp ERR! node-gyp -v v3.8.0
gyp ERR! not ok 

Yeah, srsly dude. This isn’t right for us either. I think that brew upgrade I did last night broke something, must have upgraded node. Apparently upgrading beyond node-sass 6.0.1 is fine now. Last time this happened I had to downgrade node.

https://stackoverflow.com/questions/67241196/error-no-template-named-remove-cv-t-in-namespace-std-did-you-mean-remove

No, can’t see anything above 6.0.1 out there :S I need to get back to node@14 again.

Robs-MacBook-Pro:ui rl$ brew unlink node
Unlinking /usr/local/Cellar/node/16.5.0... 7 symlinks removed.
Robs-MacBook-Pro:ui rl$ brew link --overwrite node@14
Linking /usr/local/Cellar/node@14/14.17.0... 3872 symlinks created.

What a faff. Now I can play with d3

Porting Ruby to JS.

module Rack
  module Utils
    def append_to_query_string(url, params = {})
      base, query = url.split('?')
      params = parse_query(query).merge(params.stringify_keys)
      params = params.sort_by { |k, _v| k.to_s }.to_h
      query = build_query(params)
      [base, query].reject(&:blank?).join('?')
    end
    module_function :append_to_query_string
  end
end

window.BLAHUtils = {
  isPresent: function(object) {
    return object != undefined &&  object.toString().trim() !== ''
  },
  presense: function(object) {
    return this.isPresent(object) ? object : null
  },
  appendToQueryString: function(url, params = {}) {
    [base, query] = url.split('?')
    params = {
      ...this.parseQuery(query),
      ...params
    }
    query = this.buildQuery(params)
    return [base, query].filter(v => this.isPresent(v)).join('?')
  },
  parseQuery: function(query) {
    if (!this.isPresent(query)) {
      return {}
    }
    return query.split('&').reduce((h, pair) => {
      [k, v] = pair.split('=')
      h[k] = decodeURIComponent(v)
      return h
    }, {})
  },
  buildQuery: function(params) {
    return Object.entries(params).map(([k, v]) => {
      return [k, encodeURIComponent(v)].join('=')
    }).join('&')
  }
}