博多南ウェブサービスのblog

博多南ウェブサービスのサービス紹介

Powershell Core 7.3 から、SendMessage() を呼び出して、背面にあるGoogle Play Games BETA の画面をスワイプ操作したい

やりたいこと

2023/5/3時点の、Google Play Games BETA のプレイ画面に対して、プレイ画面が他のウィンドウの下にあっても、スワイプ操作したい。

やったこと

Powershell Core 7.3 から、SendMessage 関数 を呼び出して、スワイプ操作をエミュレートしたメッセージを送信した。

### C#
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public static class Win32
{
    [DllImport("user32.dll")]
    static extern IntPtr FindWindow(
        string lpClassName,
        string lpWindowName
    );
    
    public static IntPtr FindWindowByName(string lpWindowName)
    {
        return FindWindow(null, lpWindowName);
    }

    [DllImport("user32.dll")]
    static extern int GetWindowThreadProcessId(
        IntPtr hWnd,
        IntPtr ProcessId
    );

    [DllImport("user32.dll")]
    static extern IntPtr GetForegroundWindow();

    [DllImport("kernel32.dll")]
    static extern int GetCurrentThreadId();

    [DllImport("user32.dll")]
    static extern bool SetWindowPos(
        IntPtr hWnd,
        IntPtr hWndInsertAfter,
        int X,
        int Y,
        int cx,
        int cy,
        int uFlags
    );

    [DllImport("user32.dll")]
    static extern bool AttachThreadInput(
        int idAttach,
        int idAttachTo,
        bool fAttach
    );

    [DllImport("user32.dll")]
    static extern IntPtr SendMessage(
        IntPtr hWnd,
        uint Msg,
        int wParam,
        int lParam
    );

    static void SimulateBackGround(
        string lpWindowName,
        string simualtedTask,
        int x = 0,
        int y = 0,
        int movedX = 0,
        int movedY = 0
    )
    {
        var targetWnd = FindWindowByName(lpWindowName);
        var wakfuThreadID = GetWindowThreadProcessId(targetWnd, IntPtr.Zero);

        var foreWnd = GetForegroundWindow();
        var windowThreadProcessId = GetCurrentThreadId();
        AttachThreadInput(windowThreadProcessId, wakfuThreadID, true);

        int mousePos = (y << 16) | (x & 0xffff);
        int movedMousePos = (movedY << 16) | (movedX & 0xffff);

        const int WM_MOUSEMOVE = 0x0200;
        const int WM_LBUTTONDOWN = 0x0201;
        const int WM_LBUTTONUP = 0x0202;
        const int MK_LBUTTON = 0x0001;
        
        switch (simualtedTask)
        {
            case "tap":
                SendMessage(targetWnd, WM_MOUSEMOVE, 0, mousePos);
                SendMessage(targetWnd, WM_LBUTTONDOWN, MK_LBUTTON, mousePos);
        
                System.Threading.Thread.Sleep(50);
                
                SendMessage(targetWnd, WM_MOUSEMOVE, 0, mousePos);
                SendMessage(targetWnd, WM_LBUTTONUP, 0, mousePos);
                break;
            case "swipe":
                SendMessage(targetWnd, WM_MOUSEMOVE, 0, mousePos);
                SendMessage(targetWnd, WM_LBUTTONDOWN, MK_LBUTTON, mousePos);

                System.Threading.Thread.Sleep(100);

                SendMessage(targetWnd, WM_MOUSEMOVE, 0, movedMousePos);
                SendMessage(targetWnd, WM_LBUTTONDOWN, MK_LBUTTON, movedMousePos);

                System.Threading.Thread.Sleep(110);
                SendMessage(targetWnd, WM_LBUTTONUP, 0, movedMousePos);
                break;
            default:
                break;
        }

        const int SWP_NOSIZE = 0x0001;
        const int SWP_NOMOVE = 0x0002;
        const int SWP_SHOWWINDOW = 0x0040;
        SetWindowPos(foreWnd, -1, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
        SetWindowPos(foreWnd, -2, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOMOVE | SWP_NOSIZE);
        
        AttachThreadInput(windowThreadProcessId, wakfuThreadID, false);
    }

    public static void TapBackGround(
        string lpWindowName,
        int x,
        int y
        )
    {
        SimulateBackGround(lpWindowName, "tap", x, y);      
    }    

    public static void SwipeBackGround(
        string lpWindowName,
        int x,
        int y,
        int movedX,
        int movedY
        )
    {
        SimulateBackGround(lpWindowName, "swipe", x, y, movedX, movedY);
    }
}
"@

### Powershell
Set-Variable WINDOW_TITLE -Option Constant -Value "操作したいアプリのウィンドウタイトル"
[int]$local:startXforSwipe = 500
[int]$local:startYforSwipe = 600
[int]$local:endXforSwipe = 1000
[int]$local:endYforSwipe = 600
[Win32]::SwipeBackGround(
                            ${WINDOW_TITLE},
                            ${startXforSwipe},
                            ${startYforSwipe},
                            ${endXforSwipe},
                            ${endYforSwipe}
                            )

# tap
# [int]$local:xforTap = 500
# [int]$local:yforTap = 600
# [Win32]::TapBackGround(
#                         ${WINDOW_TITLE},
#                         ${xforTap},
#                         ${yforTap}
#                         )

やっていて困ったこと

参考

Bing Search を使っているときにEnter key -> 送信を防ぐときのメモ

やりたいこと

Bing Search を利用しているとき、Enter key でメッセージが送信されるのを防ぎたいときのメモです。

つまり

  • Enter key -> テキストエリア内の改行
  • Shift key + Enter key -> メッセージ送信

です。

やったこと

以下のUserScript を、Edge で利用できるようにした。

これは、

を行うものです。

// ==UserScript==
// @name         Enter for new line and Shift + Enter for submit in Bing Chat
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Allows using Enter for new line and Shift + Enter for submit in Bing Chat text area
// @author       You
// @match        https://www.bing.com/search*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    // https://stackoverflow.com/questions/6014702/how-do-i-detect-shiftenter-and-generate-a-new-line-in-textarea/6015906#6015906
    // and modified
    document.addEventListener('keydown', function(event) {
        // ここはBing Search チャット画面の要素が変更されたら更新しないといけないよ
        let cibSerpMain = document.querySelector(".cib-serp-main");
        let cibActionBarMain = cibSerpMain.shadowRoot.querySelector("#cib-action-bar-main");
        let divRoot = cibActionBarMain.shadowRoot.querySelector(".root");
        let mainContainter = divRoot.querySelector(".main-container");
        let inputContainter = mainContainter.querySelector(".input-container");
        let cibTextInput = inputContainter.querySelector("cib-text-input");
        let searchBoxForm = cibTextInput.shadowRoot.querySelector("#searchboxform");
        let textInput = searchBoxForm.querySelector(".text-input");
        let searchBox = textInput.querySelector("#searchbox");

        if (searchBox) {
            if (event.keyCode == 13 && event.shiftKey) {
                event.stopImmediatePropagation();
                const keyboardEvent = new KeyboardEvent('keydown', {
                    code: 'Enter',
                    key: 'Enter',
                    view: window,
                    bubbles: true
                });
                searchBox.dispatchEvent(keyboardEvent);
                searchBox.value = '';
            } else if (event.keyCode == 13) {
                event.stopImmediatePropagation();
            }
        }
    }, true);
})();

