Add support for compressed request bodies
authorTom Hughes <tom@compton.nu>
Sun, 21 Jan 2018 15:50:32 +0000 (15:50 +0000)
committerTom Hughes <tom@compton.nu>
Wed, 24 Jan 2018 14:25:02 +0000 (14:25 +0000)
config/initializers/compressed_requests.rb [new file with mode: 0644]
test/integration/compressed_requests_test.rb [new file with mode: 0644]

diff --git a/config/initializers/compressed_requests.rb b/config/initializers/compressed_requests.rb
new file mode 100644 (file)
index 0000000..c6a84a1
--- /dev/null
@@ -0,0 +1,42 @@
+module OpenStreetMap
+  class CompressedRequests
+    def initialize(app)
+      @app = app
+    end
+
+    def method_handled?(env)
+      %w[POST PUT].include? env["REQUEST_METHOD"]
+    end
+
+    def encoding_handled?(env)
+      %w[gzip deflate].include? env["HTTP_CONTENT_ENCODING"]
+    end
+
+    def call(env)
+      if method_handled?(env) && encoding_handled?(env)
+        extracted = decode(env["rack.input"], env["HTTP_CONTENT_ENCODING"])
+
+        env.delete("HTTP_CONTENT_ENCODING")
+        env["CONTENT_LENGTH"] = extracted.bytesize
+        env["rack.input"] = StringIO.new(extracted)
+      end
+
+      if env["HTTP_CONTENT_ENCODING"]
+        [415, {}, []]
+      else
+        @app.call(env)
+      end
+    end
+
+    def decode(input, content_encoding)
+      input.rewind
+
+      case content_encoding
+      when "gzip" then Zlib::GzipReader.new(input).read
+      when "deflate" then Zlib::Inflate.inflate(input.read)
+      end
+    end
+  end
+end
+
+Rails.configuration.middleware.use OpenStreetMap::CompressedRequests
diff --git a/test/integration/compressed_requests_test.rb b/test/integration/compressed_requests_test.rb
new file mode 100644 (file)
index 0000000..0c66b30
--- /dev/null
@@ -0,0 +1,181 @@
+require "test_helper"
+
+class CompressedRequestsTest < ActionDispatch::IntegrationTest
+  def test_no_compression
+    user = create(:user)
+    changeset = create(:changeset, :user => user)
+
+    node = create(:node)
+    way = create(:way)
+    relation = create(:relation)
+    other_relation = create(:relation)
+    # Create some tags, since we test that they are removed later
+    create(:node_tag, :node => node)
+    create(:way_tag, :way => way)
+    create(:relation_tag, :relation => relation)
+
+    # simple diff to change a node, way and relation by removing
+    # their tags
+    diff = <<CHANGESET.strip_heredoc
+      <osmChange>
+       <modify>
+        <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset.id}' version='1'/>
+        <way id='#{way.id}' changeset='#{changeset.id}' version='1'>
+         <nd ref='#{node.id}'/>
+        </way>
+       </modify>
+       <modify>
+        <relation id='#{relation.id}' changeset='#{changeset.id}' version='1'>
+         <member type='way' role='some' ref='#{way.id}'/>
+         <member type='node' role='some' ref='#{node.id}'/>
+         <member type='relation' role='some' ref='#{other_relation.id}'/>
+        </relation>
+       </modify>
+      </osmChange>
+CHANGESET
+
+    # upload it
+    post "/api/0.6/changeset/#{changeset.id}/upload",
+         :params => diff,
+         :headers => {
+           "HTTP_AUTHORIZATION" => format("Basic %s", Base64.encode64("#{user.display_name}:test")),
+           "HTTP_CONTENT_TYPE" => "application/xml"
+         }
+    assert_response :success,
+                    "can't upload an uncompressed diff to changeset: #{@response.body}"
+
+    # check that the changes made it into the database
+    assert_equal 0, Node.find(node.id).tags.size, "node #{node.id} should now have no tags"
+    assert_equal 0, Way.find(way.id).tags.size, "way #{way.id} should now have no tags"
+    assert_equal 0, Relation.find(relation.id).tags.size, "relation #{relation.id} should now have no tags"
+  end
+
+  def test_gzip_compression
+    user = create(:user)
+    changeset = create(:changeset, :user => user)
+
+    node = create(:node)
+    way = create(:way)
+    relation = create(:relation)
+    other_relation = create(:relation)
+    # Create some tags, since we test that they are removed later
+    create(:node_tag, :node => node)
+    create(:way_tag, :way => way)
+    create(:relation_tag, :relation => relation)
+
+    # simple diff to change a node, way and relation by removing
+    # their tags
+    diff = <<CHANGESET.strip_heredoc
+      <osmChange>
+       <modify>
+        <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset.id}' version='1'/>
+        <way id='#{way.id}' changeset='#{changeset.id}' version='1'>
+         <nd ref='#{node.id}'/>
+        </way>
+       </modify>
+       <modify>
+        <relation id='#{relation.id}' changeset='#{changeset.id}' version='1'>
+         <member type='way' role='some' ref='#{way.id}'/>
+         <member type='node' role='some' ref='#{node.id}'/>
+         <member type='relation' role='some' ref='#{other_relation.id}'/>
+        </relation>
+       </modify>
+      </osmChange>
+CHANGESET
+
+    # upload it
+    post "/api/0.6/changeset/#{changeset.id}/upload",
+         :params => gzip_content(diff),
+         :headers => {
+           "HTTP_AUTHORIZATION" => format("Basic %s", Base64.encode64("#{user.display_name}:test")),
+           "HTTP_CONTENT_ENCODING" => "gzip",
+           "HTTP_CONTENT_TYPE" => "application/xml"
+         }
+    assert_response :success,
+                    "can't upload a gzip compressed diff to changeset: #{@response.body}"
+
+    # check that the changes made it into the database
+    assert_equal 0, Node.find(node.id).tags.size, "node #{node.id} should now have no tags"
+    assert_equal 0, Way.find(way.id).tags.size, "way #{way.id} should now have no tags"
+    assert_equal 0, Relation.find(relation.id).tags.size, "relation #{relation.id} should now have no tags"
+  end
+
+  def test_deflate_compression
+    user = create(:user)
+    changeset = create(:changeset, :user => user)
+
+    node = create(:node)
+    way = create(:way)
+    relation = create(:relation)
+    other_relation = create(:relation)
+    # Create some tags, since we test that they are removed later
+    create(:node_tag, :node => node)
+    create(:way_tag, :way => way)
+    create(:relation_tag, :relation => relation)
+
+    # simple diff to change a node, way and relation by removing
+    # their tags
+    diff = <<CHANGESET.strip_heredoc
+      <osmChange>
+       <modify>
+        <node id='#{node.id}' lon='0' lat='0' changeset='#{changeset.id}' version='1'/>
+        <way id='#{way.id}' changeset='#{changeset.id}' version='1'>
+         <nd ref='#{node.id}'/>
+        </way>
+       </modify>
+       <modify>
+        <relation id='#{relation.id}' changeset='#{changeset.id}' version='1'>
+         <member type='way' role='some' ref='#{way.id}'/>
+         <member type='node' role='some' ref='#{node.id}'/>
+         <member type='relation' role='some' ref='#{other_relation.id}'/>
+        </relation>
+       </modify>
+      </osmChange>
+CHANGESET
+
+    # upload it
+    post "/api/0.6/changeset/#{changeset.id}/upload",
+         :params => deflate_content(diff),
+         :headers => {
+           "HTTP_AUTHORIZATION" => format("Basic %s", Base64.encode64("#{user.display_name}:test")),
+           "HTTP_CONTENT_ENCODING" => "deflate",
+           "HTTP_CONTENT_TYPE" => "application/xml"
+         }
+    assert_response :success,
+                    "can't upload a deflate compressed diff to changeset: #{@response.body}"
+
+    # check that the changes made it into the database
+    assert_equal 0, Node.find(node.id).tags.size, "node #{node.id} should now have no tags"
+    assert_equal 0, Way.find(way.id).tags.size, "way #{way.id} should now have no tags"
+    assert_equal 0, Relation.find(relation.id).tags.size, "relation #{relation.id} should now have no tags"
+  end
+
+  def test_invalid_compression
+    user = create(:user)
+    changeset = create(:changeset, :user => user)
+
+    # upload it
+    post "/api/0.6/changeset/#{changeset.id}/upload",
+         :params => "",
+         :headers => {
+           "HTTP_AUTHORIZATION" => format("Basic %s", Base64.encode64("#{user.display_name}:test")),
+           "HTTP_CONTENT_ENCODING" => "unknown",
+           "HTTP_CONTENT_TYPE" => "application/xml"
+         }
+    assert_response :unsupported_media_type
+  end
+
+  private
+
+  def gzip_content(uncompressed)
+    compressed = StringIO.new
+    gz = Zlib::GzipWriter.new(compressed)
+    gz.write(uncompressed)
+    gz.close
+    compressed.string
+  end
+
+  def deflate_content(uncompressed)
+    Zlib::Deflate.deflate(uncompressed)
+  end
+end