OpenNHP客户端代理SDK介绍

English


1 客户端代理SDK介绍

1.1 介绍

OpenNHP客户端代理SDK是对OpenNHP Agent服务的标准化封装。应用程序通过集成此SDK,可直接调用其提供的接口方法,快速实现应用程序与OpenNHP的整合。

在不同的运行环境下仅需将SDK程序编译成对应系统的SDK文件格式:

操作系统动态库文件
Linuxnhp-agent.so
Windowsnhp-agent.dll
MacOSnhp-agent.dylib
Androidlibnhpagent.so
IOSnhpagent.xcframework

1.2 SDK的开发

OpenNHP中提供了SDK样例源码,样例中包含可能用到的初始化代理、循环敲门、取消循环敲门、单次敲门、取消单次敲门、增加nhp-server服务、设置客户端用户信息及密钥注册等方法。SDK开发人员可直接将OpenNHP项目中提供的SDK源码样例编程成相应的SDK文件直接进行调用,或参照SDK源码样例完成自定义的SDK开发。

SDK样例源码:opennhp/endpoints/agent/main/export.go

package main

/*
#include <stdlib.h>
*/
import "C"

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strings"
	"unsafe"

	"github.com/OpenNHP/opennhp/endpoints/agent"
	"github.com/OpenNHP/opennhp/nhp/common"
	"github.com/OpenNHP/opennhp/nhp/core"
)

var gAgentInstance *agent.UdpAgent
var gWorkingDir string
var gLogLevel int

func deepCopyCString(c_str *C.char) string {
	if c_str == nil {
		return ""
	}
	goStr := C.GoString(c_str)
	return strings.Clone(goStr)
}

// Release the memory of the string buffer generated by NHPSDK.
//
//export nhp_free_cstring
func nhp_free_cstring(ptr *C.char) {
	C.free(unsafe.Pointer(ptr))
}

// Initialization of the nhp_agent instance working directory path:
// The configuration files to be read are located under workingdir/etc/,
// and log files will be generated under workingdir/logs/.
//
// Input:
// workingDir: the working directory path for the agent
// logLevel: 0: silent, 1: error, 2: info, 3: debug, 4: verbose
//
// Return:
// Whether agent instance has been initialized successfully.
//
//export nhp_agent_init
func nhp_agent_init(workingDir *C.char, logLevel C.int) bool {
	if gAgentInstance != nil {
		return true
	}

	gAgentInstance = &agent.UdpAgent{}
	err := gAgentInstance.Start(deepCopyCString(workingDir), int(logLevel))
	if err != nil {
		return false
	}

	return true
}

// Synchronously stop and release nhp_agent.
//
//export nhp_agent_close
func nhp_agent_close() {
	if gAgentInstance == nil {
		return
	}

	gAgentInstance.Stop()
	gAgentInstance = nil
}

// Read the user information, resource information, server information,
// and other configuration files written under workingdir/etc,
// and asynchronously start the loop knocking thread.
//
// Input: None
//
// Return:
// -1: Uninitialized error
// >=0: The number of resources requested to knock by the knocking thread at the time of the call
//
//	(knocking resources will be synchronized with changes in the configuration in workingdir/etc/resource.toml).
//
//export nhp_agent_knockloop_start
func nhp_agent_knockloop_start() C.int {
	if gAgentInstance == nil {
		return -1
	}

	count := gAgentInstance.StartKnockLoop()
	return C.int(count)
}

// Synchronously stop the loop, knock-on sub thread.
//
//export nhp_agent_knockloop_stop
func nhp_agent_knockloop_stop() {
	if gAgentInstance == nil {
		return
	}

	gAgentInstance.StopKnockLoop()
}

// Setting agent's represented user information
//
// Input:
// userId: User identification (optional, but not recommended to be empty)
// devId: Device identification (optional)
// orgId: Organization or company identification (optional)
// userData: Additional fields required to interface with backend services (json format string, optional)
//
// Return:
// Whether the user information is set successfully
//
//export nhp_agent_set_knock_user
func nhp_agent_set_knock_user(userId *C.char, devId *C.char, orgId *C.char, userData *C.char) bool {
	if gAgentInstance == nil {
		return false
	}
	jsonStr := deepCopyCString(userData)
	var data map[string]any
	if len(jsonStr) > 0 {
		err := json.Unmarshal([]byte(jsonStr), &data)
		if err != nil {
			return false
		}
	}

	gAgentInstance.SetDeviceId(deepCopyCString(devId))
	gAgentInstance.SetKnockUser(deepCopyCString(userId), deepCopyCString(orgId), data)
	return true
}

// Add an NHP server information to the agent for use in knocking on the door
// (the agent can initiate different knocking requests to multiple NHP servers).
//
// Input:
// pubkey: Public key of the NHP server
// ip: IP address of the NHP server
// host: Domain name of the NHP server (if a domain name is set, the ip item is optional)
// port: Port number for the NHP server to operate (if set to 0, the default port 62206 will be used)
// expire: Expiration time of the NHP server's public key (in epoch seconds, set to 0 for permanent)
//
// Return:
// Whether the server information has been successfully added.
//
//export nhp_agent_add_server
func nhp_agent_add_server(pubkey *C.char, ip *C.char, host *C.char, port C.int, expire int64) bool {
	if gAgentInstance == nil {
		return false
	}

	if pubkey == nil || (ip == nil && host == nil) {
		return false
	}

	serverPort := int(port)
	if serverPort == 0 {
		serverPort = 62206 // use default server listening port
	}

	serverPeer := &core.UdpPeer{
		Type:         core.NHP_SERVER,
		PubKeyBase64: deepCopyCString(pubkey),
		Ip:           deepCopyCString(ip),
		Port:         serverPort,
		Hostname:     deepCopyCString(host),
		ExpireTime:   expire,
	}
	gAgentInstance.AddServer(serverPeer)
	return true
}

// Delete NHP server information from the agent
//
// Input:
// pubkey: NHP server public key
//
//export nhp_agent_remove_server
func nhp_agent_remove_server(pubkey *C.char) {
	if gAgentInstance == nil {
		return
	}
	if pubkey == nil {
		return
	}

	gAgentInstance.RemoveServer(deepCopyCString(pubkey))
}

// Please add a resource information for the agent to use for knocking on the door
// (the agent can initiate a knock-on request for different resources)
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Return:
// Whether the resource information has been added successfully
//
//export nhp_agent_add_resource
func nhp_agent_add_resource(aspId *C.char, resId *C.char, serverIp *C.char, serverHostname *C.char, serverPort C.int) bool {
	if gAgentInstance == nil {
		return false
	}

	if aspId == nil || resId == nil || (serverIp == nil && serverHostname == nil) {
		return false
	}

	resource := &agent.KnockResource{
		AuthServiceId:  deepCopyCString(aspId),
		ResourceId:     deepCopyCString(resId),
		ServerIp:       deepCopyCString(serverIp),
		ServerHostname: deepCopyCString(serverHostname),
		ServerPort:     int(serverPort),
	}
	err := gAgentInstance.AddResource(resource)
	return err == nil
}