できなかったこと

Shift + Enter key でメッセージ送信後、テキストエリアに改行が1つ残る(クリア後、挿入される?)。消す方法がわからなかった。

参考

Bing Search を使っているときにチャット→検索タブに移動するのを防ぐときのメモ

やりたいこと

Bing Search を利用しているとき、マウスのスクロールを使うと、ふとしたタイミングでチャットタブから検索タブに移動してしまう場合がある。

これを防ぎたいときのメモです。

やったこと

reddit の質問と回答Disabling scroll to chat? にあるUserScript を、Edge で利用できるようにした。

具体的には、

  1. Tampermonkey をEdge にインストール
  2. Tampermonkey にて、新規スクリプト

    window.addEventListener("wheel", e=>{ if(e.target.className.includes("cib-serp-main")) e.stopPropagation(); });
    

    を登録

  3. チャット画面 にて、登録したUserScript が有効となっていることを確認
  4. マウスのスクロールを行っても、チャット→検索タブに移動しないことを確認

参考

UWSC を PowerShell Core 7.3 に置き換えたときのメモ

やりたいこと

  1. Windows11 で表示した Window (Window1)のうち、(最小化ではなく)背面に隠れている画面のキャプチャを取得(画像1)
  2. UWSC の ChkImgX で、取得した画像1とターゲット画像(画像2)を比較し、画像2が画像1の中にあればその位置(位置1)を取得
  3. UWSC で、取得した位置1をもとに、Windows1 の該当する箇所をクリック

