2024年5月15日 星期三

PowerShell 獲取剪貼簿歷史紀錄 (包含pwsh)

PowerShell 獲取剪貼簿歷史紀錄

參考自這篇文章並做了一些改良
Enumerating Windows clipboard history in PowerShell - The Old New Thing (microsoft.com)

由於載入的物件僅有 PowerShell 能使用,這導致在 Pwsh 中無法使用,一個簡單的解法就是直接呼叫 PowerShell 來做事就好了

這邊附上完整的函式

function Get-ClipboardHistory {

    # 定義在 Windows PowerShell 中執行的腳本
    $scriptBlock = {
        # 導入 Windows Runtime 支持
        Add-Type -AssemblyName System.Runtime.WindowsRuntime
        # 尋找 AsTask 泛型方法,用於處理異步操作
        $asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object {
            $_.Name -eq 'AsTask' -and
            $_.GetParameters().Count -eq 1 -and
            $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1'
        })[0]

        # 定義 Await 函數,用於等待 Windows Runtime 的異步操作結果
        function Await($WinRtTask, $ResultType) {
            $asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
            $netTask = $asTask.Invoke($null, @($WinRtTask))
            $netTask.Wait(-1) | Out-Null
            $netTask.Result
        }

        # 加載剪貼板相關的 Windows Runtime 類型
        [Windows.ApplicationModel.DataTransfer.Clipboard, Windows.ApplicationModel.DataTransfer, ContentType=WindowsRuntime] | Out-Null
        # 獲取剪貼板歷史項目的異步操作
        $op = [Windows.ApplicationModel.DataTransfer.Clipboard]::GetHistoryItemsAsync()
        $result = Await ($op) ([Windows.ApplicationModel.DataTransfer.ClipboardHistoryItemsResult])

        # 獲取每個歷史項目的文本內容
        $textops = $result.Items.Content.GetTextAsync()

        # 迭代處理每個歷史項目的文本
        $textObjects = @()
        foreach ($textop in $textops) {
            $textObjects += Await($textop) ([String])
        }

        # 將對向轉換為 JSON 字符串
        $textObjects | ConvertTo-Json

    }

    # 使用 Base64 編碼 PowerShell 腳本,並通過 PowerShell.exe 執行
    $encodedBlock = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($scriptBlock.ToString()))
    ,@((powershell.exe -EncodedCommand $encodedBlock | ConvertFrom-Json))

} # (Get-ClipboardHistory)[0]



對於特殊符號有些顯示不出來變成問號,改成 UTF8 可以正常顯示

[console]::OutputEncoding = [Text.Encoding]::UTF8

這其實不是編碼不匹配的問題單純只是中文 BIG5 的符號沒有 UTF8 齊全而已,根據需求自己實作進去吧。



2024年5月13日 星期一

Box Python SDK 如何設置 proxy

Box Python SDK 如何設置 proxy



方法0 透過環境環境變數設置

只需要從 python 環境中設置環境變數即可

import os
os.environ['http_proxy'] = 'http://Enterprise.co.jp:8080'
os.environ['https_proxy'] = 'https://Enterprise.co.jp:8080'

我也有嘗試著從環境變數設置過無效,下面舊文是我掙扎的痕跡,可能是我這邊環境問題。

但總之最後測試的結果是,BOXSDK不用改直接追加這幾行在開頭就行了。


如果你從這邊設置無效順著我底下舊文直接改SDK絕對不受環境影響的。








如何 Excel 垂直合併儲存格中的文字到第一個

如何 Excel 垂直合併儲存格中的文字到第一個

應該經常遇到說上下文被儲存格分開了,需要合併又只能手動剪下文字,不然直接按合併第二格之後的文字會通通不見。

寫了個腳本來處理這件事件,腳本可以指定到快捷鍵加速操作,平常也不需要貼到目標 xlsx 檔案上只需要儲存在一個屬於自己的 xlsm 上面,當你需要這功能的時候打開這個檔案在背景就可以了。

快捷鍵可能每次都要在目標 xlsx 上重設,不過這幾乎不成問題就是了。



腳本