// Delete resource information from the agent
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
//
//export nhp_agent_remove_resource
func nhp_agent_remove_resource(aspId *C.char, resId *C.char) {
	if gAgentInstance == nil {
		return
	}

	if aspId == nil || resId == nil {
		return
	}

	gAgentInstance.RemoveResource(deepCopyCString(aspId), deepCopyCString(resId))
}

// The agent initiates a single knock on the door request to the server hosting the resource
//
// Input:
// aspId: Authentication service provider identifier
// resId: Resource identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Returns:
// The server's response message (json format string buffer pointer):
// "errCode": Error code (string, "0" indicates success)
// "errMsg": Error message (string)
// "resHost": Resource server address ("resHost": {"Server Name 1":"Server Hostname 1", "Server Name 2":"Server Hostname 2", ...})
// "opnTime": Door opening duration (integer, in seconds)
// "aspToken": Token generated after authentication by the ASP (optional)
// "agentAddr": Agent's IP address from the perspective of the NHP server
// "preActs": Pre-connection information related to the resource (optional)
// "redirectUrl": HTTP redirection link (optional)
//
// It is necessary to call nhp_agent_add_server before calling,
// to add the NHP server's public key, address, and other information to the agent
// The caller is responsible for calling nhp_free_cstring to release the returned char* pointer
//
//export nhp_agent_knock_resource
func nhp_agent_knock_resource(aspId *C.char, resId *C.char, serverIp *C.char, serverHostname *C.char, serverPort C.int) *C.char {
	ackMsg := &common.ServerKnockAckMsg{}

	func() {
		if gAgentInstance == nil {
			ackMsg.ErrCode = common.ErrNoAgentInstance.ErrorCode()
			ackMsg.ErrMsg = common.ErrNoAgentInstance.Error()
			return
		}

		if aspId == nil || resId == nil || (serverIp == nil && serverHostname == nil) {
			ackMsg.ErrCode = common.ErrInvalidInput.ErrorCode()
			ackMsg.ErrMsg = common.ErrInvalidInput.Error()
			return
		}

		resource := &agent.KnockResource{
			AuthServiceId:  deepCopyCString(aspId),
			ResourceId:     deepCopyCString(resId),
			ServerIp:       deepCopyCString(serverIp),
			ServerHostname: deepCopyCString(serverHostname),
			ServerPort:     int(serverPort),
		}

		peer := gAgentInstance.FindServerPeerFromResource(resource)
		if peer == nil {
			ackMsg.ErrCode = common.ErrKnockServerNotFound.ErrorCode()
			ackMsg.ErrMsg = common.ErrKnockServerNotFound.Error()
			return
		}

		target := &agent.KnockTarget{
			KnockResource: *resource,
			ServerPeer:    peer,
		}

		ackMsg, _ = gAgentInstance.Knock(target)
	}()

	bytes, _ := json.Marshal(ackMsg)
	ret := C.CString(string(bytes))

	return ret
}

// The agent explicitly informs the NHP server to exit its access permission to the resource.
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Return:
// Whether the exit was successful
//
// It is necessary to call nhp_agent_add_server before calling, to add the NHP server's public key, address, and other information to the agent.
//
//export nhp_agent_exit_resource
func nhp_agent_exit_resource(aspId *C.char, resId *C.char, serverIp *C.char, serverHostname *C.char, serverPort C.int) bool {
	var err error
	ackMsg := &common.ServerKnockAckMsg{}

	func() {
		if gAgentInstance == nil {
			ackMsg.ErrCode = common.ErrNoAgentInstance.ErrorCode()
			ackMsg.ErrMsg = common.ErrNoAgentInstance.Error()
			err = common.ErrNoAgentInstance
			return
		}

		if aspId == nil || resId == nil || (serverIp == nil && serverHostname == nil) {
			ackMsg.ErrCode = common.ErrInvalidInput.ErrorCode()
			ackMsg.ErrMsg = common.ErrInvalidInput.Error()
			err = common.ErrInvalidInput
			return
		}

		resource := &agent.KnockResource{
			AuthServiceId:  deepCopyCString(aspId),
			ResourceId:     deepCopyCString(resId),
			ServerIp:       deepCopyCString(serverIp),
			ServerHostname: deepCopyCString(serverHostname),
			ServerPort:     int(serverPort),
		}

		peer := gAgentInstance.FindServerPeerFromResource(resource)
		if peer == nil {
			ackMsg.ErrCode = common.ErrKnockServerNotFound.ErrorCode()
			ackMsg.ErrMsg = common.ErrKnockServerNotFound.Error()
			err = common.ErrKnockServerNotFound
			return
		}

		target := &agent.KnockTarget{
			KnockResource: *resource,
			ServerPeer:    peer,
		}

		ackMsg, err = gAgentInstance.ExitKnockRequest(target)
	}()

	return err == nil
}

// cipherType: 0-curve25519; 1-sm2
// result: "privatekey"|"publickey"
// caller is responsible to free the returned char* pointer
//
//export nhp_generate_keys
func nhp_generate_keys(cipherType C.int) *C.char {
	var e core.Ecdh
	switch core.EccTypeEnum(cipherType) {
	case core.ECC_SM2:
		e = core.NewECDH(core.ECC_SM2)
	case core.ECC_CURVE25519:
		fallthrough
	default:
		e = core.NewECDH(core.ECC_CURVE25519)
	}
	pub := e.PublicKeyBase64()
	priv := e.PrivateKeyBase64()

	res := fmt.Sprintf("%s|%s", priv, pub)
	pRes := C.CString(res)

	return pRes
}

// cipherType: 0-curve25519; 1-sm2
// privateBase64: private key in base64 format
// result: "publickey"
// caller is responsible to free the returned char* pointer
//
//export nhp_privkey_to_pubkey
func nhp_privkey_to_pubkey(cipherType C.int, privateBase64 *C.char) *C.char {
	privKey := deepCopyCString(privateBase64)
	privKeyBytes, err := base64.StdEncoding.DecodeString(privKey)
	if err != nil {
		return nil
	}

	e := core.ECDHFromKey(core.EccTypeEnum(cipherType), privKeyBytes)
	if e == nil {
		return nil
	}
	pub := e.PublicKeyBase64()
	pPub := C.CString(pub)

	return pPub
}

