鸿蒙网络编程系列59-仓颉版TLS回声服务器示例

1. 网络通讯的安全性问题及解决方案

在基于TCP或者UDP的通讯中,通讯内容是明文发送和接收的,对于安全性要求不太高的通讯场景,这种方式因为实现简单,传输效率高而得到广泛应用;
但是,如果数据包在传输过程中被拦截,攻击者可以直接读取其中的信息,这使得用户的敏感信息(如密码、个人资料等)容易遭受窃听或篡改。要避免这种情况的发生,
就需要对传输过程进行加密,典型的是基于TLS协议的通讯,它通过加密技术确保数据的保密性和完整性,防止数据在传输过程中被窃听或篡改。当使用TLS进行通讯时,客户端和服务器先进行握手,在这个过程中双方协商加密算法、交换加密密钥等,之后所有传输的数据都会被加密,即使数据包被第三方截获,由于没有解密密钥,第三方也无法读取数据的真实内容。

本系列的第33篇文章,在API 12环境下使用ArkTS语言实现了TLS回声服务器,本篇文章将在API 17环境下,使用仓颉语言实现TLS回声服务器。
在目前的版本里,鸿蒙并没有提供TLS服务端相关的API,所以,本文将使用仓颉语言原生的TLS相关API实现,典型的类有TlsSocket、TlsServerConfig等,它们在net.tls包里。

TLS服务端的实现还需要服务端数字证书及私钥,需要预先准备好相关的文件并上传到鸿蒙设备中。

2. TLS回声服务器演示

本示例运行后的界面如图所示:

选择服务端数字证书及服务端数字证书私钥,然后单击“启动”按钮,可以绑定服务端到本地端口,如图所示:

更进一步的TLS通讯需要TLS客户端的配合,我们将在下一篇文章介绍TLS服务端和客户端的数据收发过程。

3. TLS回声服务器示例编写

下面详细介绍创建该示例的步骤(确保DevEco Studio已安装仓颉插件)。

步骤1:创建[Cangjie]Empty Ability项目。

步骤2:在module.json5配置文件加上对权限的声明:

"requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]

这里添加了访问互联网的权限。

步骤3:在build-profile.json5配置文件加上仓颉编译架构:

"cangjieOptions": {
      "path": "./src/main/cangjie/cjpm.toml",
      "abiFilters": ["arm64-v8a", "x86_64"]
    }

步骤4:在main_ability.cj文件里添加如下的代码:

package ohos_app_cangjie_entry

internal import ohos.base.AppLog
internal import ohos.ability.AbilityStage
internal import ohos.ability.LaunchReason
internal import cj_res_entry.app
import ohos.ability.*

//Ability全局上下文
var globalAbilityContext: Option<AbilityContext> = Option<AbilityContext>.None
class MainAbility <: Ability {
    public init() {
        super()
        registerSelf()
    }

    public override func onCreate(want: Want, launchParam: LaunchParam): Unit {
        AppLog.info("MainAbility OnCreated.${want.abilityName}")
        globalAbilityContext = Option<AbilityContext>.Some(this.context)
        match (launchParam.launchReason) {
            case LaunchReason.START_ABILITY => AppLog.info("START_ABILITY")
            case _ => ()
        }
    }

    public override func onWindowStageCreate(windowStage: WindowStage): Unit {
        AppLog.info("MainAbility onWindowStageCreate.")
        windowStage.loadContent("EntryView")
    }
}

步骤5:在index.cj文件里添加如下的代码:

package ohos_app_cangjie_entry

import ohos.base.*
import ohos.component.*
import ohos.state_manage.*
import ohos.state_macro_manage.*
import ohos.file_picker.*
import ohos.ability.*
import ohos.file_fs.*
import crypto.x509.*
import ohos.crypto.*
import std.convert.*
import net.tls.*
import std.socket.*

@Observed
//文件选择状态
class FileSelectStatus {
    @Publish
    public var fileSelected: Bool = false
    @Publish
    public var fileUri: String = ""
}

@Entry
@Component
class EntryView {
    @State
    var title: String = 'TLS回声服务器示例';

    //连接、通讯历史记录
    @State
    var msgHistory: String = ''

    //证书文件选择状态
    @State
    var certFileStatus: FileSelectStatus = FileSelectStatus()

