2021年6月27日 星期日

程式流程小技巧

if { } else { } 是流程進行的分歧點, 當層級過多時, 程式碼的邏輯就變的難以理解, 例如像是:

          if(... ) {

                if(... ) {

                       if(... ) {

                                   ...

                       } else {

                       }

                } else { 

                       ...

                } ...

          } else if ( ) {

          } else if( ) {

          }

改用一次性的 while loop, 例如:

            do { 

                        if( ) { ... 

                                 break

                         } 

                        if( ) { ... 

                                break

                        } 

                        ...

             } while(false)

若沒有 do{ } while 指令, 只有 while { } 指令, 只要最後使用 break 來跳脫迴圈就可:

           while (true) {

                    if ( ) { 

                         ...

                         break

                     }  

                     ...

                    break

            } 

這樣就能將程式碼攤平, 變的較容易閱讀

2021年6月22日 星期二

使用 gradle 管理並編譯 kotlin 程式碼

1. 首先上 gradle  官網: https://gradle.org/releases 只要下載 binary-only 壓縮檔就可以了

2. 打開終端機, 把下載回來的檔案(gradle-7.1-bin.zip)解壓縮, 再將執行檔所在目錄加進 PATH 內, gradle 就安裝完成:

     cd Downloads  &&  unzip gradle-7.1-bin.zip

     PATH=$PATH:/home/mint/Downloads/gradle-7.1/bin

3.建個目錄儲存各種專案, 主程式目錄名稱, 同時也會被當成是專案名稱, 例如下述 example 次專案, 進入目錄內執行 gradle init 將會再當地目錄初始化成新專案, 接著跳出選項, 要依照畫面適當選取, 最後執行 gradle run 就能啟動樣版程式

     mkdir  Project  &&  cd Project

     mkdir  example  &&  cd example

     gradle init

     gradle run

觀察它所產生的檔案, 我打算利用 make 語法, 編寫一個簡短的 Makefile 方便以後初始化新專案,而不用每次初始化都要用選的:
# /home/mint/Project/Makefile
shellpwd  = $(shell pwd)
project  = `basename $(shellpwd)`
gradlebin  = /home/mint/Downloads/gradle-7.1/bin/gradle
templateKts  = /home/mint/Project/template.gradle.kts
templateApp  = /home/mint/Project/template.kt
appDir  = app
kotlinDir  = src/main/kotlin
targetDir  = $(appDir)/$(kotlinDir)
AppKt  = $(targetDir)/App.kt
settingsKts  = settings.gradle.kts
setProject  = "rootProject.name=(\"$(project)\")"
setInclude = "include(\"$(appDir)\")"
buildKts = $(appDir)/build.gradle.kts
ktsSetApp  = "application { mainClass.set(\"$(project).AppKt\") }"
ktsSetSrc = "sourceSets.main { java.srcDirs(\"$(kotlinDir)\") }"

name:
    @echo this is a kotlin project: $(project)

$(targetDir): $(appDir)/src/main
    @[ -d $@ ]|| mkdir $@

$(appDir)/src/main: $(appDir)/src
    @[ -d $@ ] || mkdir $@

$(appDir)/src: $(appDir)
    @[ -d $@ ] || mkdir $@

$(appDir):
    @[ -d $@ ] || mkdir $@

init: $(settingsKts) $(AppKt)
    @echo ... $(project) is ready

$(settingsKts):
    @if [ -f $@ ]; then echo .; else echo $(setProject)      > $@ && echo $(setInclude) >> $@; fi

$(AppKt): $(targetDir) $(buildKts)
    @if [ -f $@ ]; then echo .; else echo package $(project) > $@ && cat $(templateApp) >> $@; fi

$(buildKts):
    @if [ -f $@ ]; then echo .; else cp $(templateKts) $@ && echo $(ktsSetSrc) >> $@ && echo $(ktsSetApp) >> $@ ;fi
   
