2022年7月30日 星期六

Go 開發筆記 - 透過 Electron 實現 PC app GUI 之 go-astilectron / go-astilectron-bundler / go-astilectron-bootstrap


前陣子練習 Golang 後,除了建制後端 API 及網頁服務外,在想該怎樣做個 Windows / macOS 的 PC App 時,逛了一下熱門的 GUI 套件,有常見的 QT, GTK3/GTK4 等等,最後想起 Electron 這套,找了一下果真也有人串好 Go 與 Electron 整合方式。就來試試看 go-astilectron 這套吧!

處理的項目:
專案初始化:

% cat main.go 
package main

import (
    "github.com/asticode/go-astilectron"
)

func main() {

}

% go mod init github.com/changyy/study-go-electron
% go mod tidy

接著切一個目錄 web 做網頁開發管理:

% tree -L 1 web
web
├── README.md
├── dist
├── env_nvm.sh
├── env_vim.sh
├── node_modules
├── package-lock.json
├── package.json
├── src
├── webpack.common.js
├── webpack.development.js
└── webpack.production.js

3 directories, 8 files

其中 package.json 內使用 webpack 安置開發模式和發版封裝、使用 eslint 做 coding style 規範:

% cat web/package.json | jq '.scripts'
{
  "eslint": "eslint --ext .js src/",
  "eslint-fix": "eslint --fix --ext .js src/",
  "watch": "webpack --config webpack.development.js --watch",
  "start": "webpack serve --config webpack.development.js --open",
  "build": "webpack --config webpack.production.js"
}

而 web 程式碼入口點:

% tree web/src     
web/src
├── index.html
└── js
    ├── helper-old-style.js
    ├── helper.js
    └── index.js

1 directory, 4 files

試試看在 web/src/index.html 內,在 body.onload 內呼叫 hehe(),而 web/src/js/index.js 是一個統整區,而 webpack 在 production mode 會精簡程式碼,因此要匯出的功能都必須做點處理,像是故意埋在 windows.xx 等等,或是要調整 webpack 包裝的機制,如 output 區要多描述 libraryTarget 和 library 資訊,會自動綁定在 window.xx 環境。

回過頭來,關於 go astilectron 的使用,在不需要封裝成各平台的環境時,可以很簡單地用以下程式碼,並在根目錄用 `% go run .` 就可以運行了:

    a, _ := astilectron.New(log.New(os.Stderr, "", 0), astilectron.Options{
        AppName: "MyGoAstilectronProject",
    })
    defer a.Close()

    // Start astilectron
    a.Start()

    w, _ := a.NewWindow("./resources/app/index.html", &astilectron.WindowOptions{
        Center: astikit.BoolPtr(true),
        Height: astikit.IntPtr(600),
        Width:  astikit.IntPtr(600),
    })
    w.Create()

    // Blocking pattern
    a.Wait()

如此,在開發網頁版型時,單純在 web 目錄做事,如 npm run start 就可以喚起瀏覽器來瀏覽,等做完事後,在靠 npm run build 封裝到 web/dist 目錄以及複製一份到 resource/app 目錄中,後續就著靠 ~/go/bin/astilectron-bundler 封裝成 PC App,在封裝成 PC app 時,會面臨到 resources 目錄內的文件也得跟著封裝進去,也就是上述程式碼指定的 "./resources/app/index.html" 網頁資源,或者網頁位址要改成絕對路徑也是一招,但等同也要求使用此 PC app 者,下需要自行配置好網頁資料。

這時,就要使用 go-astilectron-bundler 跟 go-astilectron-bootstrap ,前者是編譯出 PC app ,後者是封裝好資源,使得執行 PC app 時,可以順道把對應的資源("./resources/app/")擺定位,這時程式碼就會變得稍微複雜一點,並且執行執行 `go run .` 不見得能常運行,將使得開發週期拉的很長(每次編譯要花幾分鐘的時間)

% ~/go/bin/astilectron-bundler cc
% time ~/go/bin/astilectron-bundler
...
~/go/bin/astilectron-bundler  63.79s user 10.56s system 108% cpu 1:08.21 total

最後,就把他配置成可以傳一個 -dev 參數,使用該參數時,不經過 ~/go/bin/astilectron-bundler 運行(運行的速度也只是小減 20% 時間而已 XD)

% go run . -dev

目前覺得用 Go 與 Electron framework 一起使用的好處還不夠強烈,以及整合開發的優勢頂多是用 Go 寫工具(api)等等,以及使 Electron 時,本來就是重度倚賴 HTML/JS/CSS 在 UI 呈現,因此直接 node.js 的整合優勢仍是最佳的。未來挑選這樣的價格

更多資訊就在這邊:github.com/changyy/study-go-electron ,或是直接觀看 github.com/asticode/go-astilectron 內有提到 demo 範例,可以直接看 demo 內的實作。

2022年7月26日 星期二