2 客户端代理SDK的适配

2.1 桌面版SDK

2.1.1 Windows

2.1.1.1 环境准备

Windows下的编译环境参照编译源代码系统需求章节windows部分完成编译环境的搭建。

2.1.1.2 编译SDK
  • 方法一:运行代码根目录下BAT文件,该方法可在编译整个OpenNHP执行文件的同时完成SDK样例的编译 build.bat
    (注:如果在windows下编译过程中出现错误,请尝试此编译方法:在Visual Studio的developer command prompt for VS命令窗口中,切换到项目目录,执行./build.bat命令)

  • 方法二:单独编译SDK的.dll文件指令:

    opennhp/endpoints/agent/main/目录下执行

    go build -trimpath -buildmode=c-shared -ldflags '-s -w' -v -o nhp-agent.dll main.go export.go

    (注:因为export.go文件中没有main方法,所有编译命令中加入了main.go,自定义的SDK代码文件中在加入main方法后,在编译时编译指令只需要SDK代码文件,不需要在引入main.go文件)

2.1.1.3 SDK适配
  • java

    java程序可以通过jna来完成对SDK的方法调用:

    • OpennhpLibrary接口加载OpenNHP agent SDK

      package org.example;
          
      import com.sun.jna.Library;
      import com.sun.jna.Native;
          
      /**
       * OpenNHP agent sdk interface
       *
       * @author haochangjiu
       * @version JDK 8
       * @className OpennhpLibrary
       * @date 2025/10/27
       */
      public interface OpennhpLibrary extends Library {
          // load OpenNHP agent sdk
          OpennhpLibrary INSTANCE = Native.load("nhp-agent", OpennhpLibrary.class);
          
          /**
           * @description Initialization of the nhp_agent instance working directory path:
           *              The configuration files to be read are located under workingdir/etc/,
           *              and log files will be generated under workingdir/logs/.
           * @param workingDir: the working directory path for the agent
           * @param logLevel:   0: silent, 1: error, 2: info, 3: debug, 4: verbose
           *                    return boolean Whether agent instance has been initialized successfully.
           * @return boolean
           * @author haochangjiu
           * @date 2025/10/27
           * {@link boolean}
           */
          boolean nhp_agent_init(String workingDir, int logLevel);
          
          /**
           * @description Synchronously stop and release nhp_agent.
           * @author haochangjiu
           * @date 2025/10/27
           */
          void nhp_agent_close();
          /**
           * @description Read the user information, resource information, server information,
           *              and other configuration files written under workingdir/etc,
           *              and asynchronously start the loop knocking thread.
           * @return int
           * @author haochangjiu
           * @date 2025/10/27
           * {@link int}
           */
          int nhp_agent_knockloop_start();
          
          /**
           * @description Synchronously stop the loop, knock-on sub thread
           * @author hangchangjiu
           * @date 2025/10/27
           */
          void nhp_agent_knockloop_stop();
      }
      
    • 程序主入口,调用SDK

      package org.example;
          
      import java.util.Scanner;
          
      /**
       * Application for calling the OpenNHP agent SDK
       *
       * @author haochangjiu
       * @version JDK 8
       * @className App
       * @date 2025/10/27
       */
      public class App {
          public static void main(String[] args) throws Exception {
      //        Initialize and start the OpenNHP agent SDK service
              boolean initFlag = OpennhpLibrary.INSTANCE.nhp_agent_init("D:\\console-workspace\\opennhp-knock", 3);
              if (!initFlag) {
                  System.out.println("NHP Agent init failed");
                  System.exit(0);
              }
      //        Invoke methods in the OpenNHP agent SDK via input commands
              Scanner scanner = new Scanner(System.in);
          
              while (true) {
                  System.out.print("> ");
                  if (scanner.hasNextLine()) {
                      String input = scanner.nextLine().trim();
                      if ("knock".equalsIgnoreCase(input)) {
                          System.out.println("start the loop knocking thread...");
                          OpennhpLibrary.INSTANCE.nhp_agent_knockloop_start();
                      } else if ("cancel".equalsIgnoreCase(input)) {
                          System.out.println("stop the loop knocking thread...");
                          OpennhpLibrary.INSTANCE.nhp_agent_knockloop_stop();
                      } else if ("exit".equalsIgnoreCase(input)) {
                          System.out.println("exit nhp agent service...");
                          OpennhpLibrary.INSTANCE.nhp_agent_close();
                          break;
                      } else {
                          System.out.println("invalid input");
                      }
                  }
              }
              scanner.close();
          }
      }
      
  • c/c++

    c/c++程序参照项目中SDK调用样例程序opennhp/endpoints/agent/sdkdemo/nhp-agent-demo.c来完成对客户端代理SDK的整合

    #include <stdio.h>
    #include <unistd.h>
    #include "nhp-agent.h"
      
    int main() {
        // Initialize nhp_agent, only one nhp_agent singleton is allowed per process.
        nhp_agent_init(".", 3);
      
        // Set the user information for the knock-on-the-door feature.
        nhp_agent_set_knock_user("zengl", NULL, NULL, NULL);
      
        // Set NHP server information
        // If there is already a configuration file for the server, the call to nhp_agent_add_server can be omitted
        // Timestamp date is visible at https://unixtime.org/
        nhp_agent_add_server("replace_with_actual_publickeybase64", "192.168.1.66", NULL, 62206, 1748908471);
      
        // Send a request to the server to access the resource example/demo, and return information in the form of a JSON format string
        // Note: The resource information here is an independent input, and is unrelated to the resource information saved in the configuration file
        char *ret = nhp_agent_knock_resource("example", "demo", "192.168.1.66", NULL, 62206);
        printf("knock return: %s\n", ret);
      
        // Immediately close the agent's access to the example/demo resources,
        // if not invoked, access permission will automatically close after the door opening duration has passed.
        nhp_agent_exit_resource("example", "demo", "192.168.1.66", NULL, 62206);
      
        // Turn off and release nhp_agent.
        nhp_agent_close();
        return 0;
    }
    
  • python

    使用Python标准库中的ctypes完成对SDK的整合

    import ctypes
    from time import sleep
      
    # Windows
    nhp_agent = ctypes.CDLL('nhp-agent.dll')
    # Linux
    # mylib = ctypes.CDLL('./nhp-agent.so')
    # macOS
    # mylib = ctypes.CDLL('./nhp-agent.dylib')
      
    nhp_agent.nhp_agent_init.argtypes = [ctypes.c_char_p, ctypes.c_int]
    nhp_agent.nhp_agent_init.restype = ctypes.c_bool
      
    nhp_agent.nhp_agent_init.restype = ctypes.c_int
      
      
      
    if __name__ == '__main__':
        flag = nhp_agent.nhp_agent_init(ctypes.c_char_p(b"D:\\nhpagent"),3)
        if flag:
            print("nhp-agent init success")
        else:
            print("nhp-agent init failed")
        # start the loop knocking thread
        status = nhp_agent.nhp_agent_knockloop_start()
        if status >= 0:
            print("nhp-agent knockloop success")
            # Delay between calls
            sleep(30)
        else:
            print("nhp-agent knockloop failed")
      
        # stop nhp_agent
        nhp_agent.nhp_agent_close()
    
  • 其他语言

    其他开发语言(C#、Rust、Go、Nodejs)可根据各语言独有的对SDK文件调用方法来完成SDK适配,其中Go也可以引入OpenNHP中agent部分源码,来完成对OpenNHP的适配,不需要开发SDK。

2.1.2 Linux

2.1.2.1 环境准备

Linux下的编译环境参照编译源代码系统需求章节Linux部分完成编译环境的搭建。

2.1.2.2 编译SDK
  • 方法一:运行代码根目录下脚本 make

  • 方法二:单独编译SDK的.so文件指令:

    opennhp/endpoints/agent/main/目录下执行

    go build -trimpath -buildmode=c-shared -ldflags '-s -w' -v -o nhp-agent.so main.go export.go

    (注:因为export.go文件中没有main方法,所有编译命令中加入了main.go,自定义的SDK代码文件中在加入main方法后,在编译时编译指令只需要SDK代码文件,不需要在引入main.go文件)

2.1.2.3 SDK适配

Linux下对SDK的适配与Windows一致,代码参照章节2.1.1.3

(注:需确保程序能正常加载到SDK的.so文件)

2.1.3 MacOS

2.1.3.1 环境准备

MacOS下的编译环境参照编译源代码系统需求章节MacOS部分完成编译环境的搭建。

2.1.3.2 编译SDK

通过make指令编译后的SDK是.so文件,而在MacOS上的动态库文件是.dylib格式,因此需要通过单独执行进行SDK的编译。

opennhp/endpoints/agent/main/目录下执行编译指令

GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -buildmode=c-shared -o nhp-agent.dylib main.go export.go

(注:因为export.go文件中没有main方法,所有编译命令中加入了main.go,自定义的SDK代码文件中在加入main方法后,在编译时编译指令只需要SDK代码文件,不需要在引入main.go文件)

2.1.3.3 SDK适配

MacOS下对SDK的适配与Windows一致,代码参照章节2.1.1.3

(注:需确保程序能正常加载到SDK的.dylib文件)

2.2 移动版SDK

2.2.1 Android

2.2.1.1 环境准备
  • 在Linux上完成Android的客户端代理SDK编译,需要参照编译源代码系统需求章节Linux部分完成编译环境的搭建。

  • Android NDK环境:

    • 下载并安装Android NDK

      wget https://dl.google.com/android/repository/android-ndk-r25b-linux.zip

      unzip android-ndk-r25b-linux.zip

    • 设置环境变量

      • 编辑bashrc文件

        vim ~/.bashrc

      • 增加环境变量

        #设置 NDK 路径(根据你的实际安装路径)
        export ANDROID_NDK_HOME=/opt/android-ndk-r25b/
        export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64
        #对于 arm64-v8a 使用 aarch64 工具链
        export CC=$TOOLCHAIN/bin/aarch64-linux-android21-clang
        export CXX=$TOOLCHAIN/bin/aarch64-linux-android21-clang++
        
      • 使配置生效

        source ~/.bashrc

2.2.1.2 编译SDK

opennhp/endpoints/agent/main/目录下执行编译指令

GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build -buildmode=c-shared -o libnhpagent.so main.go export.go

(注:Android项目在通过jna加载.so文件时会在输入的.so文件名称前增加lib,在编译SDK时名称应以lib开头,例如:libnhpagent.so)

2.2.1.3 SDK适配
  • Android配置(Kotlin和java通用)

    • 1、在build.gradle(app)中加入如下配置: 在android下加入

      sourceSets {
              main {
                  jniLibs.srcDirs = ['src/main/jniLibs', 'libs']
              }
          }
      

      dependencies 下加入如下依赖 // 注意:安卓推荐使用适配的 JNA 版本,如 5.13.0 及以上 implementation 'net.java.dev.jna:jna:5.13.0@aar' // 权限请求框架:https://github.com/getActivity/XXPermissions implementation libs.xxpermissions

      libs.versions.toml文件 [versions]下加入 xxpermissions = "18.6" [libraries]下加入 xxpermissions = { module = "com.github.getActivity:XXPermissions", version.ref = "xxpermissions" }

    • 2、AndroidManifest.xml文件加入文件存储读写权限

      <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
      <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
      <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
      <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
      
  • Kotlin

    Kotlin开发Android应用适配SDK样例

      package com.example.androidtestsoapp
        
      import android.os.Bundle
      import android.os.Environment
      import android.util.Log
      import androidx.activity.ComponentActivity
      import androidx.activity.compose.setContent
      import androidx.activity.enableEdgeToEdge
      import androidx.compose.foundation.layout.fillMaxSize
      import androidx.compose.foundation.layout.padding
      import androidx.compose.material3.Scaffold
      import androidx.compose.material3.Text
      import androidx.compose.runtime.Composable
      import androidx.compose.ui.Modifier
      import androidx.compose.ui.tooling.preview.Preview
      import com.example.androidtestsoapp.ui.theme.AndroidTestSoAppTheme
      import com.hjq.permissions.Permission
      import com.hjq.permissions.XXPermissions
      import java.io.File
        
      class MainActivity : ComponentActivity() {
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              enableEdgeToEdge()
              setContent {
                  AndroidTestSoAppTheme {
                      Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                          Greeting(
                              name = "Android",
                              modifier = Modifier.padding(innerPadding)
                          )
                      }
                  }
              }
              // Request permissions - read/write
              XXPermissions.with(this)
                  .permission(Permission.WRITE_EXTERNAL_STORAGE)
                  .permission(Permission.READ_MEDIA_IMAGES)
                  .permission(Permission.READ_MEDIA_VIDEO)
                  .permission(Permission.READ_MEDIA_AUDIO)
                  .request { permissions, allGranted ->
                      if (allGranted) {
                          Log.d("MainActivity", "Permissions granted")
                          performFileOperations()
                      } else {
                          Log.d("MainActivity", "Permissions not granted")
                      }
                  }
          }
      }
        
      /**
       * Need to place the nhp folder containing the etc folder in the phone's download folder
       * After reading the phone storage download directory, call OpennhpLibrary
       */
      private fun performFileOperations() {
          // Read phone storage download directory
          val appDir = Environment.getExternalStorageDirectory().toString() + File.separator + "download"
          // Check if zero folder exists in download
          val file = File(appDir)
          if (!file.exists()) {
              Log.d("MainActivity", "Download folder does not exist")
              return
          }
          Log.d("MainActivity", "Download folder exists")
          val appDir1 = Environment.getExternalStorageDirectory().toString() + File.separator + "download" + File.separator + "nhp"
          // Check if nhp folder exists in download
          val file1 = File(appDir1)
          if (!file1.exists()) {
              Log.d("MainActivity", "nhp folder does not exist")
              return
          }
          val appDir2 = Environment.getExternalStorageDirectory().toString() + File.separator + "download" + File.separator + "nhp"+ File.separator + "etc"
          // Check if etc folder exists in download
          val file2 = File(appDir2)
          if (!file2.exists()) {
              Log.d("MainActivity", "Etc folder does not exist")
              return
          }
        
          val initFlag = OpennhpLibrary.INSTANCE.nhp_agent_init(appDir1, 2)
          if (!initFlag) {
              println("NHP Agent init failed")
              return
          }
          println("start the loop knocking thread...")
          val flag:Int = OpennhpLibrary.INSTANCE.nhp_agent_knockloop_start()
          // Print result
          if (flag > 0) {
              println("NHP Agent knockloop start success")
          } else {
              println("NHP Agent knockloop start failed")
          }
      }
        
      @Composable
      fun Greeting(name: String, modifier: Modifier = Modifier) {
          Text(
              text = "Hello $name!",
              modifier = modifier
          )
      }
        
      @Preview(showBackground = true)
      @Composable
      fun GreetingPreview() {
          AndroidTestSoAppTheme {
              Greeting("Android")
          }
      }
    
  • java

    • 创建OpennhpLibrary接口来加载OpenNHP agent SDK。 (注:Android项目在引入.so文件时,会在动态库文件前加入lib,即代码中加载的SDK名称为nhpagent,实际程序加载的SDK为libnhpagent.so文件)

      package org.example;
          
      import com.sun.jna.Library;
      import com.sun.jna.Native;
          
      /**
       * OpenNHP agent sdk interface
       *
       * @author haochangjiu
       * @version JDK 8
       * @className OpennhpLibrary
       * @date 2025/10/27
       */
      public interface OpennhpLibrary extends Library {
          // load OpenNHP agent sdk
          OpennhpLibrary INSTANCE = Native.load("nhpagent", OpennhpLibrary.class);
          
          /**
           * @description Initialization of the nhp_agent instance working directory path:
           *              The configuration files to be read are located under workingdir/etc/,
           *              and log files will be generated under workingdir/logs/.
           * @param workingDir: the working directory path for the agent
           * @param logLevel:   0: silent, 1: error, 2: info, 3: debug, 4: verbose
           *                    return boolean Whether agent instance has been initialized successfully.
           * @return boolean
           * @author haochangjiu
           * @date 2025/10/27
           * {@link boolean}
           */
          boolean nhp_agent_init(String workingDir, int logLevel);
          
          /**
           * @description Synchronously stop and release nhp_agent.
           * @author haochangjiu
           * @date 2025/10/27
           */
          void nhp_agent_close();
          /**
           * @description Read the user information, resource information, server information,
           *              and other configuration files written under workingdir/etc,
           *              and asynchronously start the loop knocking thread.
           * @return int
           * @author haochangjiu
           * @date 2025/10/27
           * {@link int}
           */
          int nhp_agent_knockloop_start();
          
          /**
           * @description Synchronously stop the loop, knock-on sub thread
           * @author hangchangjiu
           * @date 2025/10/27
           */
          void nhp_agent_knockloop_stop();
      }
      
    • 调用SDK:样例中将配置文件的etc文件夹放在手机下载目录的nhp目录下

      package org.example;
          
      import android.os.Bundle;
      import android.os.Environment;
      import android.util.Log;
      import androidx.appcompat.app.AppCompatActivity;
          
          
      import com.OpennhpLibrary;
      import com.fancy.zerotrust.R;
          
      import java.io.File;
          
      public class MainActivity extends AppCompatActivity {
          
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              // Read the phone's storage download directory.
              String appDir = Environment.getExternalStorageDirectory() + File.separator + "download";
              // Does the nhp directory exist in the downloads
              File file = new File(appDir);
              if (!file.exists()) {
                  Log.d("MainActivity","download file not exist!");
                  return;
              }
              Log.d("MainActivity","download file exist!");
              String appDir1 = Environment.getExternalStorageDirectory() + File.separator + "download"+ File.separator + "nhp";
              boolean initFlag = OpennhpLibrary.INSTANCE.nhp_agent_init(appDir1, 3);
              if (!initFlag) {
                  System.out.println("NHP Agent init failed");
                  System.exit(0);
              }
              System.out.println("start the loop knocking thread...");
              OpennhpLibrary.INSTANCE.nhp_agent_knockloop_start();
          }
      }
      