Sub CombineTextInColumns()
    Dim rng As Range
    Dim cell As Range
    Dim startCell As Range
    Dim combinedText As String
    Dim col As Long
    Dim shouldMerge As Boolean

    ' 控制是否合併儲存格的變數
    shouldMerge = False  ' 如果設為 False 則不合併儲存格,如果設為 True 則合併儲存格

    ' 確認用戶已選擇儲存格
    If Not TypeName(Selection) = "Range" Then
        MsgBox "請選擇儲存格"
        Exit Sub
    End If

    Set rng = Selection
    ' 按列處理選定範圍
    For col = 1 To rng.Columns.Count
        Set startCell = rng.Cells(1, col)
        combinedText = startCell.Value
        ' 合併每一列中的文字到第一個儲存格
        For Each cell In rng.Columns(col).Cells
            If cell.Address <> startCell.Address Then
                If combinedText <> "" And cell.Value <> "" Then
                    combinedText = combinedText & Chr(10) & cell.Value
                ElseIf cell.Value <> "" Then
                    combinedText = cell.Value
                End If
            End If
        Next cell

        ' 更新第一個儲存格的內容
        startCell.Value = combinedText
        startCell.WrapText = True

        ' 清除該列中第一個儲存格以外的其他儲存格內容
        For Each cell In rng.Columns(col).Cells
            If cell.Address <> startCell.Address Then
                cell.ClearContents
            End If
        Next cell

        ' 根據條件合併儲存格
        If shouldMerge Then
            rng.Columns(col).Merge
            rng.Columns(col).VerticalAlignment = xlTop
        End If
    Next col

    ' 自動調整所有選定範圍的行高
    rng.Rows.AutoFit
End Sub






補一個日文註解版本的

Sub CombineTextInColumns()
    Dim rng As Range
    Dim cell As Range
    Dim startCell As Range
    Dim combinedText As String
    Dim col As Long
    Dim shouldMerge As Boolean

    ' セルをマージするかどうかを制御する変数
    shouldMerge = False  ' False の場合はセルをマージしない、 True の場合はセルをマージ

    ' ユーザーがセルを選択していることを確認
    If Not TypeName(Selection) = "Range" Then
        MsgBox "セルを選択してください"
        Exit Sub
    End If

    Set rng = Selection
    ' 選択範囲の各列を処理
    For col = 1 To rng.Columns.Count
        Set startCell = rng.Cells(1, col)
        combinedText = startCell.Value
        ' 各列のセルのテキストを最初のセルに結合
        For Each cell In rng.Columns(col).Cells
            If cell.Address <> startCell.Address Then
                If combinedText <> "" And cell.Value <> "" Then
                    combinedText = combinedText & Chr(10) & cell.Value
                ElseIf cell.Value <> "" Then
                    combinedText = cell.Value
                End If
            End If
        Next cell

        ' 最初のセルの内容を更新
        startCell.Value = combinedText
        startCell.WrapText = True

        ' 最初のセル以外のセルの内容をクリア
        For Each cell In rng.Columns(col).Cells
            If cell.Address <> startCell.Address Then
                cell.ClearContents
            End If
        Next cell

        ' 条件に基づいてセルをマージ
        If shouldMerge Then
            rng.Columns(col).Merge
            rng.Columns(col).VerticalAlignment = xlTop
        End If
    Next col

    ' 選択範囲のすべての行の高さを自動調整
    rng.Rows.AutoFit
End Sub



2024年5月5日 星期日

PowerShell獲取檔案權限表格

PowerShell獲取檔案權限表格

原本的物件不是很好使,寫了個函式方便快速查詢

輸出範例

Identity                         FullControl Modify ReadAndExecute Read  Write
--------                         ----------- ------ -------------- ----  -----
NT AUTHORITY\SYSTEM              Allow
NT AUTHORITY\Authenticated Users             Allow
BUILTIN\Administrators           Allow
BUILTIN\Users                                       Allow          Allow Deny




函式代碼

Get-FilePermissions

