|
2025-11-04
2025-11-04
概要 †PythonのWebアプリのデスクトップアプリ化(改良版)で作成したwebviewの部分をGoで書き直して見る。 やっている事は上記の記事(のpython版のwebview)とほぼ同じ
目次 †
ソース †go.mod module go_webview_sample go 1.23.4 require ( github.com/pelletier/go-toml/v2 v2.2.4 github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 ) main.go package main
import (
"fmt"
"log"
"net"
"os"
"os/exec"
"regexp"
"runtime"
"strings"
"time"
"github.com/pelletier/go-toml/v2"
webview "github.com/webview/webview_go"
)
// 許可するサーバー実行ファイルの証明書サムプリント
var ALLOWED_THUMB_PRINTS = []string{
"81CD829A58A13480C5...", // 実行を許可するexeの証明書サムプリント
}
type ServerInfo struct {
URL string
Host string
Port string
Path string
Env map[string]string
TempDir string
Cmd *exec.Cmd
TimeoutSec int
}
// サーバー実行ファイル名取得
func getServerExecutableName() string {
fileName := "myapp_server"
if runtime.GOOS == "windows" {
fileName += ".exe"
}
return fileName
}
// サーバー実行ファイル 及び 設定ファイルのパス取得
func getServerPath(path string) (string, []string) {
// サーバー実行ファイルのパス取得
serverPath := getServerExecutablePath(path)
// 設定ファイルのパス取得
configFilePath := getServerConfigPaths(serverPath)
return serverPath, configFilePath
}
// サーバー実行ファイルのパス取得
func getServerExecutablePath(path string) string {
if path != "" {
if _, err := os.Stat(path); err == nil {
return path
}
return path
}
if cwd, err := os.Getwd(); err == nil {
result := fmt.Sprintf("%s/%s", cwd, getServerExecutableName())
if _, err := os.Stat(result); err == nil {
return result
}
}
return ""
}
// 設定ファイルのパス取得
// 以下の順に設定ファイル(toml)を読み込み環境変数を取得する(後勝ち)
// 1. ホームディレクトリの .streamlit/config.toml
// 2. exe と同じディレクトリの .streamlit/config.toml
// または カレントディレクトリの .streamlit/config.toml (開発/テスト用)
func getServerConfigPaths(path string) []string {
if path == "" {
return []string{}
}
configPaths := []string{}
if homeDir, err := os.UserHomeDir(); err == nil {
filePath := fmt.Sprintf("%s/.streamlit/config.toml", homeDir)
if _, err := os.Stat(filePath); err == nil {
fmt.Printf("Found config file in home directory: %s\n", filePath)
configPaths = append(configPaths, filePath)
}
}
exeDir := ""
if absPath, err := exec.LookPath(path); err == nil {
exeDir = strings.TrimSuffix(absPath, "/"+regexp.MustCompile(`[^/]+$`).FindString(absPath))
} else {
exeDir = "."
}
filePath := fmt.Sprintf("%s/.streamlit/config.toml", exeDir)
if _, err := os.Stat(filePath); err == nil {
fmt.Printf("Found config file in executable directory: %s\n", filePath)
configPaths = append(configPaths, filePath)
} else {
// 開発/テスト用にカレントディレクトリも確認
if cwd, err := os.Getwd(); err == nil {
filePath := fmt.Sprintf("%s/.streamlit/config.toml", cwd)
if _, err := os.Stat(filePath); err == nil {
fmt.Printf("Found config file in current working directory: %s\n", filePath)
configPaths = append(configPaths, filePath)
}
}
}
return configPaths
}
// 設定ファイルを読み込み環境変数として取得する
func loadServerEnv(configFilePath []string) map[string]string {
serverEnv := map[string]string{}
for _, path := range configFilePath {
// ファイルが存在しなければスキップ
if _, err := os.Stat(path); os.IsNotExist(err) {
continue
}
// ファイル読み込み 及び パース
docBytes, err := os.ReadFile(path)
if err != nil {
panic(err)
}
doc := string(docBytes)
var cfg map[string]interface{}
err = toml.Unmarshal([]byte(doc), &cfg)
if err != nil {
panic(err)
}
fmt.Printf("Loaded config file: %s\n", path)
for k, v := range cfg {
envName := fmt.Sprintf("STREAMLIT_%s", strings.ToUpper(strings.ReplaceAll(k, ".", "_")))
// 値がmapでない場合は無視
if _, ok := v.(map[string]interface{}); !ok {
continue
}
// キー名をキャメルケースから大文字スネークケースに変換して環境変数名を作成
for k2, v2 := range v.(map[string]interface{}) {
envVarName := fmt.Sprintf("%s_%s", envName, strings.ToUpper(regexp.MustCompile(`([a-z0-9])([A-Z])`).ReplaceAllString(k2, "${1}_${2}")))
envVarValue := fmt.Sprintf("%v", v2)
serverEnv[envVarName] = envVarValue
}
}
}
return serverEnv
}
// サーバーURLからホストとポートを抽出する
func getServerInfo(url string, path string) ServerInfo {
serverUrl := url
// サーバー実行ファイルのパス取得
serverPath, configPath := getServerPath(path)
// 設定ファイルを読み込み
serverEnv := loadServerEnv(configPath)
var host, port string
if _, exist := serverEnv["STREAMLIT_SERVER_PORT"]; exist {
host = "localhost" // 設定ファイルでポートが指定されている場合はホストを localhost に固定
port = serverEnv["STREAMLIT_SERVER_PORT"]
serverUrl = fmt.Sprintf("http://%s:%s", host, port)
} else {
// urlの頭から http:// または https:// を削除
parsedUrl := regexp.MustCompile(`^https?://`).ReplaceAllString(serverUrl, "")
// / 以降を削除
hostAndPort := regexp.MustCompile(`/.*`).ReplaceAllString(parsedUrl, "")
// ホストとポートを分割
hostParts := strings.Split(hostAndPort, ":")
if len(hostParts) == 2 {
host = hostParts[0]
port = hostParts[1]
} else {
host = hostAndPort
if strings.HasPrefix(url, "https") {
port = "443"
} else {
port = "80"
}
}
}
fmt.Printf("Server Info - Host: %s, Port: %s\n", host, port)
if serverPath != "" {
fmt.Printf("Server Executable Path: %s\n", serverPath)
for k, v := range serverEnv {
fmt.Printf(" Env: %s=%s\n", k, v)
}
}
return ServerInfo{URL: serverUrl, Host: host, Port: port, Path: serverPath, Env: serverEnv, TimeoutSec: 30}
}
// 一時ディレクトリ作成
func createTempDir() string {
tempDir, err := os.MkdirTemp("", "server_tmp_*")
if err != nil {
log.Fatal("Failed to create temp directory:", err)
}
fmt.Printf("Created temp directory: %s\n", tempDir)
return tempDir
}
// サーバープロセス起動
func startServerProcess(w webview.WebView, info *ServerInfo) {
fmt.Println("Using Server executable:", info.Path)
// 一時ディレクトリ作成
info.TempDir = createTempDir()
// サーバープロセス起動準備
cmd := exec.Command(info.Path)
cmd.Stdout = nil
cmd.Stderr = nil
// 子プロセスの環境変数に TMP/TEMP を設定
cmd.Env = append(os.Environ(),
"TMP="+info.TempDir,
"TEMP="+info.TempDir,
"TMPDIR="+info.TempDir,
)
// 設定ファイルから取得した環境変数を追加
for k, v := range info.Env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
}
// プロセスグループ設定・起動
setupProcessGroup(cmd)
info.Cmd = cmd
}
// 指定ポートが開放されるまで待機
func waitForPort(info ServerInfo) bool {
timeoutSec := info.TimeoutSec
fmt.Printf("Waiting for port %s:%s to be available...\n", info.Host, info.Port)
addr := net.JoinHostPort(info.Host, info.Port)
start := time.Now()
for {
conn, err := net.Dial("tcp", addr)
if err == nil {
conn.Close()
fmt.Printf("server is ready at %s\n", addr)
return true
}
if time.Since(start).Seconds() > float64(timeoutSec) {
fmt.Printf("server did not start within %d seconds\n", timeoutSec)
return false
}
time.Sleep(300 * time.Millisecond)
}
}
// サーバー起動を待ち、成功でNavigate / 失敗でタイムアウト画面
func startWait(w webview.WebView, info ServerInfo) {
fmt.Printf("Waiting for server at %s:%s ...\n", info.Host, info.Port)
go func() {
if waitForPort(info) {
w.Dispatch(func() { w.Navigate(info.URL) })
} else {
w.Dispatch(func() { w.SetHtml(timeoutHTML()) })
}
}()
}
// サーバープロセス終了時の後処理
func serverPostProcess(info *ServerInfo) {
fmt.Println("serverPostProcess")
fmt.Printf("TempDir: %s\n", info.TempDir)
killProcessTree(info.Cmd)
// 一時ディレクトリが残っていれば削除
if info.TempDir != "" {
if _, err := os.Stat(info.TempDir); err == nil {
_ = os.RemoveAll(info.TempDir)
}
}
}
func main() {
// 引数取得
serverUrl := "http://localhost:8501"
serverExePath := ""
if len(os.Args) >= 2 {
serverUrl = os.Args[1]
}
if len(os.Args) >= 3 {
serverExePath = os.Args[2]
}
// WebView 起動: まずは起動中メッセージを表示
debug := true
w := webview.New(debug)
defer w.Destroy()
w.SetTitle("Sample WebView")
w.SetSize(800, 600, webview.HintNone)
//setupWindowStyle(w) // タイトルバー色などの調整
w.SetHtml(loadingHTML())
// サーバー情報取得
info := getServerInfo(serverUrl, serverExePath)
// [起動] 押下時の関数をバインド(JS から window.forceStartApp() で呼び出し可)
_ = w.Bind("forceStartApp", func() {
//w.SetHtml(loadingHTML())
w.Dispatch(func() { w.SetHtml(loadingHTML()) })
startServerProcess(w, &info)
startWait(w, info)
})
// [再試行] 押下時の関数をバインド(JS から window.reloadApp() で呼び出し可)
_ = w.Bind("reloadApp", func() {
//reloadApp(w, info)
w.Dispatch(func() { w.SetHtml(loadingHTML()) })
startWait(w, info)
})
go func() {
// サーバーexe証明書チェック
isCertified := false
if info.Path != "" {
isCertified = checkServerCertificate(info.Path)
}
if isCertified {
// サーバプロセス起動
startServerProcess(w, &info)
startWait(w, info)
} else if info.Path != "" && !isCertified {
// 証明書エラー画面表示
w.Dispatch(func() { w.SetHtml(certificateErrorHTML()) })
} else {
startWait(w, info)
}
}()
// プロセス終了時は子プロセスも終了
defer serverPostProcess(&info)
// イベントループ開始
w.Run()
}
webview_contents.go package main
// 起動中に表示する簡易HTML
func loadingHTML() string {
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
text-align: center;
margin-top: 20%;
}
.loading {
position: relative;
}
.loading::after {
content: "";
display: inline-block;
width: 30px;
text-align: left;
animation: dots 1.5s steps(3, end) infinite;
}
@keyframes dots {
0% { content: ""; }
25% { content: "."; }
50% { content: ".."; }
75% { content: "..."; }
}
</style>
</head>
<body>
<h1 class="loading">アプリケーションを起動しています</h1>
</body>
</html>`
}
// タイムアウト時に表示する簡易HTML
func timeoutHTML() string {
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { text-align: center; margin-top: 20%; }
.msg { color: #b00; }
.btn { margin-top: 16px; padding: 8px 16px; font-size: 14px; }
</style>
</head>
<body>
<div>
<h1 class="msg">内部サーバーが規定時間内に起動しませんでした</h1>
<button id="retryBtn" class="btn">再試行</button>
</div>
<script>
(function(){
var btn = document.getElementById('retryBtn');
if(btn){
btn.addEventListener('click', function(){
btn.disabled = true; btn.textContent = '再試行中…';
if(window.reloadApp){ window.reloadApp(); }
});
}
})();
</script>
</body>
</html>`
}
func certificateErrorHTML() string {
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { text-align: center; margin-top: 20%; }
.msg { color: #b00; }
.btn { margin-top: 16px; padding: 8px 16px; font-size: 14px; }
</style>
</head>
<body>
<div>
<h2 class="msg">内部サーバ証明書が存在しないか許可された証明書と一致しません</h2>
<div>このまま起動しますか?</div>
<button id="forceStartBtn" class="btn">起動する</button>
</div>
<script>
(function(){
var btn = document.getElementById('forceStartBtn');
if(btn){
btn.addEventListener('click', function(){
btn.disabled = true; btn.textContent = '起動中...';
if(window.forceStartApp){ window.forceStartApp(); }
});
}
})();
</script>
</body>
</html>`
}
proc_unix.go //go:build !windows
package main
import (
"log"
"os/exec"
"syscall"
)
// プロセスグループ設定・起動
func setupProcessGroup(cmd *exec.Cmd) {
// 子プロセスがさらに孫プロセスを生成するケースに備え、
// Unix ではプロセスグループを切り出して終了時にまとめて Kill する。
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
err := cmd.Start()
if err != nil {
log.Fatal("Failed to start Streamlit:", err)
}
}
// プロセスツリーごと終了
func killProcessTree(cmd *exec.Cmd) {
if cmd == nil || cmd.Process == nil {
return
}
if pgid, err := syscall.Getpgid(cmd.Process.Pid); err == nil {
_ = syscall.Kill(-pgid, syscall.SIGKILL)
return
}
_ = cmd.Process.Kill()
}
// サーバー実行ファイルの証明書サムプリントを確認
func checkServerCertificate(path string) bool {
// 非対応
return false
}
proc_windows.go //go:build windows
package main
import (
"fmt"
"log"
"os/exec"
"strings"
"syscall"
"unsafe"
)
// Windows では Job Object に子プロセスをぶら下げ、
// ハンドル Close によりツリー全体を終了させる。
var jobObject syscall.Handle
const createNoWindow = 0x08000000 // CREATE_NO_WINDOW
// プロセスグループ設定・起動
func setupProcessGroup(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: createNoWindow,
}
err := cmd.Start()
if err != nil {
log.Fatal("Failed to start Streamlit:", err)
}
// Windows では Start 後に Job Object を割り当て、終了時にツリー全体を終了できるようにする。
postStartManage(cmd)
}
func postStartManage(cmd *exec.Cmd) {
if cmd == nil || cmd.Process == nil {
return
}
// 既に作成済みならスキップ
if jobObject != 0 {
return
}
h, err := createKillOnCloseJob()
if err != nil {
return
}
// プロセスハンドルを開く
const PROCESS_ALL_ACCESS = 0x1F0FFF
ph, err := syscall.OpenProcess(PROCESS_ALL_ACCESS, false, uint32(cmd.Process.Pid))
if err != nil {
syscall.CloseHandle(h)
return
}
if !assignProcessToJobObject(h, ph) {
syscall.CloseHandle(ph)
syscall.CloseHandle(h)
return
}
syscall.CloseHandle(ph)
jobObject = h
}
func killProcessTree(cmd *exec.Cmd) {
// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE により配下プロセスをまとめて終了する。
if jobObject != 0 {
syscall.CloseHandle(jobObject)
jobObject = 0
return
}
if cmd != nil && cmd.Process != nil {
_ = cmd.Process.Kill()
}
}
// --- Win32 API bindings ---
var (
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
procCreateJobObjectW = modkernel32.NewProc("CreateJobObjectW")
procSetInformationJobObject = modkernel32.NewProc("SetInformationJobObject")
procAssignProcessToJobObject = modkernel32.NewProc("AssignProcessToJobObject")
moddwmapi = syscall.NewLazyDLL("dwmapi.dll")
procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute")
)
const (
jobObjectInfoExtendedLimit = 9
jobObjectLimitKillOnJobClose = 0x00002000
)
type ioCounters struct {
ReadOperationCount uint64
WriteOperationCount uint64
OtherOperationCount uint64
ReadTransferCount uint64
WriteTransferCount uint64
OtherTransferCount uint64
}
type jobObjectBasicLimitInformation struct {
PerProcessUserTimeLimit int64
PerJobUserTimeLimit int64
LimitFlags uint32
MinimumWorkingSetSize uintptr
MaximumWorkingSetSize uintptr
ActiveProcessLimit uint32
Affinity uintptr
PriorityClass int32
SchedulingClass uint32
}
type jobObjectExtendedLimitInformation struct {
BasicLimitInformation jobObjectBasicLimitInformation
IoInfo ioCounters
ProcessMemoryLimit uintptr
JobMemoryLimit uintptr
PeakProcessMemoryUsed uintptr
PeakJobMemoryUsed uintptr
}
func createKillOnCloseJob() (syscall.Handle, error) {
r1, _, e1 := procCreateJobObjectW.Call(0, 0)
if r1 == 0 {
return 0, e1
}
h := syscall.Handle(r1)
var info jobObjectExtendedLimitInformation
info.BasicLimitInformation.LimitFlags = jobObjectLimitKillOnJobClose
size := uint32(unsafe.Sizeof(info))
ok, _, e2 := procSetInformationJobObject.Call(
uintptr(h),
uintptr(jobObjectInfoExtendedLimit),
uintptr(unsafe.Pointer(&info)),
uintptr(size),
)
if ok == 0 {
syscall.CloseHandle(h)
return 0, e2
}
return h, nil
}
func assignProcessToJobObject(hJob syscall.Handle, hProcess syscall.Handle) bool {
r1, _, _ := procAssignProcessToJobObject.Call(uintptr(hJob), uintptr(hProcess))
return r1 != 0
}
// サーバーファイルの証明書チェック(PowerShell版)
func checkServerCertificate(path string) bool {
// パス未指定は失敗扱い
if strings.TrimSpace(path) == "" {
return false
}
// PowerShell 経由で Authenticode 署名の Thumbprint を取得
psPath := strings.ReplaceAll(path, "'", "''")
psScript := fmt.Sprintf("(Get-AuthenticodeSignature -FilePath '%s').SignerCertificate.Thumbprint", psPath)
getThumb := func(cmdName string) (string, error) {
out, err := exec.Command(cmdName, "-NoProfile", "-NonInteractive", "-Command", psScript).Output()
if err != nil {
return "", err
}
t := strings.TrimSpace(string(out))
if t == "" {
return "", fmt.Errorf("empty thumbprint")
}
// 正規化: 大文字化・空白/コロン除去
t = strings.ToUpper(t)
t = strings.ReplaceAll(t, " ", "")
t = strings.ReplaceAll(t, ":", "")
return t, nil
}
// まず Windows 既定の PowerShell、次に pwsh を試す
thumb, err := getThumb("powershell")
if err != nil {
if t2, err2 := getThumb("pwsh"); err2 == nil {
thumb = t2
} else {
fmt.Printf("Failed to get certificate thumbprint: %v\n", err)
return false
}
}
// 取得したサムプリントが許可リストに存在するか確認
for _, allowed := range ALLOWED_THUMB_PRINTS {
a := strings.ToUpper(strings.TrimSpace(allowed))
a = strings.ReplaceAll(a, " ", "")
a = strings.ReplaceAll(a, ":", "")
if thumb == a {
return true
}
}
fmt.Printf("Certificate thumbprint not allowed: %s\n", thumb)
return false
}
ビルド †Linux/Mac環境でLinux/Mac用にビルドする x_build.sh #!/bin/bash go build -o ./dist/myapp_go Linux/Mac環境でWindows用にビルドする x_build_win.sh #!/bin/bash
set -euo pipefail
# Windows 用の exe をビルドします。
# 既定: amd64 向け、GUI(コンソール非表示)
# 使い方:
# ./x_build_win.sh [amd64|arm64]
ARCH="${1:-amd64}"
case "$ARCH" in
amd64|arm64) ;;
*) echo "[ERR] ARCH は amd64 か arm64 を指定してください" >&2; exit 1 ;;
esac
OUT="./dist/myapp_go.exe"
echo "[INFO] Building Windows ${ARCH} -> ${OUT}"
# webview は cgo を利用するため、クロスコンパイルには mingw-w64 が必要
# 例 (macOS/Homebrew): brew install mingw-w64
# amd64: x86_64-w64-mingw32-gcc
# arm64: aarch64-w64-mingw32-gcc
TRIPLET=""
if [[ "$ARCH" == "amd64" ]]; then
TRIPLET="x86_64-w64-mingw32"
else
TRIPLET="aarch64-w64-mingw32"
fi
GCC_BIN="${TRIPLET}-gcc"
GPP_BIN="${TRIPLET}-g++"
if ! command -v "$GCC_BIN" >/dev/null 2>&1 || ! command -v "$GPP_BIN" >/dev/null 2>&1; then
echo "[ERR] ${TRIPLET} のコンパイラが見つかりません。mingw-w64 をインストールし、PATH を通してください。" >&2
echo " 例: brew install mingw-w64" >&2
echo " export PATH=\"$(brew --prefix 2>/dev/null || echo /opt/homebrew)/opt/mingw-w64/bin:\$PATH\"" >&2
exit 1
fi
echo "[INFO] Using CC=$GCC_BIN, CXX=$GPP_BIN"
# -ldflags "-H windowsgui" でコンソールを非表示化
env GOOS=windows GOARCH="$ARCH" CGO_ENABLED=1 CC="$GCC_BIN" CXX="$GPP_BIN" \
go build -ldflags "-H windowsgui -s -w" -o "$OUT" ./
echo "[OK] Build completed: ${OUT}"
Windows環境でWindows用にビルドする x_build_win.bat @echo off REM ################################################################### REM CGO_ENABLED=1 でビルドする必要がある為、以下のビルド環境が必要 REM ################################################################### REM (1) Build Tools for Visual Studio のインストール REM (1-1) https://visualstudio.microsoft.com/ja/downloads/ からダウンロードし、 REM 「C++によるデスクトップ開発」にチェックを入れてインストール REM (1-2) cl.exe, link.exe に PATHを通す REM 例) C:\Program Files (x86)\Microsoft Visual Studio\XXXX\BuildTools\VC\Tools\MSVC\XX.XX.XXXXX\bin\Hostx64\x64 REM (2) gccのインストール REM 「Build Tools for Visual Studio」にはGCCは含まれていない為、別途 gcc をインストールする必要がある。 REM ※Build Tools で提供されるのはMSVCという別のコンパイラ REM (2-1) msys2 (https://www.msys2.org) をインストール REM (2-2) MSYS2 UCRT64 ターミナル から「pacman -S --needed base-devel mingw-w64-ucrt-x86_64-toolchain」を実行 REM ※MSYS2/MinGW-w64 UCRT64(ユニバーサルCランタイム)向け GCC/リンカをまとめて導入 REM (2-3) gcc, g++ へのPATHを通しておく REM 例) C:\msys64\ucrt64\bin REM ################################################################### set OUT="dist\myapp_go.exe" REM set CONSOLE_FLAG="-H windowsgui" set CONSOLE_FLAG= set GOOS=windows set GOARCH=amd64 set CGO_ENABLED=1 REM set CC=gcc REM set CXX=g++ go build -ldflags %CONSOLE_FLAG% " -s -w" -o %OUT% ./ おまけ(PowerShell非依存の証明書チェック) †証明書をチェックをPowerShellに依存しない形でAIに書かせてみた。 proc_windows.go var (
modcrypt32 = syscall.NewLazyDLL("crypt32.dll")
procCryptQueryObject = modcrypt32.NewProc("CryptQueryObject")
procCryptMsgGetParam = modcrypt32.NewProc("CryptMsgGetParam")
procCryptMsgClose = modcrypt32.NewProc("CryptMsgClose")
procCertFindCertificateInStore = modcrypt32.NewProc("CertFindCertificateInStore")
procCertFreeCertificateContext = modcrypt32.NewProc("CertFreeCertificateContext")
procCertCloseStore = modcrypt32.NewProc("CertCloseStore")
procCertOpenStore = modcrypt32.NewProc("CertOpenStore")
procCertEnumCertificatesInStore = modcrypt32.NewProc("CertEnumCertificatesInStore")
)
const (
CERT_QUERY_OBJECT_FILE = 1
CERT_QUERY_OBJECT_BLOB = 2
CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED = 0x00000008
CERT_QUERY_CONTENT_FLAG_PKCS7_UNSIGNED = 0x00000010
CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED = 0x00000020
CERT_QUERY_FORMAT_FLAG_BINARY = 0x00000002
CMSG_SIGNER_INFO_PARAM = 6
CERT_COMPARE_SUBJECT_CERT = 11
)
// CERT_FIND_SUBJECT_CERT = (CERT_COMPARE_SUBJECT_CERT<<16) | 0
const CERT_FIND_SUBJECT_CERT = (CERT_COMPARE_SUBJECT_CERT << 16) | 0
const (
X509_ASN_ENCODING = 0x00000001
PKCS_7_ASN_ENCODING = 0x00010000
)
type cryptBlob struct {
cbData uint32
pbData *byte
}
type certNameBlob = cryptBlob
type cryptAlgorithmIdentifier struct {
pszObjId *byte
Parameters cryptBlob
}
type cryptAttrBlob = cryptBlob
type cryptAttribute struct {
pszObjId *byte
cValue uint32
rgValue *cryptAttrBlob
}
type cryptAttributes struct {
cAttr uint32
rgAttr *cryptAttribute
}
type cmsgSignerInfo struct {
dwVersion uint32
Issuer certNameBlob
SerialNumber cryptBlob
HashAlgorithm cryptAlgorithmIdentifier
HashEncryptionAlgorithm cryptAlgorithmIdentifier
EncryptedHash cryptBlob
AuthAttrs cryptAttributes
UnauthAttrs cryptAttributes
}
type fileTime struct {
dwLowDateTime uint32
dwHighDateTime uint32
}
type certInfo struct {
dwVersion uint32
SerialNumber cryptBlob
SignatureAlgorithm cryptAlgorithmIdentifier
Issuer certNameBlob
NotBefore fileTime
NotAfter fileTime
Subject certNameBlob
}
type certContext struct {
dwCertEncodingType uint32
pbCertEncoded *byte
cbCertEncoded uint32
pCertInfo *certInfo
hCertStore syscall.Handle
}
// サーバーファイルの証明書チェック(PowerShell非依存版)
func checkServerCertificate(path string) bool {
// パス未指定は失敗扱い
if strings.TrimSpace(path) == "" {
fmt.Print("checkServerCertificate: false(1)")
return false
}
// 署名は PE の Attribute Certificate Table に格納されるため、PKCS#7 BLOB を抽出
f, err := os.Open(path)
if err != nil {
fmt.Print("checkServerCertificate: false(2)")
return false
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
fmt.Print("checkServerCertificate: false(3)")
return false
}
if len(data) < 0x40 || string(data[:2]) != "MZ" {
fmt.Print("checkServerCertificate: false(4)")
return false
}
peOff := int(uint32(data[0x3C]) | uint32(data[0x3D])<<8 | uint32(data[0x3E])<<16 | uint32(data[0x3F])<<24)
if peOff <= 0 || peOff+4 > len(data) || string(data[peOff:peOff+4]) != "PE\x00\x00" {
fmt.Print("checkServerCertificate: false(5)")
return false
}
coff := peOff + 4
if coff+20 > len(data) {
fmt.Print("checkServerCertificate: false(6)")
return false
}
sizeOpt := int(uint32(data[coff+16]) | uint32(data[coff+17])<<8)
optOff := coff + 20
if optOff+sizeOpt > len(data) || sizeOpt < 2 {
fmt.Print("checkServerCertificate: false(7)")
return false
}
magic := uint16(data[optOff]) | uint16(data[optOff+1])<<8
ddStart := 0
switch magic {
case 0x10b:
ddStart = optOff + 96
case 0x20b:
ddStart = optOff + 112
default:
fmt.Print("checkServerCertificate: false(8)")
return false
}
if ddStart+8*5 > optOff+sizeOpt {
fmt.Print("checkServerCertificate: false(9)")
return false
}
secDir := ddStart + 8*4
va := uint32(data[secDir]) | uint32(data[secDir+1])<<8 | uint32(data[secDir+2])<<16 | uint32(data[secDir+3])<<24
sz := uint32(data[secDir+4]) | uint32(data[secDir+5])<<8 | uint32(data[secDir+6])<<16 | uint32(data[secDir+7])<<24
if va == 0 || sz < 8 || int(va+sz) > len(data) {
fmt.Print("checkServerCertificate: false(10)")
return false
}
off := int(va)
end := off + int(sz)
var pkcs7 []byte
for off+8 <= end {
dwLen := int(uint32(data[off]) | uint32(data[off+1])<<8 | uint32(data[off+2])<<16 | uint32(data[off+3])<<24)
if dwLen < 8 || off+dwLen > end {
break
}
certType := int(uint16(data[off+6]) | uint16(data[off+7])<<8)
if certType == 2 { // WIN_CERT_TYPE_PKCS_SIGNED_DATA
pkcs7 = data[off+8 : off+dwLen]
break
}
off = (off + ((dwLen + 7) &^ 7))
}
if len(pkcs7) == 0 {
fmt.Print("checkServerCertificate: false(11)")
return false
}
// Open a temporary certificate store from the PKCS#7 blob
var hStore syscall.Handle
blob := cryptBlob{cbData: uint32(len(pkcs7))}
if len(pkcs7) > 0 {
blob.pbData = &pkcs7[0]
}
provider := []byte("PKCS7\x00")
rStore, _, _ := procCertOpenStore.Call(
uintptr(unsafe.Pointer(&provider[0])),
uintptr(X509_ASN_ENCODING|PKCS_7_ASN_ENCODING),
0,
0,
uintptr(unsafe.Pointer(&blob)),
)
hStore = syscall.Handle(rStore)
if hStore == 0 {
fmt.Print("checkServerCertificate: false(12)")
return false
}
defer procCertCloseStore.Call(uintptr(hStore), 0)
// Enumerate certificates in the PKCS#7 and compare thumbprints
var pCtx uintptr
for {
rCtx, _, _ := procCertEnumCertificatesInStore.Call(uintptr(hStore), pCtx)
if rCtx == 0 {
break
}
// Free previous context if any
if pCtx != 0 {
procCertFreeCertificateContext.Call(pCtx)
}
pCtx = rCtx
cc := (*certContext)(unsafe.Pointer(pCtx))
if cc.pbCertEncoded == nil || cc.cbCertEncoded == 0 {
continue
}
n := int(cc.cbCertEncoded)
der := make([]byte, n)
copy(der, (*[1 << 30]byte)(unsafe.Pointer(cc.pbCertEncoded))[:n:n])
sum := sha1.Sum(der)
thumb := strings.ToUpper(hex.EncodeToString(sum[:]))
for _, a := range ALLOWED_THUMB_PRINTS {
aa := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(a, " ", ""), ":", ""))
if thumb == aa {
// Free current context and return
procCertFreeCertificateContext.Call(pCtx)
fmt.Print("checkServerCertificate: OK")
return true
}
}
}
// Free last context if not already freed
if pCtx != 0 {
procCertFreeCertificateContext.Call(pCtx)
}
fmt.Print("checkServerCertificate: false(16)")
return false
}
おまけ(アイコンなど) †syso を作成して アイコンやアプリケーションの情報を設定する。 参考: https://github.com/josephspurrier/goversioninfo アイコンファイルの作成 †icoファイルを準備する versioninfo.json の作成 †参考
{
"FixedFileInfo": {
"FileVersion": {
"Major": 1,
"Minor": 2,
"Patch": 3,
"Build": 1
}
},
"StringFileInfo":
{
"Comments": "",
"CompanyName": "Sample Company",
"FileDescription": "MyApp",
"InternalName": "",
"LegalCopyright": "Copyright (C) 2014-2025 Sample Company",
"LegalTrademarks": "",
"OriginalFilename": "myapp_go.exe",
"PrivateBuild": "",
"ProductName": "Sample MyApp",
"ProductVersion": "v1.2.3.20251101_1",
"SpecialBuild": ""
}
}
syso の作成 †go run github.com/hymkor/goversioninfo/cmd/goversioninfo@master -64 -icon=icon.ico -o versioninfo.syso あとは、普通に go build するだけで、アイコン等が exe に設定される。 |