run:
    $(gradlebin)  run  -q  --args='arg1 arg2 arg3 ...'

build:
    $(gradlebin) build --offline

clean:
    rm -rf  build  $(appDir)/build  .gradle

再寫一個 /home/mint/Project/template.gradle.kts 樣版檔案, 用來產生 build.gradle.kts:

// template.gradle.kts
plugins {
    kotlin("jvm") version "1.5.10"
    application
}
repositories { mavenCentral() }
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") }

以及另一個  /home/mint/Project/template.kt 樣版範例, 用來產生 kotline 的程式碼:

// template.kt
import kotlinx.coroutines.*
import kotlin.coroutines.*
fun main() {
    runBlocking{
        launch {
            println("Hello")
        }
        launch {
            println("world")
        }
        launch {
            println("you are welcome")
        }
    }
}

將上述 3 個檔案各自存檔後, 在 Makefile 所在目錄內執行 make init, 或是使用參數 -f 指定 Makefile 位置, 上述 make init 用來先產生 settings.grtadle.kts, app/build.gradle.kts, 及 app/src/main/kotlin/App.kt 等範例代碼, 這樣 gradle run 就能編譯並且執行程式:

       make   init   -f   /home/mint/Project/Makefile

      gradle  run

不用寫程式, 就能讓上面的樣版程式跑起來, 夠神奇吧! 實際上是 gradle 先根據 build.gradle.kts 內容,先上網路倉庫(repositories), 找尋插件(plugins)將 kotlin 編譯程式  kotlin("jvm") version "1.5.10" 以及各種需要的相關(dependencies)程式庫,下載後存入硬碟, 再呼叫 kotlin 去編譯成 Java 代碼(byte code), 最後在 J(ava)V(irtual)M(achine 內執行程式. 一切全自動完成, 這就是 gradle 厲害的地方.

ps.

1. gradle 缺點是網路一定要通暢才能運作, 主要是 gradle daemon 要先跑起來.雖然可以用 gradle --offline build 離線編譯程式, 但沒了網路, 例如用 iptables 先把網路的輸出通道關閉:

    sudo  iptables  -A  OUTPUT  -m  owner  --gid-owner  mint  -j  REJECT

    gradle  --offline  build

他就無法編譯了?  這參數 --offline 目的就真的有點奇怪, 似乎名不正言不順.

 2. 如果用複製/貼上面的 Makefile 內容, 無法讓 make 運作, 可能是縮排(tab)的問題, 可以用文字編輯程式稍微修改, 因為 make 規定所有標的物的執行代碼(冒號: 下一行)必須要先縮排(tab)才能運作, 否則會出現錯誤訊息.

2021年6月13日 星期日

使用 kotlin 語言把 http 協定連線升級成 WebSocket 協定所需要的回應編碼: base64( sha1(clientKey) )

 參考資料: https://datatracker.ietf.org/doc/html/rfc6455

    // echo -n dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11 | sha1sum 
    // sha1Hex  : b37a4f2cc0624f1690f64606cf385945b2bec4ea
    // keyBase64: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
   
    val clientKey = "dGhlIHNhbXBsZSBub25jZQ=="  
    val guID      = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
    val base64sym = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    val key_GUID  = "${clientKey.trim()}$guID"
    val binary    = MessageDigest.getInstance("SHA-1").digest(key_GUID.toByteArray())
    val sha1Hex   = StringBuilder()
    val keyBase64 = StringBuilder()
    val temp3 = IntArray(3)
    var accumulate = 0
    for (c in binary)  {
        sha1Hex.append(String.format("%02x", c))
        temp3[accumulate ++] = c.toInt()
        if (accumulate == 3) {
            accumulate = 0;
            keyBase64.append(base64sym[ (temp3[0] and 0xfc) shr 2])
            keyBase64.append(base64sym[((temp3[0] and 0x03) shl 4) + ((temp3[1] and 0xf0) shr 4)])
            keyBase64.append(base64sym[((temp3[1] and 0x0f) shl 2) + ((temp3[2] and 0xc0) shr 6)])
            keyBase64.append(base64sym[  temp3[2] and 0x3f])
        }
    }
    if (accumulate > 0) {
        keyBase64.append(base64sym[ (temp3[0] and 0xfc) shr 2])
        if (accumulate == 2) {
            keyBase64.append(base64sym[((temp3[0] and 0x03) shl 4) + ((temp3[1] and 0xf0) shr 4)])
            keyBase64.append(base64sym[ (temp3[1] and 0x0f) shl 2])
        }
        keyBase64.append('=')
        if(accumulate == 1) keyBase64.append('=')
    }
    Log.d("WebSocket_key:", "$clientKey, $keyBase64, $sha1Hex") 

2021年6月10日 星期四

寫一個 Makefile 利用 openssl 產生自我簽章(selfsign)的證書(certification) 讓 Android SSLServerSocket 正常運作

將下列文字存成 Makefile, 複製到 Android 專案目錄內, 接著在終端機下指令 Make, 就會產生一個鑰匙庫(keystore.p12)放在 app/src/main/res/raw 目錄內, 而私鑰則放在 key 目錄下:
#Makefile
storepass=PKCS12_keystore_for_android_app
tempDir=/dev/shm
saveDir=app/src/main/res/raw
privateDir=key

gen: keystore.p12

keystore.p12: selfsign
    openssl pkcs12 -export -inkey $(privateDir)/prvkey.pem -passout pass:$(storepass) -in $(tempDir)/$< -out $(saveDir)/$@
    
selfsign: buildDirectory
    openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $(privateDir)/prvkey.pem -subj "/C=TW/ST=Taiwan/L=TaipeiCity/CN=localhost" -out $(tempDir)/$@
    
buildDirectory:
    [ -d $(saveDir) ] || mkdir $(saveDir)
    [ -d $(privateDir) ] || mkdir $(privateDir)

clean:
    rm -rf $(tempDir)/selfsign

Android app 可以利用 resources 將它載入, 之後就能讓 SSL Server Socket 正常運作:

        // ...
        val tls = SSLContext.getInstance("TLS").apply {
            val keyStore = KeyStore.getInstance("PKCS12")
            val storepass = "PKCS12_keystore_for_android_app"//pass phrase define in Makefile
            resources.openRawResource(R.raw.keystore).apply {
                keyStore.load(this, storepass.toCharArray())
                close()
            }
            val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply {
                init(keyStore, storepass.toCharArray())
            }
            init(kmf.keyManagers, null, null)
        }
        val sslServer = tls.serverSocketFactory.createServerSocket(8443) .apply{
            setReuseAddress(true)
        }

如果寫的是 ssl client 端程式, 則要將信任網站的簽署檔(cert file) 放入 keystore, 交由 TrustManager 來管理, Android 可以使用 BKS 格式檔:

        val tls = SSLContext.getInstance("TLS").apply {
            val keyStore = KeyStore.getInstance("PKCS12")
            val storepass = "PKCS12_keystore_for_android_app"//pass phrase define in Makefile
            resources.openRawResource(R.raw.keystore).apply {
                keyStore.load(this, storepass.toCharArray())
                close()
            }
            val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply {
                init(keyStore, storepass.toCharArray())
            }
            val trustStore = KeyStore.getInstance("BKS")
            val storepass = "PKCS12_keystore_for_android_app"//pass phrase define in Makefile
            resources.openRawResource(R.raw.truststore).apply {
                trustStore.load(this, storepass.toCharArray())
                close()
            }
            val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply {
                init(trustStore)
            }
            init(kmf.keyManagers, tmf.trustManagers, SecureRandom())
        }

簡單 c 程式碼, 根據五行八卦相生相剋推斷吉凶

#include "stdio.h" // 五行: //               木2 //      水1           火3 //         金0     土4 // // 先天八卦 vs 五行 //                    ...