2.2.2 IOS

2.2.2.1 环境准备
  • 在MacOS上完成IOS的客户端代理SDK编译,需要参照编译源代码系统需求章节MacOS部分完成编译环境的搭建。

  • 确保已完成Xcode,如未安装Xcode需去App Store下载安装。

  • 安装gomobile:

    • 安装gomobile

      go install golang.org/x/mobile/cmd/gomobile@latest

    • 初始化gomobile

      gomobile init

2.2.2.2 SDK样例

在编译IOS所需的.xcframework文件时,需要导出的方法名称必须大写开头,同时参数类型为标准的Go语言类型,不能是C.int和C.char。另外一点需要注意的是代码不能在package main下,将程序移动到新建的sdk路径下。

根据OpenNHP中的export.go文件进行修改如下:

package sdk

import "C"
import (
	"encoding/base64"
	"encoding/json"
	"fmt"

	"github.com/OpenNHP/opennhp/endpoints/agent"
	"github.com/OpenNHP/opennhp/nhp/common"
	"github.com/OpenNHP/opennhp/nhp/core"
)

var gAgentInstance *agent.UdpAgent
var gWorkingDir string
var gLogLevel int

// Initialization of the nhp_agent instance working directory path:
// The configuration files to be read are located under workingdir/etc/,
// and log files will be generated under workingdir/logs/.
//
// Input:
// workingDir: the working directory path for the agent
// logLevel: 0: silent, 1: error, 2: info, 3: debug, 4: verbose
//
// Return:
// Whether agent instance has been initialized successfully.
func NhpAgentInit(workingDir string, logLevel int) bool {
	if gAgentInstance != nil {
		return true
	}

	gAgentInstance = &agent.UdpAgent{}
	err := gAgentInstance.Start(workingDir, logLevel)
	if err != nil {
		return false
	}

	return true
}

