タケユー・ウェブ日報

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