function Get-FilePermissions {
     [CmdletBinding(DefaultParameterSetName = "")]
     param(
         [Parameter(Position = 0, ParameterSetName = "", Mandatory)]
         [string]$Path,
         [Parameter(Position = 1, ParameterSetName = "")]
         [string]$Identity
     )

     # 確認指定的路徑是否存在
     if (!(Test-Path $Path)) {
         Write-Error "Cannot find path '$Path' because it does not exist." -ErrorAction Stop
     }

     # 取得指定路徑的存取控制清單 (ACL)
     $acl = Get-Acl $Path

     # 定義需要檢查的權限列表
     $permissions = @('FullControl', 'Modify', 'ReadAndExecute', 'Read', 'Write')

     # 初始化一個字典來儲存每個使用者的權限狀態
     $permissionDict = @{}

     # 遍歷存取控制規則
     foreach ($accessRule in $acl.Access) {
         # 取得使用者名稱
         $user = $accessRule.IdentityReference.Value

         # 如果字典尚未包含目前用戶,則新增一個條目
         if (-not $permissionDict.ContainsKey($user)) {
             # 動態建立權限字段
             $userPermissions = [ordered]@{ Identity = $user }
             foreach ($permission in $permissions) {
                 $userPermissions[$permission] = ''
             }
             $permissionDict[$user] = $userPermissions
         }

         # 更新權限狀態,拒絕權限優先
         foreach ($permission in $permissions) {
             if ($accessRule.FileSystemRights -like "*$permission*") {
                 $state = $accessRule.AccessControlType.ToString()
                 if ($permissionDict[$user][$permission] -ne 'Deny') {
                     $permissionDict[$user][$permission] = $state
                 }
             }
         }
     }

     # 將字典中的值轉換為物件數組
     if ($Identity) {
         $permission = [PSCustomObject]($permissionDict[$Identity])
     } else {
         $permission = $permissionDict.Values | ForEach-Object { [PSCustomObject]$_ }
     }

     # 傳回產生的權限表格
     return $permission
}

# Get-FilePermissions "Z:\test.txt"|Format-Table|Out-String
# Get-FilePermissions "Z:\test.txt" -Identity 'BUILTIN\Administrators'


使用範例

# 輸出表格
Get-FilePermissions "Z:\test.txt"|Format-Table|Out-String

# 指定特定使用者或群組名稱
Get-FilePermissions "Z:\test.txt" -Identity 'BUILTIN\Administrators'



2024年5月1日 星期三

Linux Ubuntu 自動更新腳本

Linux Ubuntu 自動更新腳本

更新要打的指令其實就那麼幾個只是經常打覺得煩了,習慣上會弄成一個 update.sh 這樣來做比較快

#!/bin/bash

# 更新軟件包列表
sudo apt update

# 升級已安裝的軟件包
sudo apt upgrade -y

# 清理不再需要的軟件包
sudo apt autoremove -y

# 清理下載的安裝包緩存
sudo apt clean

複製之後使用下面的指令打開編輯器

nano ~/update.sh


然後貼上按下 CTRL+S 儲存,再按 CTRL+X 離開
接著在更改權限讓他變成可執行檔案

chmod +x ~/update.sh


到這邊就大功告成了可以使用下面快捷更新

./update.sh



執行之後還是有可能沒更完全

  1. 保留的軟件包:某些軟件包可能因為依賴關係的問題被保留下來,不進行自動升級。
  2. 需要手動干預:有時候,軟件包需要你手動確認升級,尤其是那些可能影響系統設定的重要軟件包。


查看哪些沒有被更新

sudo apt list --upgradable


1. 強制更新

sudo apt full-upgrade

2. 移除

sudo apt remove YOUR_APP_NAME

3. 移除(連同所有設定檔)

sudo apt purge YOUR_APP_NAME




PowerShell 如何接收外部程式返回的二進制值

PowerShell 如何接收外部程式返回的二進制值

這個超坑人阿 PowerShell 預設會自動把收到的數值編碼,導致二進制數值讀進來的瞬間就因為轉換的關係資料遺失了

舉個例子來說如果要用 OpenSSL 簽名,簽名有點複雜在簡化一點產生一個隨機數值並返回二進位數值

openssl rand -out - 8

拿到的值就是爛掉的因為轉換編碼的過程就發生不可逆的資料丟失了
查了一下還好是有解的