// Synchronously stop and release nhp_
func NhpAgentClose() {
	if gAgentInstance == nil {
		return
	}

	gAgentInstance.Stop()
	gAgentInstance = nil
}

// Read the user information, resource information, server information,
// and other configuration files written under workingdir/etc,
// and asynchronously start the loop knocking thread.
//
// Input: None
//
// Return:
// -1: Uninitialized error
// >=0: The number of resources requested to knock by the knocking thread at the time of the call
//
//	(knocking resources will be synchronized with changes in the configuration in workingdir/etc/resource.toml).
//
//export NhpAgentKnockloopStart
func NhpAgentKnockloopStart() int {
	if gAgentInstance == nil {
		return -1
	}

	count := gAgentInstance.StartKnockLoop()
	return count
}

// Synchronously stop the loop, knock-on sub thread.
func NhpAgentKnockloopStop() {
	if gAgentInstance == nil {
		return
	}

	gAgentInstance.StopKnockLoop()
}

// Setting agent's represented user information
//
// Input:
// userId: User identification (optional, but not recommended to be empty)
// devId: Device identification (optional)
// orgId: Organization or company identification (optional)
// userData: Additional fields required to interface with backend services (json format string, optional)
//
// Return:
// Whether the user information is set successfully
func NhpAgentSetKnockUser(userId string, devId string, orgId string, userData string) bool {
	if gAgentInstance == nil {
		return false
	}
	var data map[string]any
	if len(userData) > 0 {
		err := json.Unmarshal([]byte(userData), &data)
		if err != nil {
			return false
		}
	}

	gAgentInstance.SetDeviceId(devId)
	gAgentInstance.SetKnockUser(userId, orgId, data)
	return true
}