    //私钥文件选择状态
    @State
    var keyFileStatus: FileSelectStatus = FileSelectStatus()

    //服务端端口
    @State
    var port: UInt16 = 9990

    //服务运行状态
    @State
    var running: Bool = false

    //服务端套接字
    var echoServer: ?TcpServerSocket = None

    let scroller: Scroller = Scroller()

    func build() {
        Row {
            Column {
                Text(title)
                    .fontSize(14)
                    .fontWeight(FontWeight.Bold)
                    .width(100.percent)
                    .textAlign(TextAlign.Center)
                    .padding(10)

                Flex(FlexParams(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center)) {
                    Text("服务端数字证书:").fontSize(14).width(90).flexGrow(1)

                    Button("选择").onClick {
                        evt => selectFile(this.certFileStatus)
                    }.width(60).fontSize(14)
                }.width(100.percent).padding(5)

                Text(certFileStatus.fileUri).fontSize(14).width(100.percent).padding(10)

                Flex(FlexParams(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center)) {
                    Text("服务端数字证书私钥:").fontSize(14).width(90).flexGrow(1)

                    Button("选择").onClick {
                        evt => selectFile(this.keyFileStatus)
                    }.width(60).fontSize(14)
                }.width(100.percent).padding(5)

                Text(keyFileStatus.fileUri).fontSize(14).width(100.percent).padding(10)

                Flex(FlexParams(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center)) {
                    Text("绑定的服务器端口:").fontSize(14).width(90).flexGrow(1)

                    TextInput(text: port.toString())
                        .onChange({
                            value => if (value == "") {
                                port = 0
                            } else {
                                port = UInt16.parse(value)
                            }
                        })
                        .setType(InputType.Number)
                        .width(80)
                        .fontSize(11)

                    Button(if (running) {
                        "停止"
                    } else {
                        "启动"
                    })
                        .onClick {
                            evt => if (!running) {
                                startServer()
                            } else {
                                stopServer()
                            }
                        }
                        .width(60)
                        .fontSize(14)
                        .enabled(certFileStatus.fileSelected && keyFileStatus.fileSelected && port != 0)
                }.width(100.percent).padding(5)

                Scroll(scroller) {
                    Text(msgHistory)
                        .textAlign(TextAlign.Start)
                        .padding(10)
                        .width(100.percent)
                        .backgroundColor(0xeeeeee)
                }
                    .align(Alignment.Top)
                    .backgroundColor(0xeeeeee)
                    .height(300)
                    .flexGrow(1)
                    .scrollable(ScrollDirection.Vertical)
                    .scrollBar(BarState.On)
                    .scrollBarWidth(20)
            }.width(100.percent).height(100.percent)
        }.height(100.percent)
    }

    //选择文件
    func selectFile(fileSelectStatus: FileSelectStatus) {
        let picker = DocumentViewPicker(getContext())
        let documentSelectCallback = {
            errorCode: Option<AsyncError>, data: Option<Array<String>> => match (errorCode) {
                case Some(e) => msgHistory += "选择失败,错误码:${e.code}\r\n"
                case _ => match (data) {
                    case Some(value) =>
                        fileSelectStatus.fileUri = value[0]
                        fileSelectStatus.fileSelected = true
                    case _ => ()
                }
            }
        }
        picker.select(documentSelectCallback, option: DocumentSelectOptions(selectMode: DocumentSelectMode.MIXED))
    }

    //启动tls服务器
    func startServer() {
        let socketAddress = SocketAddress("0.0.0.0", port)

        //回显TcpSocket服务端
        echoServer = TcpServerSocket(bindAt: socketAddress)

        let tlsCfg = getTlsServerCfg()
        //允许恢复tls会话
        let sessionContext = TlsSessionContext.fromName("echo-server")
        //启动一个线程监听客户端连接
        spawn {
            //绑定到本地端口
            echoServer?.bind()
            msgHistory += "绑定到本地端口,等待连接...\r\n"
            running = true
            while (true) {
                //已接受客户端连接
                let acceptEchoSocket = echoServer?.accept()
                if (let Some(echoSocket) <- acceptEchoSocket) {
                    msgHistory += "接受新的连接,对端地址为:${echoSocket.remoteAddress.kapString()}\r\n"
                    //启动一个线程处理新的socket
                    spawn {
                        try {
                            //生成服务端TLS套接字
                            let tlsSocket = TlsSocket.server(echoSocket, sessionContext: sessionContext,
                                serverConfig: tlsCfg)
                            //握手
                            tlsSocket.handshake()
                            msgHistory += "已握手\r\n"
                            //处理加密通讯
                            dealWithEchoSocket(tlsSocket)
                        } catch (err: SocketException) {
                            println(err.message)
                        }
                    }
                }
            }
        }
    }