[NAS] Private IP / Custom Domain / Wildcard SSL / HTTPS 憑證定期更新與設定 @ DS216play / DSM 7.1



之前在 2018 年已經寫過一篇:[NAS] Private IP Server 與 Let's Encrypt Wildcard SSL 憑證 / HTTPS 服務,那時是採用 Let's Encrypt Wildcard SSL 憑證,主因是只有 Private IP 的 NAS Server ,無法讓憑證服務連進來驗證 IP 與 Domain 的關係,因此主要都是靠 DNS Record (TXT Record) 的驗證方式。

然而,忘了是何時開始,原本設定好的 acme.sh 跟連續動作 script 失效好一陣子,後來才發現在 2021.08.01 起,預設已經改成 ZeroSSL 方案,所以要處理創帳號的部分:

$ ~/.acme.sh/acme.sh --upgrade
$ ~/.acme.sh/acme.sh --register-account -m YOUR_EMAIL
[Sun May 22 13:26:39 CST 2022] No EAB credentials found for ZeroSSL, let's get one
[Sun May 22 13:26:41 CST 2022] Registering account: https://acme.zerossl.com/v2/DV90
[Sun May 22 13:26:55 CST 2022] Registered
[Sun May 22 13:26:55 CST 2022] ACCOUNT_THUMBPRINT='XXXXXXXXXXXXXXXXXXXXXXX'

運行:

$ CF_Key=YOUR_KEY CF_Email=YOUR_EMAIL ~/.acme.sh/acme.sh --issue --dns dns_cf -d *.YOUR_DOMAIN

如此,在 ~/.acme.sh\*.YOUR_DOMAIN/ 中,會有:
  • ~/.acme.sh\*.YOUR_DOMAIN/\*.YOUR_DOMAIN.cer
  • ~/.acme.sh\*.YOUR_DOMAIN/\*.YOUR_DOMAIN.key
  • ~/.acme.sh\*.YOUR_DOMAIN/ca.cer
  • ~/.acme.sh\*.YOUR_DOMAIN/fullchain.cer
接著把這些 copy 出來,直接到 NAS DSM7.1 網頁介面匯入

控制台 -> 安全性 -> 憑證 -> 新增 -> 匯入憑證 ->

私鑰: Your cert key - \*.YOUR_DOMAIN.key
憑證:  Your cert - \*.YOUR_DOMAIN.cer
中繼憑證: ca.cer

控制台 -> 安全性 -> 憑證 -> 設定 -> 系統預設 -> 更換成剛剛上傳的憑證(如 *.YOUR_DOMAIN)

 

如此,再去系統 /usr/syno/etc/www/certificate/system_default/cert.conf 查一下資訊,可以看到寫死的憑證位置,未來就可以靠手段去覆蓋掉。

取代掉 ssl_certificate 欄位,如 /usr/syno/etc/www/certificate/system_default/XXX-SC-XXX.pem;

$ cat ~/.acme.sh/\*.YOUR_DOMAIN/\*.YOUR_DOMAIN.cer ~/.acme.sh/\*.YOUR_DOMAIN/ca.cer > ~/.acme.sh/\*.YOUR_DOMAIN/\*.YOUR_DOMAIN.fullchain.pem
$ sudo cp ~/.acme.sh/\*.YOUR_DOMAIN/\*.YOUR_DOMAIN.fullchain.pem /usr/syno/etc/www/certificate/system_default/XXX-SC-XXX.pem;

取代掉 ssl_certificate_key,如 /usr/syno/etc/www/certificate/system_default/XXX-SCK-XXX.pem;

$ openssl pkcs8 -topk8 -inform pem -in ~/.acme.sh/\*.YOUR_DOMAIN/\*.YOUR_DOMAIN.key -outform pem -nocrypt -out ~/.acme.sh/\*.YOUR_DOMAIN/\*.YOUR_DOMAIN.key.pem
$ sudo cp ~/.acme.sh/\*.YOUR_DOMAIN/\*.YOUR_DOMAIN.key.pem /usr/syno/etc/www/certificate/system_default/XXX-SCK-XXX.pem;

測試與重啟 Nginx

$ sudo nginx -t && sudo synopkg restart --service nginx

上述的任務都可以擠在一個 script 中,再靠 NAS Web UI 設定定期跑,而 NAS 只需在 Web UI 上設定好一次使用指定的憑證資訊後,後續就可以靠系統底層不斷覆蓋重啟 nginx 方式來處理,像是有需要的話,也可以靠 gawk 去動態取出 ssl_certificate_key 和 ssl_certificate 的位置

ssl_certificate_path=`sudo grep "ssl_certificate " /usr/syno/etc/www/certificate/system_default/cert.conf | gawk -F';' '{print $1}' | gawk -F' ' '{print $2}'`

ssl_certificate_key_path=`sudo grep "ssl_certificate_key " /usr/syno/etc/www/certificate/system_default/cert.conf | gawk -F';' '{print $1}' | gawk -F' ' '{print $2}'` 