// Add an NHP server information to the agent for use in knocking on the door
// (the agent can initiate different knocking requests to multiple NHP servers).
//
// Input:
// pubkey: Public key of the NHP server
// ip: IP address of the NHP server
// host: Domain name of the NHP server (if a domain name is set, the ip item is optional)
// port: Port number for the NHP server to operate (if set to 0, the default port 62206 will be used)
// expire: Expiration time of the NHP server's public key (in epoch seconds, set to 0 for permanent)
//
// Return:
// Whether the server information has been successfully added.
func NhpAgentAddServer(pubkey string, ip string, host string, port int, expire int64) bool {
	if gAgentInstance == nil {
		return false
	}

	if len(pubkey) == 0 || (len(ip) == 0 && len(host) == 0) {
		return false
	}

	serverPort := int(port)
	if serverPort == 0 {
		serverPort = 62206 // use default server listening port
	}

	serverPeer := &core.UdpPeer{
		Type:         core.NHP_SERVER,
		PubKeyBase64: pubkey,
		Ip:           ip,
		Port:         serverPort,
		Hostname:     host,
		ExpireTime:   expire,
	}
	gAgentInstance.AddServer(serverPeer)
	return true
}

// Delete NHP server information from the agent
//
// Input:
// pubkey: NHP server public key
func NhpAgentRemoveServer(pubkey string) {
	if gAgentInstance == nil {
		return
	}
	if len(pubkey) == 0 {
		return
	}

	gAgentInstance.RemoveServer(pubkey)
}

// Please add a resource information for the agent to use for knocking on the door
// (the agent can initiate a knock-on request for different resources)
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Return:
// Whether the resource information has been added successfully
func NhpAgentAddResource(aspId string, resId string, serverIp string, serverHostname string, serverPort int) bool {
	if gAgentInstance == nil {
		return false
	}

	if len(aspId) == 0 || len(resId) == 0 || (len(serverIp) == 0 && len(serverHostname) == 0) {
		return false
	}

	resource := &agent.KnockResource{
		AuthServiceId:  aspId,
		ResourceId:     resId,
		ServerIp:       serverIp,
		ServerHostname: serverHostname,
		ServerPort:     serverPort,
	}
	err := gAgentInstance.AddResource(resource)
	return err == nil
}

// Delete resource information from the agent
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
func NhpAgentRemoveResource(aspId string, resId string) {
	if gAgentInstance == nil {
		return
	}

	if len(aspId) == 0 || len(resId) == 0 {
		return
	}

	gAgentInstance.RemoveResource(aspId, resId)
}

// The agent initiates a single knock on the door request to the server hosting the resource
//
// Input:
// aspId: Authentication service provider identifier
// resId: Resource identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Returns:
// The server's response message (json format string buffer pointer):
// "errCode": Error code (string, "0" indicates success)
// "errMsg": Error message (string)
// "resHost": Resource server address ("resHost": {"Server Name 1":"Server Hostname 1", "Server Name 2":"Server Hostname 2", ...})
// "opnTime": Door opening duration (integer, in seconds)
// "aspToken": Token generated after authentication by the ASP (optional)
// "agentAddr": Agent's IP address from the perspective of the NHP server
// "preActs": Pre-connection information related to the resource (optional)
// "redirectUrl": HTTP redirection link (optional)
//
// It is necessary to call NhpAgentAddServer before calling,
// to add the NHP server's public key, address, and other information to the agent
// The caller is responsible for calling NhpFreeCstring to release the returned char* pointer
func NhpAgentKnockResource(aspId string, resId string, serverIp string, serverHostname string, serverPort int) string {
	ackMsg := &common.ServerKnockAckMsg{}

	func() {
		if gAgentInstance == nil {
			ackMsg.ErrCode = common.ErrNoAgentInstance.ErrorCode()
			ackMsg.ErrMsg = common.ErrNoAgentInstance.Error()
			return
		}

		if len(aspId) == 0 || len(resId) == 0 || (len(serverIp) == 0 && len(serverHostname) == 0) {
			ackMsg.ErrCode = common.ErrInvalidInput.ErrorCode()
			ackMsg.ErrMsg = common.ErrInvalidInput.Error()
			return
		}

		resource := &agent.KnockResource{
			AuthServiceId:  aspId,
			ResourceId:     resId,
			ServerIp:       serverIp,
			ServerHostname: serverHostname,
			ServerPort:     serverPort,
		}

		peer := gAgentInstance.FindServerPeerFromResource(resource)
		if peer == nil {
			ackMsg.ErrCode = common.ErrKnockServerNotFound.ErrorCode()
			ackMsg.ErrMsg = common.ErrKnockServerNotFound.Error()
			return
		}

		target := &agent.KnockTarget{
			KnockResource: *resource,
			ServerPeer:    peer,
		}

		ackMsg, _ = gAgentInstance.Knock(target)
	}()

	bytes, _ := json.Marshal(ackMsg)

	return string(bytes)
}

