タケユー・ウェブ日報

Ruby on Rails や Flutter といったWeb・モバイルアプリ技術を武器にお客様のビジネス立ち上げを支援する、タケユー・ウェブ株式会社の技術ブログです。

AWS WAF v2 を使って特定のURLパスにアクセス元IPアドレス制限をかける

やりたかったこと

  • ALB 配下のWebサーバーの特定のURLパス( /admin/ )には特定のIPアドレスからのみアクセスできるようにする
  • IPアドレスの数は1個~たくさん

断念したこと

CloudFormation テンプレート

ポイント

  • AWS::WAFv2::IPSetIPアドレス(プレフィクス付き)を指定すること
  • CloudFrontで使う場合は Scope: "CLOUDFRONT" にすることと、米国東部 (バージニア北部) リージョン (us-east-1) にリソースを作成すること。ALBで使う場合は Scope: "REGIONAL"
  • ルールの順番は次の通り
    1. 「特定のURLパス」以外なら許可
    2. 「特定のIPアドレス」なら許可
    3. どちらでもない場合は拒否

YAML

Parameters:
  LoadBalancerArn:
    Type: String
  PathPattern:
    Type: String
    Default: /admin/

Resources:
  WlitelistIpAddressSet:
    Type: "AWS::WAFv2::IPSet"
    Properties:
      Addresses:
        -  200.100.0.0/24
        -  100.200.100.200/32
      IPAddressVersion: IPV4
      Scope: "REGIONAL"

  WhitelistPathPatternSet:
    Type: AWS::WAFv2::RegexPatternSet
    Properties: 
      RegularExpressionList: 
        - !Sub "^${PathPattern}*"
      Scope: "REGIONAL"

  WebACLAssociation:
    Type: AWS::WAFv2::WebACLAssociation
    Properties: 
      ResourceArn: !Ref LoadBalancerArn
      WebACLArn: !GetAtt WhitelistWAFv2WebACL.Arn

  WhitelistWAFv2WebACL:
    Type: "AWS::WAFv2::WebACL"
    Properties:
      DefaultAction:
        Block: {}
      Rules:
        - Name: "WhitelistWAFv2WebACLRulePathPattern"
          Action:
              Allow: {}
          Priority: 100
          Statement:
            NotStatement:
              Statement:
                RegexPatternSetReferenceStatement:
                  Arn: !GetAtt WhitelistPathPatternSet.Arn
                  FieldToMatch: 
                    UriPath: {}
                  TextTransformations: 
                    - Type: "URL_DECODE"
                      Priority: 0
          VisibilityConfig:
            CloudWatchMetricsEnabled: true
            MetricName: "WhitelistWAFv2WebACLRulePathPatternMetric"
            SampledRequestsEnabled: true
        - Name: "WhitelistWAFv2WebACLRuleIPSet"
          Action:
              Allow: {}
          Priority: 1000
          Statement:
            IPSetReferenceStatement:
              Arn: !GetAtt WlitelistIpAddressSet.Arn
          VisibilityConfig:
            CloudWatchMetricsEnabled: true
            MetricName: "WhitelistWAFv2WebACLRuleIPSetMetric"
            SampledRequestsEnabled: true
      Scope: "REGIONAL"
      VisibilityConfig:
        CloudWatchMetricsEnabled: true
        MetricName: "WhitelistWAFv2WebACLMetric"
        SampledRequestsEnabled: true

Rails + PostGIS (activerecord-postgis-adapter) で矩形内に含まれるレコードを検索する

# == Schema Information
#
# Table name: places
#
#  id                    :bigint           not null, primary key
#  geom                  :geography        not null, point, 4326
#
# Indexes
#
#  index_places_on_geom                   (geom) USING gist

class Place < ApplicationRecord
end

class CreatePlaces < ActiveRecord::Migration[6.0]
  def change
    create_table :places do |t|
      t.st_point :geom, geographic: true, null: false
      t.index :geom, using: :gist
    end
  end
end
Place.where("geom && ST_MakeEnvelope(:min_lng, :min_lat, :max_lng, :max_lat, 4326)", min_lat: min_lat, min_lng: min_lng, max_lat: max_lat, max_lng: max_lng)

postgis.net

blog.takeyuweb.co.jp

Docker Compose で PostGIS を使った開発環境を構築する

postgis/postgis イメージがあるのでそれを使います。

hub.docker.com

