整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

神秘SDK暗刷百度广告 植入数千款APP

神秘SDK暗刷百度广告 植入数千款APP

腾讯安全了解到,腾讯安全反诈骗实验室追踪到暴风影音、天天看、塔读文学等众多应用中集成的某SDK存在下载恶意子包,通过webview配合js脚本在用户无感知的情况下刷百度广告的恶意操作。

该恶意SDK通过众多应用开发者所开发的正规应用,途经各中应用分发渠道触达千万级用户;其背后的黑产则通过恶意SDK留下的后门控制千万用户,动态下发刷量代码,大量刷广告曝光量和点击量,赚取大量广告费用,给广告主造成了巨额广告费损失。

根据安全人员详细分析,此恶意SDK主要存在以下特点:

1、该SDK被1000+千应用开发者使用,通过应用开发者的分发渠道抵达用户。主要涉及的应用包括掌通家园、暴风影音、天天看、塔读文学等,潜在可能影响上千万用户;

2、刷量子包通过多次下载并加载,并从服务器获取刷量任务,使用webview加载js脚本实现在用户无感知的情况下自动化的进行刷量任务。

此类流量黑产给传统的广告反作弊带来了极大挑战,传统通过IP、曝光频率、点击率等表象数据形成的反作弊策略难以识别这种控制大量真实设备做’肉鸡’的刷量作弊,使得大量广告费用流入黑产手中,却无法给广告主带来应有的广告效果。

SDK作恶流程和影响范围

此恶意SDK集成在应用中的那部分代码没有提供实际功能,其在被调用后会定时上报设备相关信息,获取动态子包的下载链接,下载子包并加载调用。然后由子包执行相应的恶意行为。

恶意SDK作恶流程示意图:



受恶意SDK影响的主要应用列表:



恶意SDK作恶行为详细分析

此恶意SDK被众多的中小应用开发者集成,我们以应用塔读文学为例,对其恶意行为进行详细分析。



恶意SDK代码结构



此sdk代码较少,没有什么实际的功能。其在被加载调用后,会设置定时任务,每隔3600秒(1小时)启动GatherService,上报设备相关信息,获取动态子包__gather_impl.jar的下载链接



GatherService链接服务器,获取__gather_impl.jar的下载链接



请求链接:http://gather.andr****.com:5080/gupdate/v1

请求数据:包括uid、应用包名、设备id、应用版本、手机厂商、型号、系统版本、imei、sdk版本等内容



返回内容:包括子包的版本、下载url、文件md5



动态加载下载的__gather_impl.jar



子包__gather_impl.jar代码结构,此子包的主要功能有:1、上传用户设备信息,2、下载并动态加载子包stat-impl.jar



1)、链接服务器,上传用户设备信息



服务器链接:http://userdata.andr****.com/userdata/userdata.php (此url在分析时已失效,无法链接)

上报内容:包括位置信息(经纬度),用户安装列表(软件名、包名),设备信息(厂商、型号、fingerprint,是否root),deviceid、手机号、运营商、imei、mac等。



2)、再次请求服务器,获取stat-impl.jar的下载链接



请求链接:http://iupd.andr****.com:6880/wupdate/v1

请求数据:包括uid、imei、sdk版本、手机厂商、型号、系统版本、应用包名、设备id、设备指令集等内容



返回内容:包括子包的版本、下载url、文件md5



子包下载完成后,调用native方法动态加载此子包






stat-impl.jar的代码结构:



stat-impl.jar子包被加载后,线程com.drudge.rmt.g会被启动,其作用主要是用来联网获取刷量任务,并调度任务的执行。



主要的刷量任务包括:1、刷百度搜索的关键字,2、使用js脚本实现自动点击、滑动来刷百度广告和亿量广告的点击,3、使用webview刷网页访问。

1、刷百度关键字搜索

此任务会根据获取json字符串,进行相应的操作,包括设置BAIDUID、更新配置、添加任务、设置剪切板和使用关键字刷百度搜索



设置关键字,使用webview加载对应的url



捕获到的刷百度关键字的webview加载请求:



链接服务器http://tw.andr****.com:6080/wtask/v1获取相关任务,并将任务内容存入[package]/cache/volley目录下





2、使用js脚本刷百度广告

使用webview加载http://mobads.baidu.com/ads/index.htm,并在加载完成后执行js脚本实现自动滑动、点击、保存等操作来自动刷广告



相关的js脚本

1)、js函数定义滑动、点击、保存等操作



Java层解析并实现js层传递过来的操作命令



2)、js函数判断并获取页面元素



...



3)、js函数计算页面元素相对位置,并进行滑动、点击操作



...

捕获到的刷百度广告的webview加载请求:



3、使用webview刷网页访问

此任务向服务器请求需要访问的url链接,在获取到相应的网页url后,使用webview加载进行访问。

请求需要访问的url链接



请求链接

http://us.yiqimeng.men:8080/geturls?k=beike-xinshiye&c=5

返回内容