shell - How to get original binary data from external command output in PowerShell? - Stack Overflow

只能說真愛生命不要用 PowerShell 管道傳輸非字串的東西…




完成的函式

代碼我把他弄好成一個函式了

function Invoke-CommandAndGetBinaryOutput {
    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [string]$CommandLine
    )
    Process {
        # 用空白分割命令行,並分配文件名與參數
        $parts = $CommandLine -split ' ', 2
        $fileName = $parts[0]
        $arguments = $parts[1]

        # 建立 ProcessStartInfo 對象並配置
        $processInfo = New-Object System.Diagnostics.ProcessStartInfo -Property @{
            FileName = $fileName
            Arguments = $arguments
            RedirectStandardOutput = $true
            RedirectStandardError = $true
            UseShellExecute = $false
            CreateNoWindow = $true
        }

        # 開始進程
        $process = New-Object System.Diagnostics.Process
        $process.StartInfo = $processInfo
        $process.Start() | Out-Null
        $process.WaitForExit()

        # 使用 MemoryStream 讀取二進制數據
        $memoryStream = New-Object System.IO.MemoryStream
        try {
            $process.StandardOutput.BaseStream.CopyTo($memoryStream)
            $binaryOutput = $memoryStream.ToArray()

            # 返回二進制數據
            return $binaryOutput
        } finally {
            $memoryStream.Dispose()
        }
    }
} # Write-Host (Invoke-CommandAndGetBinaryOutput "openssl rand -out - 8")

使用範例就是

Write-Host (Invoke-CommandAndGetBinaryOutput "openssl rand -out - 8")

拿到的是二進位數值,這個不要隨便亂成任意編碼(UTF-8)會丟失信息的。

想要安全的攜帶大概就是走base64的路子了,轉法下面有。




驗證

就能拿到正確的數值了,不過 OpenSSL 只有簽名的時候結果才是固定的這邊有點難驗證就是了,大概寫一下可驗證的辦法

走檔案路線的不會受到影響結果一定是對的

# 調用 OpenSSL 進行簽名 (走檔案路線)
$toSignData | Set-Content $toSignDataPath -NoNewline
openssl dgst -sha512 -sign $privatekeyPath -binary -out $signaturePath $toSignDataPath
$base64Signature = [System.Web.HttpServerUtility]::UrlTokenEncode([System.IO.File]::ReadAllBytes($signaturePath)) -replace('[012]$')

然後是副程式的用法

$toSignData | Set-Content $toSignDataPath -NoNewline
$bytes = Invoke-CommandAndGetBinaryOutput "OpenSSL dgst -sha512 -binary -sign `"$privatekeyPath`" `"$toSignDataPath`""
$base64Signature = [System.Web.HttpServerUtility]::UrlTokenEncode($bytes) -replace('[012]$')

寫個大概而已自己在琢磨一下 OpenSSL 怎麼使用




2024年4月27日 星期六

利用 Curl.exe 提交 Box 的 JWT 請求獲取 Access Token

利用 Curl.exe 提交 Box 的 JWT 請求獲取 Access Token

這邊比較難搞的是JWT,比較無腦一點就是直接載人家函式來弄,編碼之後會變成一長串文字,由Post送出去呼叫伺服器的API。


獲取 Box JSON File

請從BOX建立應用程式下載JWT認證Json私鑰檔案

$config 是直接從BOX上抓下來的Json檔案,他格式大概會長這樣子

{
  "boxAppSettings": {
    "clientID": "clientID",
    "clientSecret": "clientSecret",
    "appAuth": {
      "publicKeyID": "publicKeyID",
      "privateKey": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nSTRING\n-----END ENCRYPTED PRIVATE KEY-----",
      "passphrase": "secret_passphrase"
    }
  },
  "enterpriseID": "enterpriseID"
}


打包成JWT令牌

一個完整的JWT是由三個部分的base64url組成,分別是 "$heder.$payload.$Signature"

這部分請參考這份代碼獲取 $assertion

hunandy14/PsJwt (github.com)



向伺服器發送JWT請求

$assertion 是完整的 JWT 令牌,包含簽名後的字串