といった、自動化をやりたい。

ただ、Window1 が背面にある場合、ChkImgX で画面のキャプチャが取得できない(真っ黒画面が取得される)ことが分かった(これは、Window1側の設定によるものなのかは不明。)

そこで、Powershell Core 7.3 + C++ (14?)でやってみたときのメモです。

やったこと

  1. 以下を実施するdll(独自dll ) を、C++ で作成
    1. Win32API の PrintWindow を呼び出して、(最小化ではなく)背面に隠れている画面のキャプチャを取得(画像1)
    2. OpenCV 4.7 で、取得した画像1とターゲット画像(画像2)を比較し、画像2が画像1の中にあればその位置(位置1)を取得
  2. Powershell から、独自dll を呼び出して、位置1を取得
  3. (今回対象の Window が Windows Subsystem for Android のアプリだったので)Powershell から、位置1をタップするコマンド(adb.exe ***)を実行

初めてのC++ステップ

C++ について、初めて触れる → 独自dll 作成までの各ステップ

  1. Vissual Studio 2022 をインストールする
  2. マイクロソフトのLearning を参考に、独自dll とそれを呼び出すコンソールアプリ(アプリ1)を作成する
  3. このタイミングで、独自dll のRelease バージョンビルド後、Powershell から呼び出せることを確認する(でないと、どこが悪いかわからなくなるので)
  4. デバッグ用として、独自dll からの結果をアプリ1で表示する方法、またはVisual Studio 2022 のデバッガを起動して変数の値を表示する方法を確認する
  5. PrintWindow 関数を呼び出すときに必要なお作法(GetDC でデバイスコンテキスト取得したら、処理完了後、プログラム上で責任もって解放する(ReleaseDC)等)を調べる
  6. PrintWindow 関数を呼び出して、画像1が取得できることを確認する(クリップボードに保存が簡単だった)
  7. 画像1を、OpenCV で使用するMat 型に変換し、変換できていることを確認する(画像ファイルに保存が簡単だった)
  8. OpenCV の matchTemplate 関数を使用して位置1を取得し、取得できていることを確認する(OpenCV のサンプルコードが参考になった)
  9. 独自dll のRelease バージョンビルド後、Powershell から呼び出せることを確認する

実際のコード

ChkImgCpp.h

#pragma once

#define CHKIMGCPP_API __declspec(dllexport)

#include <windows.h>
#include <string>

struct CHKIMGCPP_API ReturnPoint {
    int doneMatching; // 0: not done template matching for some errors, 1: done template matching
    int maxValInt; // maxVal of temaplate matching scaled 0 to 100
    int x;
    int y;
};

extern "C" CHKIMGCPP_API ReturnPoint __stdcall point_template_matched_bitmap(
    const wchar_t* class_name,
    const wchar_t* window_title,
    const wchar_t* bitmap_file_path
);

ChkImgCpp.cpp

#include "pch.h" // use stdafx.h in Visual Studio 2017 and earlier

#include <windows.h>
#include <dwmapi.h>

#include <string>

#include "ChkImgCpp.h"
#include <opencv2/opencv.hpp>

/**
 *  WindowDCを解放するためのdeleter
 * https://setuna-kanata.hatenadiary.org/entry/20100713/1279039787
 */
struct dc_deleter_release
{
    void operator()(HDC hdc)const
    {
        ReleaseDC(hwnd, hdc);
    }

    dc_deleter_release(HWND hwnd) : hwnd(hwnd) {}

    HWND hwnd;
};
struct dc_deleter_delete
{
    void operator()(HDC hdc)const
    {
        DeleteDC(hdc);
    }

    dc_deleter_delete() {}
};
struct hbitmap_deleter_delete
{
    void operator()(HBITMAP hBitmap)const
    {
        DeleteObject(hBitmap);
    }

    hbitmap_deleter_delete() {}
};