// The agent explicitly informs the NHP server to exit its access permission to the resource.
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Return:
// Whether the exit was successful
//
// It is necessary to call NhpAgentAddServer before calling, to add the NHP server's public key, address, and other information to the
func NhpAgentExitResource(aspId string, resId string, serverIp string, serverHostname string, serverPort int) bool {
	var err error
	ackMsg := &common.ServerKnockAckMsg{}

	func() {
		if gAgentInstance == nil {
			ackMsg.ErrCode = common.ErrNoAgentInstance.ErrorCode()
			ackMsg.ErrMsg = common.ErrNoAgentInstance.Error()
			err = common.ErrNoAgentInstance
			return
		}

		if len(aspId) == 0 || len(resId) == 0 || (len(serverIp) == 0 && len(serverHostname) == 0) {
			ackMsg.ErrCode = common.ErrInvalidInput.ErrorCode()
			ackMsg.ErrMsg = common.ErrInvalidInput.Error()
			err = common.ErrInvalidInput
			return
		}

		resource := &agent.KnockResource{
			AuthServiceId:  aspId,
			ResourceId:     resId,
			ServerIp:       serverIp,
			ServerHostname: serverHostname,
			ServerPort:     serverPort,
		}

		peer := gAgentInstance.FindServerPeerFromResource(resource)
		if peer == nil {
			ackMsg.ErrCode = common.ErrKnockServerNotFound.ErrorCode()
			ackMsg.ErrMsg = common.ErrKnockServerNotFound.Error()
			err = common.ErrKnockServerNotFound
			return
		}

		target := &agent.KnockTarget{
			KnockResource: *resource,
			ServerPeer:    peer,
		}

		ackMsg, err = gAgentInstance.ExitKnockRequest(target)
	}()

	return err == nil
}

// cipherType: 0-curve25519; 1-sm2
// result: "privatekey"|"publickey"
// caller is responsible to free the returned char* pointer
//
//export NhpGenerateKeys
func NhpGenerateKeys(cipherType int) string {
	var e core.Ecdh
	switch core.EccTypeEnum(cipherType) {
	case core.ECC_SM2:
		e = core.NewECDH(core.ECC_SM2)
	case core.ECC_CURVE25519:
		fallthrough
	default:
		e = core.NewECDH(core.ECC_CURVE25519)
	}
	pub := e.PublicKeyBase64()
	priv := e.PrivateKeyBase64()

	res := fmt.Sprintf("%s|%s", priv, pub)

	return res
}

// cipherType: 0-curve25519; 1-sm2
// privateBase64: private key in base64 format
// result: "publickey"
// caller is responsible to free the returned char* pointer
//
//export NhpPrivkeyToPubkey
func NhpPrivkeyToPubkey(cipherType int, privateBase64 string) string {
	privKey := privateBase64
	privKeyBytes, err := base64.StdEncoding.DecodeString(privKey)
	if err != nil {
		return ""
	}

	e := core.ECDHFromKey(core.EccTypeEnum(cipherType), privKeyBytes)
	if e == nil {
		return ""
	}
	pub := e.PublicKeyBase64()

	return pub
}

2.2.2.3 编译SDK

opennhp/endpoints/agent/sdk/目录下执行编译指令(注:重新编辑的sdk源码文件放在了opennhp/endpoints/agent/sdk/下)

` gomobile bind -target ios -o nhpagent.xcframework .`