    //从socket读取数据并回写到socket
    func dealWithEchoSocket(echoSocket: TlsSocket) {
        //存放从socket读取数据的缓冲区
        let buffer = Array<UInt8>(1024, item: 0)

        while (true) {
            //从socket读取数据
            var readCount = echoSocket.read(buffer)
            if (readCount > 0) {
                //把接收到的数据转换为字符串
                let content = String.fromUtf8(buffer[0..readCount])
                msgHistory += "[${echoSocket.remoteAddress}]:${content}\r\n"
                //回写客户端,把content写入echoSocket
                echoSocket.write(content.toArray())
            }
        }
    }

    //停止tls服务器
    func stopServer() {
        echoServer?.close()
        running = false
        msgHistory += "服务已停止\r\n"
    }

    //获取服务端TLS配置信息
    func getTlsServerCfg() {
        //得到服务端x509数字证书
        let x509 = getCert(certFileStatus.fileUri)
        //得到服务端私钥
        let privateKey = getPrivateKey(keyFileStatus.fileUri)

        var tlsCfg = TlsServerConfig(x509, privateKey)

        //设置支持的TLS版本
        tlsCfg.maxVersion = TlsVersion.V1_3
        tlsCfg.minVersion = TlsVersion.V1_2
        return tlsCfg
    }

    //获取私钥
    func getPrivateKey(keyPath: String) {
        //获取文件在沙箱cache文件夹的路径
        let sandboxFilePath = getSandboxFilePath(keyPath)

        //从沙箱读取证书文件信息
        let certContent = FileFs.readText(sandboxFilePath)
        return PrivateKey.decodeFromPem(certContent)
    }

    //把文件复制到沙箱并返回沙箱中的文件路径
    func getSandboxFilePath(oriFilePath: String) {
        let fileName = getFileNameFromPath(oriFilePath)
        let file = FileFs.open(oriFilePath)

        //构造文件在沙箱cache文件夹的路径
        let sandboxFilePath = getContext().filesDir.replace("files", "cache") + "/" + fileName

        //复制私钥到沙箱给定路径
        FileFs.copyFile(file.fd, sandboxFilePath)
        //关闭文件
        FileFs.close(file)

        return sandboxFilePath
    }

    //获取数字证书
    func getCert(certPath: String) {
        //获取文件在沙箱cache文件夹的路径
        let sandboxFilePath = getSandboxFilePath(certPath)

        //从沙箱读取证书文件信息
        let certContent = FileFs.readText(sandboxFilePath)
        return X509Certificate.decodeFromPem(certContent)
    }

    //从文件路径获取文件名称
    public func getFileNameFromPath(filePath: String) {
        let segments = filePath.split('/')
        //文件名称
        return segments[segments.size - 1]
    }

    //获取Ability上下文
    func getContext(): AbilityContext {
        match (globalAbilityContext) {
            case Some(context) => context
            case _ => throw Exception("获取全局Ability上下文异常")
        }
    }
}

步骤6:编译运行,可以使用模拟器或者真机。

步骤7:按照本文第2部分“TLS回声服务器演示”操作即可。

4. 代码分析

仓颉语言版本的TLS服务器和ArkTS版本的实现差异非常大,基本没有相似性,相对来说,仓颉语言版本更靠底层一些,首先是启动一个TCP服务器,在指定的端口进行监听,在监听到新的客户端连接时,启动一个新的线程处理该连接,该线程调用TlsSocket的server函数生成一个服务端TLS套接字,接着处理该套接字的读写,主线程则继续监听新的连接。

(本文作者原创,除非明确授权禁止转载)

本文源码地址:
https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tls/TLSEchoServer4Cj

本系列源码地址:
https://gitee.com/zl3624/harmonyos_network_samples

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值