// https://gist.github.com/siwa32/98704
std::string convString(const std::wstring& input)
{
    size_t i;
    char* buffer = new char[input.size() * MB_CUR_MAX + 1];
    wcstombs_s(&i, buffer, input.size() * MB_CUR_MAX + 1, input.c_str(), _TRUNCATE);
    std::string result = buffer;
    delete[] buffer;
    return result;
}

ReturnPoint __stdcall point_template_matched_bitmap(
    const wchar_t* class_name,
    const wchar_t* window_title,
    const wchar_t* bitmap_file_path
)
{
    ReturnPoint r{};
    r.doneMatching = 0; // 0: not done template matching for some errors, 1: done template matching

    HWND hwnd = FindWindow(class_name, window_title);
    if (hwnd == NULL)
    {
        return r;
    }

    RECT rect{};
    if (DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &rect, sizeof(rect)) != S_OK)
    {
        return r;
    }

    // ReleaseDC 等を呼ばなくてすむように、std::unique_ptr を利用
    // https://setuna-kanata.hatenadiary.org/entry/20100713/1279039787
    std::unique_ptr<HDC__, dc_deleter_release> hdc(::GetDC(hwnd), dc_deleter_release(hwnd));

    // Bitmap to cv::Mat
    // http://ckron.net/?p=205
    BITMAPINFO bmpInfo{};
    bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmpInfo.bmiHeader.biWidth = rect.right - rect.left;
    bmpInfo.bmiHeader.biHeight = rect.bottom - rect.top;
    bmpInfo.bmiHeader.biPlanes = 1;
    bmpInfo.bmiHeader.biBitCount = 32;
    bmpInfo.bmiHeader.biCompression = BI_RGB;

    LPDWORD lpPixel{};
    std::unique_ptr<HBITMAP__, hbitmap_deleter_delete>
        hBitMap(CreateDIBSection(hdc.get(), &bmpInfo, DIB_RGB_COLORS, (void**)&lpPixel, NULL, 0), hbitmap_deleter_delete());
    if (hBitMap.get() != NULL)
    {
        // メモリDC
        std::unique_ptr<HDC__, dc_deleter_delete> hdcMem(CreateCompatibleDC(hdc.get()), dc_deleter_delete());

        SelectObject(hdcMem.get(), hBitMap.get());
        PrintWindow(hwnd, hdcMem.get(), PW_RENDERFULLCONTENT);
    }

    BITMAP bmp{};
    GetObject(hBitMap.get(), sizeof(BITMAP), &bmp);

    /*
   // for debugging bitmap data
   // http://www13.plala.or.jp/kymats/study/MULTIMEDIA/convert_dib_ddb.html
   if (OpenClipboard(hwnd))
   {
       HBITMAP hb;
       hb = CreateDIBitmap(hdc.get(), &bmpInfo.bmiHeader, CBM_INIT, lpPixel, &bmpInfo, DIB_RGB_COLORS);
       EmptyClipboard();
       SetClipboardData(CF_BITMAP, hb);
       CloseClipboard();
   }
   return r;
   */

    // PrintWindow で取得した画像は32bitカラーイメージだったので決め打ち
    cv::Mat revMat(bmp.bmHeight, bmp.bmWidth, CV_8UC4, (void*)bmp.bmBits);
    cv::Mat mat{};
    cv::flip(revMat, mat, 0);
    if (!mat.data)
    {
        return r;
    }

    // 比較ファイル
    cv::Mat target = cv::imread(convString(bitmap_file_path), 1);
    if (!target.data)
    {
        return r;
    }

    // template matching
    cv::Mat matBGR{};
    cv::cvtColor(mat, matBGR, cv::COLOR_BGRA2BGR);

    int result_cols = matBGR.cols - target.cols + 1;
    int result_rows = matBGR.rows - target.rows + 1;

    cv::Mat result{};
    result.create(result_rows, result_cols, CV_8UC3);

    cv::matchTemplate(matBGR, target, result, cv::TM_CCOEFF_NORMED);

    double minVal{};
    double maxVal{};
    cv::Point minLoc;
    cv::Point maxLoc;
    cv::Point matchLoc;

    minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, cv::Mat());
    matchLoc = maxLoc;

    r.doneMatching = 1; // 0: not done template matching for some errors, 1: done template matching
    r.maxValInt = maxVal * 100;
    r.x = matchLoc.x + target.cols / 2;
    r.y = matchLoc.y + target.rows / 2;

    /*
   //for debugging matched point
   cv::Mat img_display{};
   matBGR.copyTo(img_display);

   rectangle(img_display, matchLoc, cv::Point(matchLoc.x + target.cols, matchLoc.y + target.rows), cv::Scalar::all(0), 2, 8, 0);

   imshow("debugging matched point", img_display);
   cv::waitKey(0);
   //*/

    return r;
}