2.2.2.4 SDK适配
  • Objective-C

    • FileCopyManager.h:声明将SDK所需配置文件拷贝到沙盒方法

      //
      //  FileCopyManager.h
      //  TestXCFramework
      //
      //  Created by haochangjiu on 2025/10/30.
      //
          
      #import <Foundation/Foundation.h>
          
      NS_ASSUME_NONNULL_BEGIN
          
      @interface FileCopyManager : NSObject
      /// Copy the specified file(s) to the etc and certs directories in the application's home directory
      + (void)copyFilesToSandboxEtc;
      @end
          
      NS_ASSUME_NONNULL_END
          
      
    • FileCopyManager.m:FileCopyManager.h的实现

      //
      //  FileCopyManager.m
      //  TestXCFramework
      //
      //  Created by haochangjiu on 2025/10/30.
      //
          
      #import "FileCopyManager.h"
      #import <Foundation/Foundation.h>
          
      @implementation FileCopyManager
          
      /// Copy the specified file(s) to the etc and certs directories in the application's home directory
      + (void)copyFilesToSandboxEtc {
          // 1. Retrieve the sandboxed Documents directory
          NSArray *documentsURLs = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
          NSURL *documentsURL = [documentsURLs firstObject];
          if (!documentsURL) {
              NSLog(@"Failed to retrieve Documents directory");
              return;
          }
              
          // 2. Define paths for etc and certs directories within the sandbox
          NSURL *etcURL = [documentsURL URLByAppendingPathComponent:@"etc"];
          NSURL *certsURL = [etcURL URLByAppendingPathComponent:@"certs"];
              
          // 3. Create etc and certs directories (if they don't exist)
          [self createDirectoryIfNotExists:etcURL];
          [self createDirectoryIfNotExists:certsURL];
              
          // 4. Copy toml files to the etc directory
          NSArray *tomlFiles = @[@"server.toml", @"config.toml", @"dhp.toml", @"resource.toml"];
          for (NSString *fileName in tomlFiles) {
              [self copyFileFromBundle:fileName toDestinationURL:etcURL];
          }
              
          // 5. Copy certificate files to the etc/certs directory
          NSArray *certFiles = @[@"server.crt", @"server.key"];
          for (NSString *fileName in certFiles) {
              [self copyFileFromBundle:fileName toDestinationURL:certsURL];
          }
      }
          
      /// Create directory if it does not exist
      + (void)createDirectoryIfNotExists:(NSURL *)directoryURL {
          NSFileManager *fileManager = [NSFileManager defaultManager];
          if (![fileManager fileExistsAtPath:directoryURL.path]) {
              NSError *error;
              BOOL success = [fileManager createDirectoryAtURL:directoryURL
                                    withIntermediateDirectories:YES
                                                     attributes:nil
                                                          error:&error];
              if (success) {
                  NSLog(@"Directory created successfully: %@", directoryURL.path);
              } else {
                  NSLog(@"Failed to create directory: %@, error: %@", directoryURL.path, error.localizedDescription);
              }
          } else {
              NSLog(@"Directory already exists: %@", directoryURL.path);
          }
      }
          
      /// Copy file from Bundle to destination path
      + (void)copyFileFromBundle:(NSString *)fileName toDestinationURL:(NSURL *)destinationURL {
          // Get the file path in the Bundle
          NSURL *sourceURL = [[NSBundle mainBundle] URLForResource:[fileName stringByDeletingPathExtension]
                                                      withExtension:[fileName pathExtension]];
          if (!sourceURL) {
              NSLog(@"File not found in Bundle: %@", fileName);
              return;
          }
              
          // Destination file path (destination directory + file name)
          NSURL *destFileURL = [destinationURL URLByAppendingPathComponent:fileName];
              
          // Copy file (if it doesn't exist)
          NSFileManager *fileManager = [NSFileManager defaultManager];
          if (![fileManager fileExistsAtPath:destFileURL.path]) {
              NSError *error;
              BOOL success = [fileManager copyItemAtURL:sourceURL toURL:destFileURL error:&error];
              if (success) {
                  NSLog(@"File copied successfully: %@ -> %@", fileName, destFileURL.path);
              } else {
                  NSLog(@"File copy failed: %@, error: %@", fileName, error.localizedDescription);
              }
          } else {
              NSLog(@"File already exists: %@", destFileURL.path);
          }
      }
          
      @end
          
      
    • ViewController.m:程序主入口,进行SDK方法调用

      //
      //  ViewController.m
      //  TestXCFramework
      //
      //  Created by haochangjiu on 2025/10/30.
      //
          
      #import "ViewController.h"
      #import <Nhpagent/Nhpagent.h>
      #import "FileCopyManager.h"
          
      @interface ViewController ()
          
      @end
          
      @implementation ViewController
          
      - (void)viewDidLoad {
          [super viewDidLoad];
          // Do any additional setup after loading the view.
          // Invoke method to copy files from etc folder to sandbox etc directory
          [FileCopyManager copyFilesToSandboxEtc];
          // Retrieve the sandbox target path (Documents), which is the parent directory of the etc folder
          NSArray *documentsURLs = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
          NSURL *documentsURL = [documentsURLs firstObject];
          if (!documentsURL) {
              NSLog(@"Error: Failed to read Documents directory");
          }
          // Get the parent directory path of the etc folder
          NSString *etcPath = documentsURL.path;
          // SdkNhpAgentInit
          BOOL initFlag = SdkNhpAgentInit(etcPath, 3);
          if (!initFlag) {
              NSLog(@"NHP Agent init failed");
              return;
          }
          // knockloop_start
          long value = SdkNhpAgentKnockloopStart();
          NSLog(@"SdkNhpAgentKnockloopStart value : %ld", value);
      }
          
      @end
          
      
  • Swift

    • FileCopyManager.swift:将SDK所需配置文件拷贝到沙盒方法

      //
      //  FileCopyManager.swift
      //  TestXCFrameworkSwift
      //
      //  Created by haochangjiu on 2025/10/30.
      //
          
      import UIKit
      import Foundation
          
      class FileCopyManager {
              
          /// Copy specified files to the etc and certs directories in the sandbox
          static func copyFilesToSandboxEtc() {
              // 1. Get the Documents directory in the sandbox
              guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
                  print("Failed to get Documents directory")
                  return
              }
                  
              // 2. Define paths for etc and certs directories in the sandbox
              let etcURL = documentsURL.appendingPathComponent("etc")
              let certsURL = etcURL.appendingPathComponent("certs")
                  
              // 3. Create etc and certs directories (if they don't exist)
              createDirectoryIfNotExists(at: etcURL)
              createDirectoryIfNotExists(at: certsURL)
                  
              // 4. Copy toml files to the etc directory
              let tomlFiles = ["server.toml", "config.toml", "dhp.toml", "resource.toml"]
              tomlFiles.forEach { fileName in
                  copyFileFromBundle(fileName: fileName, to: etcURL)
              }
                  
              // 5. Copy certificate files to the etc/certs directory
              let certFiles = ["server.crt", "server.key"]
              certFiles.forEach { fileName in
                  copyFileFromBundle(fileName: fileName, to: certsURL)
              }
          }
              
          /// Create directory if it doesn't exist
          private static func createDirectoryIfNotExists(at url: URL) {
              let fileManager = FileManager.default
              guard !fileManager.fileExists(atPath: url.path) else {
                  print("Directory already exists: \(url.path)")
                  return
              }
                  
              do {
                  try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
                  print("Directory created successfully: \(url.path)")
              } catch {
                  print("Failed to create directory: \(url.path), error: \(error.localizedDescription)")
              }
          }
              
          /// Copy file from Bundle to destination path
          private static func copyFileFromBundle(fileName: String, to destinationURL: URL) {
              // Split filename and extension (handling files with extensions)
              let fileNameWithoutExt = (fileName as NSString).deletingPathExtension
              let fileExt = (fileName as NSString).pathExtension
                  
              // Get the file path in the Bundle
              guard let sourceURL = Bundle.main.url(forResource: fileNameWithoutExt, withExtension: fileExt) else {
                  print("File not found in Bundle: \(fileName)")
                  return
              }
                  
              // Destination file path (destination directory + filename)
              let destFileURL = destinationURL.appendingPathComponent(fileName)
              let fileManager = FileManager.default
                  
              // Copy file (if it doesn't exist)
              guard !fileManager.fileExists(atPath: destFileURL.path) else {
                  print("File already exists: \(destFileURL.path)")
                  return
              }
                  
              do {
                  try fileManager.copyItem(at: sourceURL, to: destFileURL)
                  print("File copied successfully: \(fileName) -> \(destFileURL.path)")
              } catch {
                  print("File copy failed: \(fileName), error: \(error.localizedDescription)")
              }
          }
      }
          
      
    • ViewController.swift:程序主入口,进行SDK方法调用

      //
      //  ViewController.swift
      //  TestXCFrameworkSwift
      //
      //  Created by haochangjiu on 2025/10/30.
      //
          
      import UIKit
      import Nhpagent
          
      class ViewController: UIViewController {
          override func viewDidLoad() {
              super.viewDidLoad()
              // Do any additional setup after loading the view.
              // Call method to copy files from etc folder to sandbox etc directory
              FileCopyManager.copyFilesToSandboxEtc()
              // Retrieve the sandbox target path (Documents), which is the parent directory of the etc folder
              guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
                  print("Error: Failed to read Documents directory")
                  return
              }
              // Get the parent directory path of the etc folder
              let etcPath: String = documentsURL.path
              // Call SdkNhpAgentInit for initialization
              let initFlag: Bool = SdkNhpAgentInit(etcPath, 3)
              if !initFlag {
                  print("NHP Agent init failed")
              }
              // Call knockloop_start
              let value = SdkNhpAgentKnockloopStart()
              print("SdkNhpAgentKnockloopStart value: %ld", value)
        }
      }