["http://m.xinshiye.cc/cars/17/10/21/707989.html?content_id=707989u0026key=x2HAJuZaaa9YWpVa8EXTqOmHHUxhSnj75xhhAS7f6tveQsphsCm3jc9xrhV4RZbRzgm%2FQqzCVcw2dvukMqw25Q%3D%3Du0026_t=1511190410",

"http://m.xinshiye.cc/cars/17/10/11/234818.html?content_id=234818u0026key=NzLZyHQXsCdpS6bkAWab2LSzd2XApbGOJYUuN%2Bm4PFsoWk1l%2FnZSD8M1yp1cuhz%2FdL0uoNG93TVt8ai6zEU%2BQw%3D%3Du0026_t=1511190560",

"http://m.xinshiye.cc/cars/17/11/26/1769446.html?content_id=1769446u0026key=8KLxL1fm2gwNDxqT6nsSAbQ07kcEZRHBrekhzNSJcNaAg1nZmbW49pQ3EaEYJfMUeMlwSX4KzdliXJ3O37fs9g%3D%3Du0026_t=1513046929",

"http://m.xinshiye.cc/cars/17/10/31/1444661.html?content_id=1444661u0026key=mODVhDy0zyzBGH1G6sTwDYXqiy3D7pDfymsirda6s5%2BW8tarfIDPjuhT3mkqeMMDKzKr%2BFVC2Py2gzsNkMniHw%3D%3Du0026_t=1509589907",

"http://m.xinshiye.cc/cars/17/12/09/1921549.html?content_id=1921549u0026key=0XFxkCX0Bn4k%2Fw5%2FqvlSIOCREqEWoJ5jimqn%2BZAeJIwksQzydyT0AZFAVZJAritm3hpGza4TFNlONZDtoY%2BfTA%3D%3Du0026_t=1513045278"]

使用webview访问获取url



捕获到的刷求医不如健身网的webview加载请求:



相关URL整理



安全建议和防范手段

从近期Android端恶意应用的作恶手法来看,恶意开发者更多地从直接开发App应用转向开发SDK,向Android应用供应链的上游转移。通过提供恶意的SDK给应用开发者,恶意开发者可以复用这些应用的分发渠道,十分有效的扩大影响用户的范围。

而在恶意SDK的类别方面,黑产从业者主要把精力放在用户无感知的广告刷量和网站刷量等方向,通过使用代码分离和动态代码加载技术,可以完全从云端下发实际执行的代码,控制用户设备作为“肉鸡”进行广告、网站刷量等黑产行为,具有很强的隐蔽性。

这类流量型黑产逐渐增多,不仅对手机用户造成了危害,同时也给移动端广告反作弊带来了很大的挑战,传统基于IP、曝光频率、点击率等表象数据形成的反作弊策略难以识别这种控制大量真实设备做’肉鸡’的刷量作弊,难以保障应用开发者和广告主的正当权益。

针对终端用户,有如下安全建议:

1、尽可能使用正版和官方应用市场提供的APP应用;

2、移动设备即使进行安全更新;

3、安装手机管家等安全软件,实时进行防护。

apr 是一个可移植的、事件驱动的运行时,它使任何开发人员能够轻松构建出弹性的、无状态和有状态的应用程序,并可运行在云平台或边缘计算中,它同时也支持多种编程语言和开发框架。Dapr 确保开发人员专注于编写业务逻辑,不必分神解决分布式系统难题,从而显著提高了生产力。Dapr 降低了构建微服务架构类现代云原生应用的门槛。

系列

  • Dapr 与 NestJs ,实战编写一个 Pub & Sub 装饰器
  • NodeJS 基于 Dapr 构建云原生微服务应用,从 0 到 1 快速上手指南
  • 本地使用 Docker Compose 与 Nestjs 快速构建基于 Dapr 的 Redis 发布/订阅分布式应用

JavaScript

用于在 JavaScript 和 TypeScript 中构建 Dapr 应用程序的客户端库。该客户端抽象了公共 Dapr API,例如服务到服务调用、状态管理、发布/订阅、机密等,并为构建应用程序提供了一个简单、直观的 API。

安装

要开始使用 Javascript SDK,请从 NPM 安装 Dapr JavaScript SDK 包:

npm install --save @dapr/dapr

?? dapr-client 现在已弃用。 请参阅#259 了解更多信息。

https://github.com/dapr/js-sdk/issues/259

结构

Dapr Javascript SDK 包含两个主要组件:

  • DaprServer: 管理所有 Dapr sidecar 到应用程序的通信。
  • DaprClient: 管理所有应用程序到 Dapr sidecar 的通信。

上述通信可以配置为使用 gRPC 或 HTTP 协议。

Client

介绍

Dapr Client 允许您与 Dapr Sidecar 通信并访问其面向客户端的功能,例如发布事件、调用输出绑定、状态管理、Secret 管理等等。

前提条件

  • Dapr CLI 已安装https://docs.dapr.io/getting-started/install-dapr-cli/
  • 初始化 Dapr 环境https://docs.dapr.io/getting-started/install-dapr-selfhost/
  • 最新 LTS 版本的 Node 或更高版本https://nodejs.org/en/

安装和导入 Dapr 的 JS SDK

  1. 使用 npm 安装 SDK:
npm i @dapr/dapr --save
  1. 导入库:
import { DaprClient, DaprServer, HttpMethod, CommunicationProtocolEnum } from "@dapr/dapr";