このイメージは公式のpostgresイメージをベースに作成されていて、安心感があります。 Postgres の各バージョンと、PostGIS の各バージョンそれぞれの組み合わせから選べるので、多くの場面で使えるでしょう。

利用例

github.com

docker-compose.yml

version: "3"
volumes:
  pg_data:
    driver: local
services:
  pg:
    image: postgis/postgis:11-2.5-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - pg_data:/var/lib/postgresql/data
  app:
    # 省略

Rails との統合

config/database.yml

adapter: postgisactiverecord-postgis-adapter を使います。 これは標準のアダプタに空間データとRubyオブジェクトに変換などの機能を追加するものです。

default: &default
  adapter: postgis
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  database: rails_postgis_sample_development
  username: <%= ENV.fetch("DB_USERNAME") { "postgres" } %>
  password: <%= ENV.fetch("DB_PASSWORD") { "password" } %>
  host: <%= ENV.fetch("DB_HOST") { "pg" } %>

development:
  <<: *default

test:
  <<: *default
  database: rails_postgis_sample_test

production:
  <<: *default
  database: rails_postgis_sample_production
migration

enable_extension 'postgis'

class CreateExtensionPostgis < ActiveRecord::Migration[6.0]
  def up
    enable_extension 'postgis' unless extension_enabled?('postgis')
  end

  def down
    disable_extension 'postgis' if extension_enabled?('postgis')
  end
end

Rails で言語別に複数形変換 pluralize をカスタマイズする

現在スペイン語のサービス開発を行っています。その中でスペイン語の複数形で少し困ったので対応メモです。

たとえばスペイン語では、事務所用建物は local comercial ですが、これが複数形になると、localcomercial の両方が複数形になり locales comerciales となります。 また、 local の複数形は locals ではなく localescomercialcomercials ではなく comerciales です。

このような言語特有の複数形やイレギュラーは、Rails標準の pluralize では正しく変換することができません。

"local comercial".pluralize #=> "local comercials"

このようなときは、 config/initializers/inflections.rb に独自のルールを定義します。

# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:es) do |inflect|
  inflect.irregular 'local comercial', 'locales comerciales'
end

String#pluralize の引数として言語を渡すことができます。

"local comercial".pluralize(:es) #=> "locales comerciales"

api.rubyonrails.org

Google API Client for Ruby でGoogleDriveにアップロード

S3などのオブジェクトストレージと比べて面倒

  • フォルダを作る必要がある
  • 同名のフォルダを複数作成できてしまう
gem 'google-api-client'
require 'googleauth'
require 'google/apis/drive_v3'

FOLDER_ID = "xxxxxxxxxxxxxxxxxxxxxx" # https://drive.google.com/drive/folders/xxxxxxxxxxxxxxxxxxxxxx
UPLOAD_FILE_PATH = "/path/to/file.txt"

service_account_key_json = File.read("path/to/service-account.key.json")
drive_folder_id = FOLDER_ID

authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
  json_key_io: StringIO.new(service_account_key_json),
  scope: 'https://www.googleapis.com/auth/drive'
)
authorizer.fetch_access_token!

drive = Google::Apis::DriveV3::DriveService.new
drive.authorization = authorizer

drive.create_file(
  {
    name: "file.txt",
    parents: [drive_folder_id],
    modifiedTime: File::Stat.new(UPLOAD_FILE_PATH).mtime
  },
  upload_source: UPLOAD_FILE_PATH 
)

任意のフォルダを作成してアップロード

ちゃんとやるなら排他制御なども考える必要がありそうです。

require 'googleauth'
require 'google/apis/drive_v3'

FOLDER_ID = "xxxxxxxxxxxxxxxxxxxxxx" # https://drive.google.com/drive/folders/xxxxxxxxxxxxxxxxxxxxxx
UPLOAD_FILE_PATH = "/path/to/file.txt"

service_account_key_json = File.read("path/to/service-account.key.json")
drive_folder_id = FOLDER_ID

authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
  json_key_io: StringIO.new(service_account_key_json),
  scope: 'https://www.googleapis.com/auth/drive'
)
authorizer.fetch_access_token!

drive = Google::Apis::DriveV3::DriveService.new
drive.authorization = authorizer