echo "ssl_certificate_path: $ssl_certificate_path"

echo "ssl_certificate_key_path: $ssl_certificate_key_path"


ref: 

2022年7月20日 星期三

PHP 開發筆記 - 使用 Google Analytic (GA4) Measurement Protocol

使用 GA Measurement Protocol 大概也有超過五年了 XD 最近一直收到通報,說舊版 GA Measurement Protocol 即將在 2023.07.01 不在接收資料處理,簡稱看不到報表。

建立一個 GA4 專案後,發現要在使用 Measurement Protocol 的難度有稍微增加。包括測試也有點卡卡 XD 最近就記錄一下筆記:
首先,想要模擬個 page_view 時,才發現官方文件沒有多提,後來大概是組出這樣:

{
    "client_id":"00000000",
    "non_personalized_ads":true,
    "events":[
        {
            "name":"page_view",
            "params":{
                "page_title":"hello",
                "page_location":"blog.changyy.org"
            }
        }
    ]
}

而 PHP Code:

function _GA4_($api_secret, $measurement_id, $deviceId, $deviceIP, $events = array()) {
    $query_string = array(
        'measurement_id' => $measurement_id,
        'api_secret' => $api_secret,
    );  
    if (!empty($deviceIP)) {
        // https://support.google.com/analytics/answer/9143382
        // https://github.com/dataunlocker/save-analytics-from-content-blockers/issues/25
        $query_string['uip'] = $deviceIP;
        $query_string['_uip'] = $deviceIP;
    }   
    $url = 'https://www.google-analytics.com/mp/collect?'.http_build_query($query_string);
    //$url = 'https://www.google-analytics.com/debug/mp/collect?'.http_build_query($query_string);

    //  post without libcurl
    // https://ga-dev-tools.web.app/ga4/event-builder/
    $payload = array(
        'client_id' => $deviceId,
        'non_personalized_ads' => true,
        //'timestamp_micros' => "".floor(microtime(true) * 1000)."000",
        'events' => $events,
    );

    //if (isset($_SERVER) && isset($_SERVER['COUNTRY_CODE']) && !empty($_SERVER['COUNTRY_CODE']) ) {
    //        if (!isset($payload['user_properties']))
    //                $payload['user_properties'] = array();
    //        $payload['user_properties']['country_code'] = array(
    //                'value' => $_SERVER['COUNTRY_CODE'],
    //        );
    //}

    $POST_DATA = json_encode($payload);
    $options = array(
        'http' => array(
            'header' => implode("\r\n", array(
                "Content-Type: application/json",
                //"Content-Length: ".strlen($POST_DATA),
            )), 
            'method' => 'POST',
            'content' => $POST_DATA,
        )   
    );  
    $context  = @stream_context_create($options);
    $result = @file_get_contents($url, false, $context);
    //echo "result:[$result]\n$POST_DATA\n".print_r($options)."\n";exit;
}

如此,就可以用以下完成事件回報了:

_GA4_($api_secret, $measurement_id, $deviceId, $deviceIP, array(
    array(
        'name' => 'page_view',
        'params' => array(
            'page_title' => 'Hello',
            'page_location' => 'https://blog.changyy.org/',
        ),
    )
);

而使用 https://www.google-analytics.com/debug/mp/collect 驗證時,有錯誤可以在 http response body 看到資料,例如:

{
  "validationMessages": [ {
    "description": "Unable to parse Measurement Protocol JSON payload. : invalid value Cannot bind a list to map for field 'user_properties'. for type Map",
    "validationCode": "VALUE_INVALID"
  } ]
}

2022年7月7日 星期四

Node.js 開發筆記 - 模擬 Web Browser 環境,執行前端 JS code / library

最近協助同事處理一個棘手的事,把一段需要算力的 JS Code 從本地端改成雲端運行,首要碰到的就是 node.js 預設環境並沒有很適合,如果有 JS library 十分依賴 Web Browser 環境時,那段 js code 透過 eval 運行時,會出現初始化錯誤。

原本土炮零碎地拼湊出 window, document, location, hostname 等把環境弄的堪用,後來還是先找一下,找到 browser-env 這套件,非常不錯!套上去後,所有土炮破壞架構美感的東西都可以去掉了。

如此,如果要將算力很高的任務拋去雲端計算時,且不幸依賴很多常見的 Web browser 套件邏輯時,就可以如此封裝成一隻 api ,供人把算力高的任務拋去高速運算的機器處理即可。

註:此例用 eval 是個危險的用法,只適合運行信任的程式碼,亂給使用者填寫會有高資安風險的。


範例:
  • 故意在 node.js 植入前端 jQuery library - https://code.jquery.com/jquery-3.6.0.slim.min.js (實務上真的有需求時,是要找 node.js 套件)
  • 在 node.js 透過 eval 啟用整段程式碼時,缺少 web browser 環境時,會噴錯誤訊息:TypeError: Cannot read properties of undefined (reading 'createElement')