const daprHost="127.0.0.1"; // Dapr Sidecar Host
const daprPort="3500"; // Dapr Sidecar Port of this Example Server
const serverHost="127.0.0.1"; // App Host of this Example Server
const serverPort="50051"; // App Port of this Example Server 

// HTTP Example
const client=new DaprClient(daprHost, daprPort);

// GRPC Example
const client=new DaprClient(daprHost, daprPort, CommunicationProtocolEnum.GRPC);

运行

要运行示例,您可以使用两种不同的协议与 Dapr sidecar 交互:HTTP(默认)或 gRPC。

使用 HTTP(默认)

import { DaprClient } from "@dapr/dapr";
const client=new DaprClient(daprHost, daprPort);
# Using dapr run
dapr run --app-id example-sdk --app-protocol http -- npm run start

# or, using npm script
npm run start:dapr-http

使用 gRPC

由于 HTTP 是默认设置,因此您必须调整通信协议以使用 gRPC。 您可以通过向客户端或服务器构造函数传递一个额外的参数来做到这一点。

import { DaprClient, CommunicationProtocol } from "@dapr/dapr";
const client=new DaprClient(daprHost, daprPort, CommunicationProtocol.GRPC);
# Using dapr run
dapr run --app-id example-sdk --app-protocol grpc -- npm run start

# or, using npm script
npm run start:dapr-grpc

代理请求

通过代理请求,我们可以利用 Dapr 通过其 sidecar 架构带来的独特功能,例如服务发现、日志记录等,使我们能够立即“升级”我们的 gRPC 服务。 gRPC 代理的这一特性在community call 41 中得到了展示。

community call 41

https://www.youtube.com/watch?v=B_vkXqptpXY&t=71s

创建代理

要执行 gRPC 代理,只需调用 client.proxy.create() 方法创建一个代理:

// As always, create a client to our dapr sidecar
// this client takes care of making sure the sidecar is started, that we can communicate, ...
const clientSidecar=new DaprClient(daprHost, daprPort, CommunicationProtocolEnum.GRPC);

// Create a Proxy that allows us to use our gRPC code
const clientProxy=await clientSidecar.proxy.create<GreeterClient>(GreeterClient);

我们现在可以调用 GreeterClient 接口中定义的方法(在本例中来自 Hello World 示例)

  • https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.proto