要快速驗證內容有工具網站可以看 JSON Web Tokens - jwt.io



利用 Invoke-RestMethod 請求 AccessToken

    # Generate BoxToken Request
    $irmParams = @{
        Uri = 'https://api.box.com/oauth2/token'
        Method = 'Post'
        Body = @{
            grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
            assertion = "$tokenData.$signature"
            client_id = $config.boxAppSettings.clientID
            client_secret = $config.boxAppSettings.clientSecret
        }
        ContentType = 'application/x-www-form-urlencoded'
    }; if ($env:HTTP_PROXY) { $irmParams['Proxy'] = $env:HTTP_PROXY }

    # Request AccessToken
    try {
        $response = Invoke-RestMethod @irmParams
    } catch {
        Write-Error $PSItem.Exception.Message -ea 1
    }

    # Check AccessToken
    $response



利用外部程序 curl.exe 請求 AccessToken 

下面是用 PowerShell 寫的 Curl 請求代碼。這邊要注意一個坑是如果沒有加上 .exe 的話預設會呼叫內建的指令,內建會直接回傳 PowerShell 物件而不是文字,然後參數用法有不同不能直接換過去。

function RequestBoxToken {
    param (
        [string]$Assertion,
        [string]$ConfigPath
    )
    # Read configuration from JSON file
    $configContent = Get-Content $ConfigPath -Raw
    $config = ConvertFrom-Json $configContent

    # Generate Request queryString
    $url = 'https://api.box.com/oauth2/token'
    $proxyUrl = $env:http_proxy
    $response = curl.exe `
        -x $proxyUrl `
        -X POST $url `
        -d 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' `
        -d "assertion=$Assertion" `
        -d "client_id=$($config.boxAppSettings.clientID)" `
        -d "client_secret=$($config.boxAppSettings.clientSecret)"
    # RequestBoxAccessToken
    $response = ConvertFrom-Json $response
    return $response
}



Python的請求方法 

import requests
import os
import json

def request_box_token(assertion, config_path):
    # 讀取設定檔
    with open(config_path, 'r') as file:
        config = json.load(file)

    url = 'https://api.box.com/oauth2/token'

    # 設置代理
    http_proxy = os.getenv('http_proxy')
    https_proxy = os.getenv('https_proxy')
    proxies = {}
    if http_proxy:
        proxies['http'] = http_proxy
    if https_proxy:
        proxies['https'] = https_proxy

    payload = {
        'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        'assertion': assertion,
        'client_id': config['boxAppSettings']['clientID'],
        'client_secret': config['boxAppSettings']['clientSecret']
    }
    response = requests.post(url, data=payload, proxies=proxies if proxies else None)
    return response.json()

# 使用方法
assertion = '你的assertion'
config_path = 'config.json'  # 設定檔路徑
response = request_box_token(assertion, config_path)
print(response)




官方SDK請求方法

import json
from boxsdk import Client, JWTAuth

# 載入官方 JSON 檔案
with open('config.json') as json_file:
    config = json.load(json_file)

# 創建 JWTAuth 認證對象
auth = JWTAuth(
    client_id=config['boxAppSettings']['clientID'],
    client_secret=config['boxAppSettings']['clientSecret'],
    enterprise_id=config['enterpriseID'],
    jwt_key_id=config['boxAppSettings']['appAuth']['publicKeyID'],
    rsa_private_key_data=config['boxAppSettings']['appAuth']['privateKey'].replace('\\n', '\n'),
    rsa_private_key_passphrase=config['boxAppSettings']['appAuth']['passphrase']
)

# 獲取 Access Token
access_token = auth.authenticate_instance()
print(f"access_token: {access_token}")

# 使用 Access Token 創建 Box Client
client = Client(auth)

# 驗證登錄並獲取當前用戶信息
current_user = client.user().get()
print('Authenticated as:', current_user.name)


這個只要拿到 json 檔案並且審核通過後就可以直接拿來用了。至於如何通過 proxy 可以參考這一篇的說明 
CHG: 利用 Curl.exe 提交 Box 的 JWT 請求獲取 Access Token (charlottehong.blogspot.com)