# 必要なフォルダがある確認してなければ作成する
folders = "path/to/folder".split("/")
upload_folder_id = folders.each_with_object([drive_folder_id]) { |folder_name, folder_ids|
  folder = drive.list_files(
    q: "mimeType='application/vnd.google-apps.folder' and '#{folder_ids.last}' in parents and name = '#{folder_name}'",
    page_size: 1
  ).files.first
  if folder
    # noop
  else
    folder = drive.create_file({
      name: folder_name,
      mime_type: 'application/vnd.google-apps.folder',
      parents: [folder_ids.last],
    }, fields: 'id')
  end
  folder_ids.push(folder.id)
}.last

drive.create_file(
  {
    name: "file.txt",
    parents: [upload_folder_id],
    modifiedTime: File::Stat.new(UPLOAD_FILE_PATH).mtime
  },
  upload_source: UPLOAD_FILE_PATH 
)

JavaScript (TypeScript) でビデオキャプチャーからの映像を表示したり、スナップショットを取ったりしたい!

前回はJavaScript(TypeScript)で「デスクトップや他のウインドウ」を表示しました。

blog.takeyuweb.co.jp

今回は、USBビデオキャプチャー Mirabox HDMI ビデオキャプチャー HSV321 を買ったので、

f:id:uzuki05:20200612224819p:plain

  • Switch のゲームプレイ画面をブラウザ上に表示
  • 任意のタイミングでスナップショットを撮影して表示

してみました。

サンプル

See the Pen getUserMedia by Yuichi Takeuchi (@takeyuweb) on CodePen.

getUserMedia

USBビデオキャプチャーからの映像は、ブラウザからはWebカメラと同じように扱えます。 従って getUserMedia を使うことで簡単に取得することができます。

developer.mozilla.org

captureStream = await navigator.mediaDevices.getUserMedia({video: { deviceId: getSelectedVideo(), width: 1920, height: 1080 }, audio: {deviceId: getSelectedAudio()}});
const video = document.createElement("video");
video.autoplay = true;
video.srcObject = captureStream;
videoContainer.appendChild(video);

バイスの選択

// 先に許可を貰わないとenumerateDevicesが返すデバイス名(label)を取得できない
await navigator.mediaDevices.getUserMedia({video: true, audio: true});
const devices = await navigator.mediaDevices.enumerateDevices();
devices.forEach((device) => {
  console.log(device.kind + ": " + device.label + " id = " + device.deviceId);
  const option = document.createElement("option");
  option.text = device.label;
  option.value = device.deviceId;

  if (device.kind === "audioinput") {
    audioDeviceSelect.appendChild(option);
  } else if (device.kind === "videoinput") {
    videoDeviceSelect.appendChild(option);
  }
});

const getSelectedVideo = () => {
  return videoDeviceSelect.value;
}

const getSelectedAudio = () => {
  return audioDeviceSelect.value;
}

今回、私はMiraBoxで決め打ちだったので雑に書くとこんな感じになりました。

const deviceLabel = 'MiraBox'; 
let miraVideoId:string;
let miraAudioId:string;

await navigator.mediaDevices.getUserMedia({video: true, audio: true});
const devices = await navigator.mediaDevices.enumerateDevices();
devices.forEach((device) => {
  console.log(device.kind + ": " + device.label + " id = " + device.deviceId);
  if (device.kind === "audioinput") {
    if (device.label.match(deviceLabel)) {
      miraAudioId = device.deviceId;
    }
  } else if (device.kind === "videoinput") {
    if (device.label.match(deviceLabel)) {
      miraVideoId = device.deviceId;
    }
  }
});

JavaScript (TypeScript) で画面全体や他のウインドウのスクリーンショットを取りたい!

Webアプリでデスクトップや他(ブラウザ以外の)ウインドウのスクリーンショットを撮りたい場面がありました。

こういうことです。

f:id:uzuki05:20200611192328g:plain
ブラウザで画面をキャプチャ&ショット

サンプル

See the Pen wvMGvWp by Yuichi Takeuchi (@takeyuweb) on CodePen.

動作確認に使った環境は Windows 10 Pro 2004 + Chromium Edge です。

f:id:uzuki05:20200611193122p:plain
Chromium Edge

TypeScript

2020/6/11現在、TypeScriptの定義が追いついておらずコンパイルできません。

ワークアラウンド

declare global しちゃう

export {};

declare global {
  interface MediaDevices {
    getDisplayMedia(constraints?: MediaStreamConstraints): Promise<MediaStream>;
  }
}

あわせて、型定義を追加

$ npm install --save @types/dom-mediacapture-record
$ npm install --save @types/w3c-image-capture