幕后(技术工作)

  1. gRPC 服务在 Dapr 中启动。 我们通过 --app-port 告诉 Dapr 这个 gRPC 服务器在哪个端口上运行,并通过 --app-id <APP_ID_HERE> 给它一个唯一的 Dapr 应用 ID
  2. 我们现在可以通过将连接到 Sidecar 的客户端调用 Dapr Sidecar
  3. 在调用 Dapr Sidecar 时,我们提供了一个名为 dapr-app-id 的元数据键,其中包含在 Dapr 中启动的 gRPC 服务器的值(例如,我们示例中的 server
  4. Dapr 现在会将调用转发到配置的 gRPC 服务器

构建块

JavaScript 客户端 SDK 允许您与专注于 Client to Sidecar 功能的所有 Dapr 构建块进行交互。

  • https://docs.dapr.io/developing-applications/building-blocks/

调用 API

调用一个服务

import { DaprClient, HttpMethod } from "@dapr/dapr"; 

const daprHost="127.0.0.1"; 
const daprPort="3500"; 

async function start() {
  const client=new DaprClient(daprHost, daprPort); 

  const serviceAppId="my-app-id";
  const serviceMethod="say-hello";
  
  // POST Request
  const response=await client.invoker.invoke(serviceAppId , serviceMethod , HttpMethod.POST, { hello: "world" });

  // GET Request
  const response=await client.invoker.invoke(serviceAppId , serviceMethod , HttpMethod.GET);
}

start().catch((e)=> {
  console.error(e);
  process.exit(1);
});

有关服务调用的完整指南,请访问 How-To: Invoke a service。

https://docs.dapr.io/developing-applications/building-blocks/service-invocation/howto-invoke-discover-services/

状态管理 API

保存、获取和删除应用程序状态

import { DaprClient } from "@dapr/dapr"; 

const daprHost="127.0.0.1"; 
const daprPort="3500"; 

async function start() {
  const client=new DaprClient(daprHost, daprPort); 

  const serviceStoreName="my-state-store-name";

  // Save State
  const response=await client.state.save(serviceStoreName, [
    {
      key: "first-key-name",
      value: "hello"
    },
    {
      key: "second-key-name",
      value: "world"
    }
  ]);

  // Get State
  const response=await client.state.get(serviceStoreName, "first-key-name");

  // Get Bulk State
  const response=await client.state.getBulk(serviceStoreName, ["first-key-name", "second-key-name"]);

  // State Transactions
  await client.state.transaction(serviceStoreName, [
    {
      operation: "upsert",
      request: {
        key: "first-key-name",
        value: "new-data"
      }
    },
    {
      operation: "delete",
      request: {
        key: "second-key-name"
      }
    }
  ]);

  // Delete State
  const response=await client.state.delete(serviceStoreName, "first-key-name");
}

start().catch((e)=> {
  console.error(e);
  process.exit(1);
});

有关状态操作的完整列表,请访问 How-To: Get & save state。

https://docs.dapr.io/developing-applications/building-blocks/state-management/howto-get-save-state/

查询状态 API

import { DaprClient } from "@dapr/dapr";

async function start() {
  const client=new DaprClient(daprHost, daprPort);

  const res=await client.state.query("state-mongodb", {
    filter: {
      OR: [
        {
          EQ: { "person.org": "Dev Ops" }
        },
        {
          "AND": [
            {
              "EQ": { "person.org": "Finance" }
            },
            {
              "IN": { "state": ["CA", "WA"] }
            }
          ]
        }
      ]
    },
    sort: [
      {
        key: "state",
        order: "DESC"
      }
    ],
    page: {
      limit: 10
    }
  });

  console.log(res);
}

start().catch((e)=> {
  console.error(e);
  process.exit(1);
});

发布订阅 API

发布消息

import { DaprClient } from "@dapr/dapr"; 

const daprHost="127.0.0.1"; 
const daprPort="3500"; 

async function start() {
  const client=new DaprClient(daprHost, daprPort); 

  const pubSubName="my-pubsub-name";
  const topic="topic-a";
  const message={ hello: "world" }

  // Publish Message to Topic
  const response=await client.pubsub.publish(pubSubName, topic, message);
}

start().catch((e)=> {
  console.error(e);
  process.exit(1);
});

订阅消息

import { DaprServer } from "@dapr/dapr";

const daprHost="127.0.0.1"; // Dapr Sidecar Host
const daprPort="3500"; // Dapr Sidecar Port of this Example Server
const serverHost="127.0.0.1"; // App Host of this Example Server
const serverPort="50051"; // App Port of this Example Server "

async function start() {
  const server=new DaprServer(serverHost, serverPort, daprHost, daprPort);

  const pubSubName="my-pubsub-name";
  const topic="topic-a";

  // Configure Subscriber for a Topic
  await server.pubsub.subscribe(pubSubName, topic, async (data: any)=> console.log(`Got Data: ${JSON.stringify(data)}`));

  await server.start();
}

有关状态操作的完整列表,请访问 How-To: Publish and subscribe。

https://docs.dapr.io/developing-applications/building-blocks/pubsub/howto-publish-subscribe/

绑定 API

调用输出绑定

import { DaprClient } from "@dapr/dapr"; 

const daprHost="127.0.0.1"; 
const daprPort="3500"; 

async function start() {
  const client=new DaprClient(daprHost, daprPort); 

  const bindingName="my-binding-name";
  const bindingOperation="create";
  const message={ hello: "world" };

  const response=await client.binding.send(bindingName, bindingOperation, message);
}

start().catch((e)=> {
  console.error(e);
  process.exit(1);
});

有关输出绑定的完整指南,请访问 How-To: Use bindings。

https://docs.dapr.io/developing-applications/building-blocks/bindings/howto-bindings/

Secret API

检索 secret

import { DaprClient } from "@dapr/dapr"; 

const daprHost="127.0.0.1"; 
const daprPort="3500"; 

async function start() {
  const client=new DaprClient(daprHost, daprPort); 

  const secretStoreName="my-secret-store";
  const secretKey="secret-key";

  // Retrieve a single secret from secret store
  const response=await client.secret.get(secretStoreName, secretKey);

  // Retrieve all secrets from secret store
  const response=await client.secret.getBulk(secretStoreName);
}

start().catch((e)=> {
  console.error(e);
  process.exit(1);
});

有关 secrets 的完整指南,请访问 How-To: Retrieve Secrets。

https://docs.dapr.io/developing-applications/building-blocks/secrets/howto-secrets/

配置 API

获取配置 key

import { DaprClient } from "@dapr/dapr";

const daprHost="127.0.0.1";
const daprAppId="example-config";

async function start() {

  const client=new DaprClient(daprHost, process.env.DAPR_HTTP_PORT);

  const config=await client.configuration.get('config-store', ['key1', 'key2']);
  console.log(config);
}

start().catch((e)=> {
  console.error(e);
  process.exit(1);
});

Server

介绍

Dapr Server 将允许您接收来自 Dapr Sidecar 的通信并访问其面向服务器的功能,例如:订阅事件、接收输入绑定等等。

前提条件

  • Dapr CLI 已安装https://docs.dapr.io/getting-started/install-dapr-cli/
  • 初始化 Dapr 环境https://docs.dapr.io/getting-started/install-dapr-selfhost/
  • 最新 LTS 版本的 Node 或更高版本https://nodejs.org/en/

安装和导入 Dapr 的 JS SDK

  1. 使用 npm 安装 SDK:
npm i @dapr/dapr --save
  1. 导入库:
import { DaprClient, DaprServer, HttpMethod, CommunicationProtocolEnum } from "@dapr/dapr";

const daprHost="127.0.0.1"; // Dapr Sidecar Host
const daprPort="3500"; // Dapr Sidecar Port of this Example Server
const serverHost="127.0.0.1"; // App Host of this Example Server
const serverPort="50051"; // App Port of this Example Server 

// HTTP Example
const client=new DaprClient(daprHost, daprPort);

// GRPC Example
const client=new DaprClient(daprHost, daprPort, CommunicationProtocolEnum.GRPC);

运行

要运行示例,您可以使用两种不同的协议与 Dapr sidecar 交互:HTTP(默认)或 gRPC。

使用 HTTP(默认)

import { DaprServer } from "@dapr/dapr";

const server=new DaprServer(appHost, appPort, daprHost, daprPort);
// initialize subscribtions, ... before server start
// the dapr sidecar relies on these
await server.start(); 
# Using dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol http -- npm run start

# or, using npm script
npm run start:dapr-http

?? Note:这里需要 app-port,因为这是我们的服务器需要绑定的地方。 Dapr 将在完成启动之前检查应用程序是否绑定到此端口。

使用 gRPC

由于 HTTP 是默认设置,因此您必须调整通信协议以使用 gRPC。 您可以通过向客户端或服务器构造函数传递一个额外的参数来做到这一点。

import { DaprServer, CommunicationProtocol } from "@dapr/dapr";

const server=new DaprServer(appHost, appPort, daprHost, daprPort, CommunicationProtocol.GRPC);
// initialize subscribtions, ... before server start
// the dapr sidecar relies on these
await server.start(); 
# Using dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol grpc -- npm run start

# or, using npm script
npm run start:dapr-grpc

?? Note:这里需要 app-port,因为这是我们的服务器需要绑定的地方。 Dapr 将在完成启动之前检查应用程序是否绑定到此端口。

构建块

JavaScript Server SDK 允许您与专注于 Sidecar 到 App 功能的所有 Dapr 构建块进行交互。

调用 API

监听调用

import { DaprServer } from "@dapr/dapr";

const daprHost="127.0.0.1"; // Dapr Sidecar Host
const daprPort="3500"; // Dapr Sidecar Port of this Example Server
const serverHost="127.0.0.1"; // App Host of this Example Server
const serverPort="50051"; // App Port of this Example Server "

async function start() {
  const server=new DaprServer(serverHost, serverPort, daprHost, daprPort);

  await server.invoker.listen('hello-world', mock, { method: HttpMethod.GET });

  // You can now invoke the service with your app id and method "hello-world"

  await server.start();
}

start().catch((e)=> {
  console.error(e);
  process.exit(1);
});

有关服务调用的完整指南,请访问 How-To: Invoke a service。

https://docs.dapr.io/developing-applications/building-blocks/service-invocation/howto-invoke-discover-services/

发布订阅 API

订阅消息

import { DaprServer } from "@dapr/dapr";

const daprHost="127.0.0.1"; // Dapr Sidecar Host
const daprPort="3500"; // Dapr Sidecar Port of this Example Server
const serverHost="127.0.0.1"; // App Host of this Example Server
const serverPort="50051"; // App Port of this Example Server "

async function start() {
  const server=new DaprServer(serverHost, serverPort, daprHost, daprPort);

  const pubSubName="my-pubsub-name";
  const topic="topic-a";

  // Configure Subscriber for a Topic
  await server.pubsub.subscribe(pubSubName, topic, async (data: any)=> console.log(`Got Data: ${JSON.stringify(data)}`));

  await server.start();
}

start().catch((e)=> {
  console.error(e);
  process.exit(1);
});

有关状态操作的完整列表,请访问 How-To: Publish and subscribe。

https://docs.dapr.io/developing-applications/building-blocks/pubsub/howto-publish-subscribe/

绑定 API

接收一个输入绑定

import { DaprServer } from "@dapr/dapr";

const daprHost="127.0.0.1"; 
const daprPort="3500"; 
const serverHost="127.0.0.1";
const serverPort="5051";

async function start() {
  const server=new DaprServer(serverHost, serverPort, daprHost, daprPort);

  const bindingName="my-binding-name";

  const response=await server.binding.receive(bindingName, async (data: any)=> console.log(`Got Data: ${JSON.stringify(data)}`));

  await server.start();
}

start().catch((e)=> {
  console.error(e);
  process.exit(1);
});

有关输出绑定的完整指南,请访问 How-To: Use bindings。

https://docs.dapr.io/developing-applications/building-blocks/bindings/howto-bindings/

配置 API

配置 API 目前只能通过 gRPC 使用

获取配置值

import { DaprServer } from "dapr-client";

const daprHost="127.0.0.1"; 
const daprPort="3500"; 
const serverHost="127.0.0.1";
const serverPort="5051";

async function start() {
    const client=new DaprClient(daprHost, daprPort, CommunicationProtocolEnum.GRPC);
    const config=await client.configuration.get("config-redis", ["myconfigkey1", "myconfigkey2"]);
}

start().catch((e)=> {
  console.error(e);
  process.exit(1);
});

订阅 key 更改

import { DaprServer } from "dapr-client";

const daprHost="127.0.0.1"; 
const daprPort="3500"; 
const serverHost="127.0.0.1";
const serverPort="5051";

async function start() {
    const client=new DaprClient(daprHost, daprPort, CommunicationProtocolEnum.GRPC);
    const stream=await client.configuration.subscribeWithKeys("config-redis", ["myconfigkey1", "myconfigkey2"], ()=> {
        // Received a key update
    });

    // When you are ready to stop listening, call the following
    await stream.close();
}

start().catch((e)=> {
  console.error(e);
  process.exit(1);
});

Actors

Dapr actors 包允许您从 JavaScript 应用程序与 Dapr virtual actors 进行交互。下面的示例演示了如何使用 JavaScript SDK 与 virtual actors 进行交互。

如需更深入地了解 Dapr actors,请访问 actors 概览页面。

前提条件

  • Dapr CLI 已安装https://docs.dapr.io/getting-started/install-dapr-cli/
  • 初始化 Dapr 环境https://docs.dapr.io/getting-started/install-dapr-selfhost/
  • 最新 LTS 版本的 Node 或更高版本https://nodejs.org/en/
  • 已安装 JavaScript NPM 包https://www.npmjs.com/package/@dapr/dapr

场景

下面的代码示例粗略地描述了停车场现场监控系统的场景,可以在 Mark Russinovich 的这段视频中看到。

  • https://www.youtube.com/watch?v=eJCu6a-x9uo&t=3785

一个停车场由数百个停车位组成,每个停车位都包含一个传感器,可为中央监控系统提供更新。 停车位传感器(我们的 actors)检测停车位是否被占用或可用。

要自己跳入并运行此示例,请克隆源代码,该源代码可在 JavaScript SDK 示例目录中找到。

  • https://github.com/dapr/js-sdk/tree/master/examples/http/actor-parking-sensor

Actor 接口

Actor 接口定义了在 Actor 实现和调用 Actor 的客户端之间共享的合约。 在下面的示例中,我们为停车场传感器创建了一个接口。 每个传感器有 2 个方法:carEntercarLeave,它们定义了停车位的状态:

export default interface ParkingSensorInterface {
  carEnter(): Promise<void>;
  carLeave(): Promise<void>;
}

Actor 实现

Actor 实现通过扩展基本类型 AbstractActor 并实现 Actor 接口(在本例中为 ParkingSensorInterface)来定义一个类。

下面的代码描述了一个 actor 实现以及一些辅助方法。

import { AbstractActor } from "@dapr/dapr";
import ParkingSensorInterface from "./ParkingSensorInterface";

export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface {
  async carEnter(): Promise<void> {
    // Implementation that updates state that this parking spaces is occupied.
  }

  async carLeave(): Promise<void> {
    // Implementation that updates state that this parking spaces is available.
  }

  private async getInfo(): Promise<object> {
    // Implementation of requesting an update from the parking space sensor.
  }

  /**
   * @override
   */
  async onActivate(): Promise<void> {
    // Initialization logic called by AbstractActor.
  }
}

注册 Actor

使用 DaprServer 包初始化和注册你的 actors:

import { DaprServer } from "@dapr/dapr";
import ParkingSensorImpl from "./ParkingSensorImpl";

const daprHost="127.0.0.1";
const daprPort="50000";
const serverHost="127.0.0.1";
const serverPort="50001";

const server=new DaprServer(serverHost, serverPort, daprHost, daprPort);

await server.actor.init(); // Let the server know we need actors
server.actor.registerActor(ParkingSensorImpl); // Register the actor
await server.start(); // Start the server

// To get the registered actors, you can invoke `getRegisteredActors`:
const resRegisteredActors=await server.actor.getRegisteredActors();
console.log(`Registered Actors: ${JSON.stringify(resRegisteredActors)}`);

调用 Actor 方法

注册 Actor 后,使用 ActorProxyBuilder 创建一个实现 ParkingSensorInterface 的代理对象。 您可以通过直接调用 Proxy 对象上的方法来调用 actor 方法。在内部,它转换为对 Actor API 进行网络调用并取回结果。

import { DaprClient, ActorId } from "@dapr/dapr";
import ParkingSensorImpl from "./ParkingSensorImpl";
import ParkingSensorInterface from "./ParkingSensorInterface";

const daprHost="127.0.0.1";
const daprPort="50000";

const client=new DaprClient(daprHost, daprPort);

// Create a new actor builder. It can be used to create multiple actors of a type.
const builder=new ActorProxyBuilder<ParkingSensorInterface>(ParkingSensorImpl, client);

// Create a new actor instance.
const actor=builder.build(new ActorId("my-actor"));
// Or alternatively, use a random ID
// const actor=builder.build(ActorId.createRandomId());

// Invoke the method.
await actor.carEnter();

将状态与 Actor 一起使用

// ...

const PARKING_SENSOR_PARKED_STATE_NAME="parking-sensor-parked"

const actor=builder.build(new ActorId("my-actor")) 

// SET state
await actor.getStateManager().setState(PARKING_SENSOR_PARKED_STATE_NAME, true);

// GET state
const value=await actor.getStateManager().getState(PARKING_SENSOR_PARKED_STATE_NAME);
if (!value) {
  console.log(`Received: ${value}!`);
}

// DELETE state
await actor.removeState(PARKING_SENSOR_PARKED_STATE_NAME);
...

Actor 计时器和提醒器

JS SDK 支持 actors 可以通过注册 timers 或 reminders 来为自己安排定期工作。timers 和 reminders 之间的主要区别在于,Dapr actor runtime 在停用后不保留有关 timers 的任何信息,而是使用 Dapr actor state provider 保留 reminders 信息。

这种区别允许用户在轻量级但无状态的 timers 和更需要资源但有状态的 reminders 之间进行权衡。

Timers 和 reminders 的调度界面是相同的。要更深入地了解调度配置,请参阅 actors timers 和 reminders 文档。

  • https://docs.dapr.io/developing-applications/building-blocks/actors/howto-actors/#actor-timers-and-reminders

Actor Timers

// ...

const actor=builder.build(new ActorId("my-actor"));

// Register a timer
await actor.registerActorTimer(
  "timer-id", // Unique name of the timer.
  "cb-method", // Callback method to execute when timer is fired.
  Temporal.Duration.from({ seconds: 2 }), // DueTime
  Temporal.Duration.from({ seconds: 1 }), // Period
  Temporal.Duration.from({ seconds: 1 }), // TTL
  50 // State to be sent to timer callback.
);

// Delete the timer
await actor.unregisterActorTimer("timer-id");

Actor Reminders

// ...

const actor=builder.build(new ActorId("my-actor"));

// Register a reminder, it has a default callback: `receiveReminder`
await actor.registerActorReminder(
  "reminder-id", // Unique name of the reminder.
  Temporal.Duration.from({ seconds: 2 }), // DueTime
  Temporal.Duration.from({ seconds: 1 }), // Period
  Temporal.Duration.from({ seconds: 1 }), // TTL
  100 // State to be sent to reminder callback.
);

// Delete the reminder
await actor.unregisterActorReminder("reminder-id");

要处理回调,您需要覆盖 actor 中的默认 receiveReminder 实现。 例如,从我们最初的 actor 实现中:

export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface {
  // ...

  /**
   * @override
   */
  async receiveReminder(state: any): Promise<void> {
    // handle stuff here
  }

  // ...
}

有关 actors 的完整指南,请访问 How-To: Use virtual actors in Dapr。

  • https://docs.dapr.io/developing-applications/building-blocks/actors/howto-actors/

Logging

介绍

JavaScript SDK 带有一个开箱即用的基于 Console 的 logger。SDK 发出各种内部日志,以帮助用户了解事件链并解决问题。此 SDK 的使用者可以自定义日志的详细程度,并为 logger 提供自己的实现。

配置日志级别

有五个级别的日志记录,按重要性降序排列 - errorwarninfoverbosedebug。 将日志设置为一个级别意味着 logger 将发出至少与上述级别一样重要的所有日志。 例如,设置为 verbose 日志意味着 SDK 不会发出 debug 级别的日志。默认日志级别是 info

Dapr Client

import { CommunicationProtocolEnum, DaprClient, LogLevel } from "@dapr/dapr";

// create a client instance with log level set to verbose.
const client=new DaprClient(
    daprHost, 
    daprPort, 
    CommunicationProtocolEnum.HTTP, 
    { logger: { level: LogLevel.Verbose } });

有关如何使用 Client 的更多详细信息,请参阅 JavaScript Client。

https://docs.dapr.io/developing-applications/sdks/js/js-client/

DaprServer

import { CommunicationProtocolEnum, DaprServer, LogLevel } from "@dapr/dapr";

// create a server instance with log level set to error.
const server=new DaprServer(
    serverHost,
    serverPort, 
    daprHost,
    daprPort,
    CommunicationProtocolEnum.HTTP,
    { logger: { level: LogLevel.Error } });

有关如何使用 Server 的更多详细信息,请参阅 JavaScript Server。

https://docs.dapr.io/developing-applications/sdks/js/js-server/

自定义 LoggerService

JavaScript SDK 使用内置 Console 进行日志记录。要使用 Winston 或 Pino 等自定义 logger,您可以实现 LoggerService 接口。

基于 Winston 的日志记录:

创建 LoggerService 的新实现。

import { LoggerService } from "@dapr/dapr";
import * as winston from 'winston';

export class WinstonLoggerService implements LoggerService {
    private logger;

    constructor() {
        this.logger=winston.createLogger({
            transports: [
                new winston.transports.Console(),
                new winston.transports.File({ filename: 'combined.log' })
            ]
        });
    }

    error(message: any, ...optionalParams: any[]): void {
        this.logger.error(message, ...optionalParams)
    }
    warn(message: any, ...optionalParams: any[]): void {
        this.logger.warn(message, ...optionalParams)
    }
    info(message: any, ...optionalParams: any[]): void {
        this.logger.info(message, ...optionalParams)
    }
    verbose(message: any, ...optionalParams: any[]): void {
        this.logger.verbose(message, ...optionalParams)
    }
    debug(message: any, ...optionalParams: any[]): void {
        this.logger.debug(message, ...optionalParams)
    }
}

将新实现传递给 SDK。

import { CommunicationProtocolEnum, DaprClient, LogLevel } from "@dapr/dapr";
import { WinstonLoggerService } from "./WinstonLoggerService";

const winstonLoggerService=new WinstonLoggerService();

// create a client instance with log level set to verbose and logger service as winston.
const client=new DaprClient(
    daprHost,
    daprPort,
    CommunicationProtocolEnum.HTTP,
    { logger: { level: LogLevel.Verbose, service: winstonLoggerService } });

官方示例代码库

  • https://github.com/dapr/js-sdk/tree/main/examples

者: 为少 来源: 黑客下午茶

源码仓库地址

https://github.com/getsentry/sentry-javascript

支持的平台

对于每个主要的 JavaScript 平台,都有一个特定的高阶 SDK,可以在单个包中提供您需要的所有工具。有关更多详细信息,请参阅这些 SDK 的 README 和说明:

  • @sentry/browser: 浏览器的 SDK,包括对基础主干(GlobalHandlers, TryCatch, Breadcrumbs, LinkedErrors, UserAgent, Dedupe)的集成。
  • @sentry/node: 适用于 Node 的 SDK,包括 Express、Koa、Loopback、Sails 和 Connect 的集成。
  • @sentry/angular: 启用 Angular 集成的浏览器 SDK。
  • @sentry/react: 启用 React 集成的浏览器 SDK。
  • @sentry/ember: 启用 Ember 集成的浏览器 SDK。
  • @sentry/vue: 启用 Vue 集成的浏览器 SDK。
  • @sentry/gatsby: Gatsby 的 SDK。
  • @sentry/nextjs: Next.js 的 SDK。
  • @sentry/integrations: 可用于增强 JS SDK 的可插拔集成。
  • @sentry/electron: 支持原生崩溃的 Electron SDK。
  • @sentry/react-native: 支持原生崩溃的 React Native SDK。
  • @sentry/capacitor:支持原生崩溃的 Capacitor App 和 Ionic 的 SDK。
  • sentry-cordova:支持原生崩溃的 Cordova App 的 SDK。
  • raven-js:旧的稳定 JavaScript SDK,我们仍然支持并发布 SDK 的错误修复,但所有新功能都将在 @sentry/browser 中实现,它是继任者。
  • raven:旧的稳定 Node SDK,与 raven-js 一样,我们仍然支持并发布 SDK 的错误修复,但所有新功能都将在 @sentry/node 中实现,它是继任者。

用于平台 SDK 开发的共享软件包

  • @sentry/tracing: 为性能监控/跟踪提供集成和扩展。
  • @sentry/hub: SDK 的全局状态管理。
  • @sentry/minimal: Sentry 支持的最小 SDK
  • @sentry/core: 具有接口、类型定义和基类的所有 JavaScript SDK 的基础。
  • @sentry/utils: 一组对各种 SDK 有用的辅助程序和实用函数。
  • @sentry/types: 所有软件包中使用的类型定义。

开发调试

设置环境

要运行 test suite 和 code linter,需要 node.js 和 yarn。

  • https://nodejs.org/download
  • https://yarnpkg.com/en/docs/install

sentry-javascript 是一个包含多个软件包的 monorepo,使用 lerna 管理它们。首先,安装所有依赖项,使用 lerna 引导工作区,然后执行初始构建,以便 TypeScript 可以读取所有链接的类型定义。

yarn 
 
yarn lerna bootstrap 
 
yarn build 

这样,repo 就完全设置好了,您可以运行所有命令了。

构建软件包

由于我们使用的是 TypeScript,因此您需要将代码转换为 JavaScript 才能使用它。来自 repo 的顶层,有三个可用命令:

  • yarn build:dev,它运行每个包的 ES5 和 ES6 版本的一次性构建。
  • yarn build:dev:filter ,它只在与给定包相关的项目中运行 yarn build:dev(例如,运行 yarn build:dev:filter @sentry/react 将构建 react 包、它的所有依赖项(utils、core、browser 等),以及所有依赖它的包(目前是 gatsby 和 nextjs))。
  • yarn build:dev:watch,在 watch 模式下运行 yarn build:dev(推荐)

添加测试

任何重要的修复/功能都应该包括测试。您会在每个软件包中找到一个 test 文件夹。

请注意,仅对于 browser 包,如果您将新文件添加到集成测试套件中,您还需要将其添加到shell.js 中的列表中。在所有包中,向现有文件添加测试都可以开箱即用。

运行测试

运行测试与构建的工作方式相同 - 在项目根目录运行 yarn test 将对所有包运行测试,在特定包中运行 yarn test 将为该包运行测试。还有一些命令可以在每个位置运行测试的子集。查看相应 package.json 的 scripts 条目以了解详细信息。

注意:你必须在 yarn test 工作之前运行 yarn build。

调试测试

如果您在编写测试时遇到麻烦并需要调试其中之一,您可以使用 VSCode 的 debugger 来完成。

如果您尚未安装它,请安装 Tasks Shell Input 扩展,您可以在侧边栏的“扩展”选项卡中找到它作为推荐的工作区扩展之一。

  • 将断点或 debugger 语句放置在测试或底层代码中您希望 jest 暂停的任何位置。
  • 打开包含相关测试的文件,并确保其选项卡处于活动状态(以便您可以看到文件的内容)。
  • 切换到侧边栏中的 debugger,然后从下拉列表中选择 Debug unit tests - just open file。
  • 单击绿色的 “play” 按钮以 watch 模式在打开的文件中运行测试。

实战

测试代码:

https://github.com/getsentry/sentry-javascript/blob/master/packages/minimal/test/lib/minimal.test.ts

专业提示:如果您的任何断点在由多个测试运行的代码中,并且您运行整个测试文件,您将在不关心的测试中间一遍又一遍地停留在这些断点上。为避免这种情况,请将测试的初始 it 或 test 替换为 it.only 或 test.only。这样,当您遇到断点时,您就会知道您到达了有问题的测试的一部分。

Linting

与构建和测试类似,linting 可以通过调用 yarn lint 在项目根目录或单个包中完成。

注意:你必须在 yarn lint 工作之前运行 yarn build。