Powershell で呼び出す

# [ChkImgCpp]::point_template_matched_bitmap returns ReturnPoint for window (also class name) and bitmap_file_path.
# Return Point has
#  - int doneMatching; // 0: not done template matching for some errors, 1: done template matching
#  - int maxValInt; // maxVal of temaplate matching scaled 0 to 10000
#  - int x; // center x position of maxVal location
#  - int y; // center y position of maxVal location
$dllFilePathEscaped = (Join-Path -Path ${PSScriptRoot} -ChildPath "ChkImgCpp.dll").Replace('\', '\\')
$source = @"
using System.Runtime.InteropServices;
public struct ReturnPoint {
    public int doneMatching; // 0: not done template matching for some errors, 1: done template matching
    public int maxValInt; // maxVal of temaplate matching scaled 0 to 100
    public int x;
    public int y;
}
public static class ChkImgCpp
{
    [DllImport("${dllFilePathEscaped}", CharSet=CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
    public static extern ReturnPoint point_template_matched_bitmap(
        System.String class_name,
        System.String window_title,
        System.String bitmap_file_path
    );
}
"@
Add-Type -TypeDefinition $source;

$local:class_name_of_window = "<ウィンドウの class name: UWSC で取得が簡単>"
$local:title_of_window = "<ウィンドウの タイトル>"
$local:bmp_relative_path_non_escaped = "<比較するbmp file (24bit) の相対パス>"

$local:bmpFilePathEscaped = (Join-Path -Path ${PSScriptRoot} -ChildPath $bmp_relative_path_non_escaped).Replace('\', '\\')
$returnPoint = [ChkImgCpp]::point_template_matched_bitmap($class_name_of_window, $title_of_window, $bmpFilePathEscaped)

Write-Output $returnPoint  

つまづいた点

  • dll、lib、exe の違いって何?
  • ヘッダファイル、ソースファイルって何?
  • dll ってどこにある?どこにおく?
  • ビルドすると何が出力される?dll だけ?lib、exe も?
  • C++ のstring って何?どうやって関数に渡すの?ポインタの概念はなんとなくわかるけど・・
  • GetDC のあとは必ずReleaseDC しろって、何かいい方法はないの?
  • Win32API のBITMAP 型って何?save(bmp, filePath) で保存じゃないの?

参考

メモ:IRKit のサービス終了に伴う、ファームウェアの書換

IRKit のサービスが終了したので、 サーバーに接続せずにローカルのみで使うよう、 Adrian さんの fork settings-led-cloud を、IRKit に書き込んでWifi のセットアップ、リモコン信号をIRKit から送信まで確認したときのメモです。

なお、この記事は私の環境では上手くいったということを参考までにお知らせするものです。

この記事を参考に、ご自分でファームウェアのアップデートされる方に生じた何らかの損失、損害等について、 私は一切責任も負うものではありません。

以下、目次

手順

  1. Adrian さんの step-by-step guide を参考に IRKit に settings-led-cloud バージョンのファームウェアを書き込み
  2. IRKit の ssid に接続し、wifi セットアップ用の Post リクエストを送信
  3. リモコン信号がIRKit から送信されていることを確認

手順を進めるうえではまったところ

Adrian さんの step-by-step guide を参考に IRKit に settings-led-cloud バージョンのファームウェアを書き込み

IRKit の ssid に接続し、wifi セットアップ用の Post リクエストを送信

wifi 情報を serialized したものを Post する必要があるが、javascript でどうやってするのかはリンク切れのよう

なので、bouzuya さんのnode-irkit を参考にさせてもらった。

javascript で serialized wifi 情報を出力するコードは以下のとおり。

"use strict";
// Original Source Code: http://jsdo.it/mash/keyserializer-test
// MIT License

// This Source Code is transpiled from https://github.com/bouzuya/node-irkit/blob/master/src/irkit-device-key-serializer.ts typescript
// to javascript, and add a little change.

// CRC-8-Dallas/Maxim
function crc8n(u8, crcinit) {
    if (u8 < 0x00 || u8 > 0xFF) {
        throw new Error('only input less than 1byte as number');
    }
    var crc = crcinit ^ u8;
    for (var i = 0; i < 8; i++) {
        var poly = (crc & 0x80) === 0x80;
        crc = (crc << 1) ^ (poly ? 0x31 : 0x00); // 0x31 = X^8+X^5+X^4+X^0
    }
    return crc & 0xFF;
}
// tslint:enable
function crc8s(data, size, crcinit) {
    var crc = crcinit;
    for (var i = 0; i < size; i++) {
        var u8 = i < data.length ? data.charCodeAt(i) : 0x00; // 0 filled
        crc = crc8n(u8, crc);
    }
    return crc;
}
function hex(s) {
    var ret = '';
    for (var i = 0; i < s.length; i++) {
        ret += s.charCodeAt(i).toString(16).toUpperCase();
    }
    return ret;
}
function serializeSecurity(security) {
    return security.toString();
}
function serializeSSID(ssid) {
    return hex(ssid);
}
function serializePassword(password) {
    return hex(password);
}
function serializeWEPPassword(password) {
    return serializePassword(hex(password));
}
function serializeDevicekey(devicekey) {
    return devicekey;
}
var Security;
(function (Security) {
    Security[Security["NONE"] = 0] = "NONE";
    Security[Security["WEP"] = 2] = "WEP";
    Security[Security["WPA_WPA2"] = 8] = "WPA_WPA2";
})(Security || (Security = {}));

function serializeCRC(obj) {
    var crc = 0x00;
    crc = crc8n(obj.security, crc);
    crc = crc8s(obj.ssid, 33, crc);
    crc = crc8s(obj.password, 64, crc);
    crc = crc8n(1, crc); // wifi_is_set
    crc = crc8n(0, crc); // wifi_was_valid
    crc = crc8s(obj.devicekey, 33, crc);
    return crc.toString(16).toUpperCase();
}
function serialize(_a) {
    var devicekey = _a.devicekey, password = _a.password, security = _a.security, ssid = _a.ssid;
    return [
        serializeSecurity(security),
        serializeSSID(ssid),
        security === Security.WEP
            ? serializeWEPPassword(password)
            : serializePassword(password),
        serializeDevicekey(devicekey),
        '2',
        '',
        '',
        '',
        '',
        '',
        serializeCRC({
            devicekey: devicekey,
            password: password,
            security: security,
            ssid: ssid
        })
    ].join('/');
}

// ここに自分のwifi 設定を入力する
let obj = {
    devicekey: "", // ブランクでOK
    password: "", // home wifi passward (not IRKit wifi password)
    security: 8, // WPA_WPA2:8, WEP: 2
    ssid: "" // wifi ssid (not IRKit wifi ssid)
}
//

let serializedObj = serialize(obj);
console.log({ serializedObj });

例えば、ssid: test_ssid, password: test_password, security: WPA_WPA2 の場合、 以下のようなPost リクエストになるはず。

curl -X POST -i "http://192.168.1.1/wifi" -H "X-Requested-With: curl" -d '8/746573745F73736964/746573745F70617373776F7264//2//////F9'

参考リンク

Ubuntu 22.04 (Oracle Cloud Infrastructure) の Play Framework に http (port: 80) でアクセスしたい

Play Framework を始めたばかりの方向けに、サンプルを進めるうえで困ったところを共有する目的で書いています。

Oracle Cloud Infrastructure の Ubuntu 22.04 (以下、ubuntu) 内で Play Framework 2.8.x (以下、play) を実行し、http (port: 80) で "Welcome to Play!" ページにアクセスしたときのメモです。

以下、目次

やりたいこと

ubuntu で play を実行し、http://グローバル IP にアクセスする (port: 80 でアクセスする) と、"Welcome to Play!" ページが表示される(ただし、play の待ち受けポートは 9000 の default まま)。

方針

ブラウザから ubuntu 内の play まで、http でアクセスする際、許可を与える必要があるのは以下の設定であった。

ので、以下の設定を一つずつ許可にしていく。

  • Oracle Cloud Infrastructure の ネットワーク・セキュリティ・グループ 内セキュリティイングレスルール」として、『ポート80 の許可』
  • ubuntuiptables ルール」として、『ポート80 からポート9000 へリダイレクト』、『ポート9000 のリダイレクトのリクエストのみ許可』
  • 「play 内 の AllowedHostsFilter 設定」として、『グローバルIP でのhttp リクエスト許可』

詳細

Oracle Cloud Infrastructure の ネットワーク・セキュリティ・グループ 内セキュリティイングレスルール」として、『ポート80 の許可』

default ままだと、「ネットワーク・セキュリティ・グループ」または「(設定があれば)サブネットのセキュリティリスト」には、

  • ポート22 でのSSH Remote Login イングレスルール
  • タイプ3, 4 の ICMP イングレスルール
  • すべてのプロトコル、およびすべてのポートのエグレスルール

あたりが許可されていた。

ので、「ネットワーク・セキュリティ・グループ」に『ポート80 でのTCP トラフィック イングレスルール』を追加した(参考: OracleCloudよりポート許可の追加)。

ubuntuiptables ルール」として、『ポート80 からポート9000 へリダイレクト』、『ポート9000 のリダイレクトのリクエストのみ許可』

# ポート80 からポート9000 へリダイレクト
sudo iptables -t nat -A PREROUTING -m comment --comment "for http port forward" -p tcp --dport 80 -j REDIRECT --to-port 9000
# 設定行は、ssh 用設定行の次にしとこう
INDEX_INSERT=$(sudo iptables -L --line-numbers | grep 'state NEW tcp dpt:ssh' | awk '{print $1 + 1;}')
# ポート9000 のリダイレクトのリクエストのみ許可
sudo iptables -t filter -I INPUT "$INDEX_INSERT" -p tcp --dport 9000 -m conntrack --ctstate DNAT -j ACCEPT
# iptables の設定保存
sudo netfilter-persistent save

なお、sudo netfilter-persistent reload あるいは、iptables-save を呼ぶと、iptables の filter rules が重複して登録された。 これのせい?iptables-save duplicates libvirt and ufw rules on iptables-restore with iptables-persistent

よくわからないし、対処方法も調べていないので、上では明示的にsudo netfilter-persistent reload していない。

sudo reboot 時には、保存した内容のみ登録されたので、回避方法はsudo netfilter-persistent reload しない。

「play 内 の AllowedHostsFilter 設定」として、『グローバルIP でのhttp リクエスト許可』

play.filters.hosts {
  # Allow requests to example.com, its subdomains, and localhost:9000.
  # and add Global IP (ex.: 203.0.113.0)
  allowed = [".example.com", "localhost:9000", "203.0.113.0"]

補足

  • play の待ち受けポートは 9000 の default ままとしたのは、80 を割り当てようとするとroot 権限が必要らしいので
  • 「セキュリティ・リスト」と「ネットワーク・セキュリティ・グループ」は、同じ設定を登録でき、役割も重なっている部分がある。「ネットワーク・セキュリティ・グループ」でできることは、「ネットワーク・セキュリティ・グループ」を使用することを推奨するらしい。セキュリティ・リストとネットワーク・セキュリティ・グループの比較

参考にしたところ

以上でした。

【Play Framework 2.8.x サンプル】開発環境をrun 起動時に、https および http2 でアクセス可能にしたい

Play Framework を始めたばかりの方向けに、サンプルを進めるうえで困ったところを共有する目的で書いています。

Play Framework 2.8.16 にて、run 起動時に、https および http2 でアクセス可能としたときのメモです。

以下、目次

前提

方針

  • ローカル環境に対して https および http2 でアクセス(ローカル開発環境の https 化 を参考)
  • openssl, keytool を使って、certificate items を jks に変換
  • sbt run -D(options) で play を実行

結果

できました play-tls-test

補足

sbt run -D(options) 実行時、以下の WARNING がでます。

WARNING: An illegal reflective access operation has occurred
WARNING: Please consider reporting this to the maintainers of scala.reflect.package$
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

そのため、Java 17 では https でアクセスできないようでした。 play 2.9 では解決するよう?

参考にしたところ

以上でした。