From 5fd1967074051a404111ae2c185b741314886d14 Mon Sep 17 00:00:00 2001 From: "dong.yan" Date: Tue, 30 Jun 2026 12:06:23 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20add=20360=20EPP=20(360=E7=BB=88?= =?UTF-8?q?=E7=AB=AF=E5=AE=89=E5=85=A8=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?)=20v10.0.0.08331=20service=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add OctoBus adapter for 360 Enterprise Endpoint Protection Platform. Supports dashboard overview, terminal management, alarm query, virus scan stats, and vulnerability fix stats via the 360 EPP Web admin API with RSA-encrypted password authentication. Closes #241 --- services/360__360-epp_v10.0.0.08331/README.md | 108 ++++ .../360__360-epp_v10.0.0.08331/bin/360-epp.js | 7 + .../config.schema.json | 27 + .../360__360-epp_v10.0.0.08331/package.json | 12 + .../proto/360_epp.proto | 151 +++++ .../secret.schema.json | 16 + .../360__360-epp_v10.0.0.08331/service.json | 53 ++ .../360__360-epp_v10.0.0.08331/src/360-epp.js | 524 ++++++++++++++++++ .../360__360-epp_v10.0.0.08331/src/service.js | 7 + .../test/360-epp.test.js | 145 +++++ .../test/mock_upstream.js | 153 +++++ 11 files changed, 1203 insertions(+) create mode 100644 services/360__360-epp_v10.0.0.08331/README.md create mode 100644 services/360__360-epp_v10.0.0.08331/bin/360-epp.js create mode 100644 services/360__360-epp_v10.0.0.08331/config.schema.json create mode 100644 services/360__360-epp_v10.0.0.08331/package.json create mode 100644 services/360__360-epp_v10.0.0.08331/proto/360_epp.proto create mode 100644 services/360__360-epp_v10.0.0.08331/secret.schema.json create mode 100644 services/360__360-epp_v10.0.0.08331/service.json create mode 100644 services/360__360-epp_v10.0.0.08331/src/360-epp.js create mode 100644 services/360__360-epp_v10.0.0.08331/src/service.js create mode 100644 services/360__360-epp_v10.0.0.08331/test/360-epp.test.js create mode 100644 services/360__360-epp_v10.0.0.08331/test/mock_upstream.js diff --git a/services/360__360-epp_v10.0.0.08331/README.md b/services/360__360-epp_v10.0.0.08331/README.md new file mode 100644 index 00000000..fe0aca12 --- /dev/null +++ b/services/360__360-epp_v10.0.0.08331/README.md @@ -0,0 +1,108 @@ +# 360终端安全管理系统 (EPP) OctoBus Service + +360 Enterprise Endpoint Protection Platform (EPP) v10.0.0.08331 的 OctoBus 适配服务,提供终端管理、告警查询、病毒扫描统计、漏洞修复统计等 API。 + +## 导入 OctoBus + +```bash +octobus service import --id 360-epp ./services/360__360-epp_v10.0.0.08331 +``` + +## 包文件说明 + +- `service.json`: OctoBus 服务描述文件 +- `proto/360_epp.proto`: gRPC API 定义 +- `config.schema.json`: 非敏感配置(endpoint、超时、TLS) +- `secret.schema.json`: 敏感配置(管理员账号密码) +- `src/360-epp.js`: 360 EPP REST API 代理实现 +- `src/service.js`: OctoBus SDK `defineService` wrapper +- `bin/360-epp.js`: 服务入口 +- `test/360-epp.test.js`: 单元测试(参数校验、API 映射、错误映射) +- `test/mock_upstream.js`: 本地 mock 服务器 + +## 配置 + +### Config(非敏感配置) + +```json +{ + "endpoint": "https://192.168.0.109", + "timeoutMs": 30000, + "skipTlsVerify": true +} +``` + +| 字段 | 说明 | 默认值 | +|------|------|--------| +| `endpoint` | 360 EPP 管理控制台地址 | 必填 | +| `timeoutMs` | HTTP 请求超时(毫秒) | 30000 | +| `skipTlsVerify` | 跳过 TLS 证书验证 | true | + +### Secret(敏感配置) + +```json +{ + "username": "eppadmin", + "password": "your-password" +} +``` + +| 字段 | 说明 | +|------|------| +| `username` | 360 EPP 管理员账号 | +| `password` | 360 EPP 管理员密码(明文) | + +## RPC 方法 + +| 方法 | 说明 | +|------|------| +| `Qihoo360_EPP.Qihoo360_EPP/GetDashboardInfo` | 获取仪表盘概览信息 | +| `Qihoo360_EPP.Qihoo360_EPP/ListTerminals` | 查询终端列表(分页、关键词搜索) | +| `Qihoo360_EPP.Qihoo360_EPP/GetTerminalDetail` | 获取终端详细信息 | +| `Qihoo360_EPP.Qihoo360_EPP/ListAlarms` | 查询告警日志列表 | +| `Qihoo360_EPP.Qihoo360_EPP/GetVirusStats` | 获取病毒扫描统计数据 | +| `Qihoo360_EPP.Qihoo360_EPP/GetLeakFixStats` | 获取漏洞修复统计数据 | +| `Qihoo360_EPP.Qihoo360_EPP/GetTerminalHardware` | 获取终端硬件配置信息 | + +## 认证流程 + +本服务使用 360 EPP 的 Web 管理后台认证流程: + +1. 调用 `GET /user/getPubKey?username=xxx` 获取 RSA 公钥 +2. 使用 RSA 公钥加密密码,同时对密码做 MD5 哈希 +3. 调用 `POST /user/login` 提交 `username` / `password`(MD5) / `rPassword`(RSA加密) +4. 获取会话 Cookie (`PN`) 用于后续 API 调用 + +会话 Cookie 在服务运行期间缓存复用。若会话过期(errno=10401),会自动重新登录。 + +## 错误映射 + +| 场景 | gRPC Status | +|------|-------------| +| 认证失败 | `UNAUTHENTICATED` | +| 参数无效 | `INVALID_ARGUMENT` | +| 会话过期 | `UNAUTHENTICATED` | +| 上游 API 错误 | `FAILED_PRECONDITION` | +| 网络/超时 | `UNAVAILABLE` / `DEADLINE_EXCEEDED` | + +## 支持版本 + +- 360终端安全管理系统 v10.0.0.08331 +- 认证方式:Web 管理后台 RSA 加密登录 + +## 注意事项 + +- 本服务使用 Web 管理后台的认证接口,需要具有管理权限的账号 +- 密码通过 RSA 非对称加密传输,不在网络中明文传递 +- 会话 Cookie 在内存中缓存,实例重启后需要重新登录 +- 建议为 OctoBus 创建专用的管理员账号 +- 写操作(如终端隔离、策略下发)暂未实现,仅提供查询能力 + +## 本地验证 + +```bash +cd services +npm run validate -- --service-dir 360__360-epp_v10.0.0.08331 +npm test -- --service-dir 360__360-epp_v10.0.0.08331 +npm run pack:check +``` diff --git a/services/360__360-epp_v10.0.0.08331/bin/360-epp.js b/services/360__360-epp_v10.0.0.08331/bin/360-epp.js new file mode 100644 index 00000000..508272f0 --- /dev/null +++ b/services/360__360-epp_v10.0.0.08331/bin/360-epp.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../src/service.js"; + +runServiceMain(service); diff --git a/services/360__360-epp_v10.0.0.08331/config.schema.json b/services/360__360-epp_v10.0.0.08331/config.schema.json new file mode 100644 index 00000000..230b3659 --- /dev/null +++ b/services/360__360-epp_v10.0.0.08331/config.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "endpoint": { + "type": "string", + "description": "360 EPP 管理系统 REST API 基地址,例如 https://192.168.0.109" + }, + "baseUrl": { + "type": "string", + "description": "Legacy alias for endpoint." + }, + "timeoutMs": { + "type": "integer", + "minimum": 1, + "default": 30000, + "description": "HTTP 请求超时时间(毫秒)" + }, + "skipTlsVerify": { + "type": "boolean", + "default": true, + "description": "跳过 TLS 证书验证(适用于自签名证书部署)" + } + }, + "required": ["endpoint"] +} diff --git a/services/360__360-epp_v10.0.0.08331/package.json b/services/360__360-epp_v10.0.0.08331/package.json new file mode 100644 index 00000000..043f7c79 --- /dev/null +++ b/services/360__360-epp_v10.0.0.08331/package.json @@ -0,0 +1,12 @@ +{ + "name": "360-epp", + "version": "0.0.0", + "private": true, + "type": "module", + "bin": { + "360-epp": "bin/360-epp.js" + }, + "dependencies": { + "@chaitin-ai/octobus-sdk": "^0.5.0" + } +} diff --git a/services/360__360-epp_v10.0.0.08331/proto/360_epp.proto b/services/360__360-epp_v10.0.0.08331/proto/360_epp.proto new file mode 100644 index 00000000..31eb3a79 --- /dev/null +++ b/services/360__360-epp_v10.0.0.08331/proto/360_epp.proto @@ -0,0 +1,151 @@ +syntax = "proto3"; + +package Qihoo360_EPP; + +import "google/protobuf/wrappers.proto"; +import "google/protobuf/struct.proto"; + +option go_package = "miner/grpc-service/Qihoo360_EPP"; + +service Qihoo360_EPP { + // 获取仪表盘概览信息(终端数量、病毒数量、漏洞数量等) + rpc GetDashboardInfo(GetDashboardInfoRequest) returns (GetDashboardInfoResponse) {} + + // 查询终端列表 + rpc ListTerminals(ListTerminalsRequest) returns (ListTerminalsResponse) {} + + // 获取终端详情 + rpc GetTerminalDetail(GetTerminalDetailRequest) returns (GetTerminalDetailResponse) {} + + // 查询告警日志列表 + rpc ListAlarms(ListAlarmsRequest) returns (ListAlarmsResponse) {} + + // 查询病毒扫描统计 + rpc GetVirusStats(GetVirusStatsRequest) returns (GetVirusStatsResponse) {} + + // 查询漏洞修复统计 + rpc GetLeakFixStats(GetLeakFixStatsRequest) returns (GetLeakFixStatsResponse) {} + + // 查询终端硬件信息 + rpc GetTerminalHardware(GetTerminalHardwareRequest) returns (GetTerminalHardwareResponse) {} +} + +// ========== Dashboard ========== + +message GetDashboardInfoRequest {} + +message GetDashboardInfoResponse { + google.protobuf.Struct data = 1; // 仪表盘原始数据,透传 +} + +// ========== Terminal Management ========== + +message ListTerminalsRequest { + google.protobuf.Int64Value page = 1; // 页码,默认 1 + google.protobuf.Int64Value page_size = 2; // 每页条数,默认 20 + string keyword = 3; // 搜索关键词(IP/主机名/MAC) + string group_id = 4; // 终端分组 ID + string status = 5; // 在线状态:online/offline +} + +message TerminalInfo { + string id = 1; + string name = 2; + string ip = 3; + string mac = 4; + string os = 5; + string status = 6; // 在线状态 + string group_name = 7; // 所属分组 + string last_online_time = 8; + string antivirus_status = 9; // 杀毒软件状态 +} + +message ListTerminalsResponse { + repeated TerminalInfo terminals = 1; + int64 total = 2; +} + +message GetTerminalDetailRequest { + string terminal_id = 1; // 终端 ID +} + +message GetTerminalDetailResponse { + string id = 1; + string name = 2; + string ip = 3; + string mac = 4; + string os = 5; + string os_version = 6; + string status = 7; + string group_name = 8; + string last_online_time = 9; + string antivirus_version = 10; + string antivirus_status = 11; + string cpu = 12; + string memory = 13; + string disk = 14; + google.protobuf.Struct raw = 15; // 原始数据透传 +} + +// ========== Alarm Management ========== + +message ListAlarmsRequest { + google.protobuf.Int64Value page = 1; + google.protobuf.Int64Value page_size = 2; + string alarm_type = 3; // 告警类型:virus/attack/vulnerability/abnormal_login + string start_time = 4; // 开始时间 + string end_time = 5; // 结束时间 + string severity = 6; // 严重程度:high/medium/low +} + +message AlarmInfo { + string id = 1; + string type = 2; + string severity = 3; + string title = 4; + string description = 5; + string terminal_name = 6; + string terminal_ip = 7; + string created_time = 8; + string status = 9; // 处理状态 +} + +message ListAlarmsResponse { + repeated AlarmInfo alarms = 1; + int64 total = 2; + google.protobuf.Struct statistics = 3; // 各状态统计 +} + +// ========== Virus Management ========== + +message GetVirusStatsRequest {} + +message GetVirusStatsResponse { + google.protobuf.Struct data = 1; // 病毒统计数据,透传 +} + +// ========== Leak Fix Management ========== + +message GetLeakFixStatsRequest {} + +message GetLeakFixStatsResponse { + google.protobuf.Struct data = 1; // 漏洞统计数据,透传 +} + +// ========== Terminal Hardware ========== + +message GetTerminalHardwareRequest { + string terminal_id = 1; +} + +message GetTerminalHardwareResponse { + string terminal_id = 1; + string cpu_model = 2; + string cpu_cores = 3; + string memory_size = 4; + string disk_size = 5; + string gpu_model = 6; + string motherboard = 7; + repeated string network_adapters = 8; + google.protobuf.Struct raw = 9; +} diff --git a/services/360__360-epp_v10.0.0.08331/secret.schema.json b/services/360__360-epp_v10.0.0.08331/secret.schema.json new file mode 100644 index 00000000..1bc9a0ba --- /dev/null +++ b/services/360__360-epp_v10.0.0.08331/secret.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "username": { + "type": "string", + "description": "360 EPP 管理员账号" + }, + "password": { + "type": "string", + "description": "360 EPP 管理员密码" + } + }, + "required": ["username", "password"] +} diff --git a/services/360__360-epp_v10.0.0.08331/service.json b/services/360__360-epp_v10.0.0.08331/service.json new file mode 100644 index 00000000..6f5a4fff --- /dev/null +++ b/services/360__360-epp_v10.0.0.08331/service.json @@ -0,0 +1,53 @@ +{ + "schema": "chaitin.octobus.service.v1", + "name": "360-epp", + "displayName": "360终端安全管理系统(EPP)", + "description": "OctoBus package for 360 Enterprise Endpoint Protection Platform (360 EPP v10.0.0.08331). Provides terminal management, alarm query, virus scan stats, and vulnerability fix stats APIs.", + "runtime": { + "mode": "long-running" + }, + "proto": { + "roots": [ + "proto" + ], + "files": [ + "proto/360_epp.proto" + ] + }, + "configSchema": "config.schema.json", + "secretSchema": "secret.schema.json", + "sdk": { + "cli": { + "commands": { + "Qihoo360_EPP.Qihoo360_EPP/GetDashboardInfo": { + "name": "get-dashboard-info", + "description": "获取 360 EPP 仪表盘概览信息(终端数量、病毒数量、漏洞数量等)" + }, + "Qihoo360_EPP.Qihoo360_EPP/ListTerminals": { + "name": "list-terminals", + "description": "查询 360 EPP 终端列表,支持分页和关键词搜索" + }, + "Qihoo360_EPP.Qihoo360_EPP/GetTerminalDetail": { + "name": "get-terminal-detail", + "description": "获取终端详细信息" + }, + "Qihoo360_EPP.Qihoo360_EPP/ListAlarms": { + "name": "list-alarms", + "description": "查询 360 EPP 告警日志列表" + }, + "Qihoo360_EPP.Qihoo360_EPP/GetVirusStats": { + "name": "get-virus-stats", + "description": "获取病毒扫描统计数据" + }, + "Qihoo360_EPP.Qihoo360_EPP/GetLeakFixStats": { + "name": "get-leakfix-stats", + "description": "获取漏洞修复统计数据" + }, + "Qihoo360_EPP.Qihoo360_EPP/GetTerminalHardware": { + "name": "get-terminal-hardware", + "description": "获取终端硬件配置信息" + } + } + } + } +} diff --git a/services/360__360-epp_v10.0.0.08331/src/360-epp.js b/services/360__360-epp_v10.0.0.08331/src/360-epp.js new file mode 100644 index 00000000..525b5815 --- /dev/null +++ b/services/360__360-epp_v10.0.0.08331/src/360-epp.js @@ -0,0 +1,524 @@ +// Qihoo360_EPP 360 EPP Terminal Security Management System proxy +// Authenticates via RSA-encrypted password login, maintains PN session cookie +// API endpoints follow CodeIgniter convention: /controller/method + +import { GrpcError, grpcStatus } from '@chaitin-ai/octobus-sdk'; +import crypto from 'crypto'; + +const DEFAULT_TIMEOUT_MS = 30000; + +const METHOD_PREFIX = 'Qihoo360_EPP.Qihoo360_EPP'; + +// ========== Helpers ========== + +const grpcCodeFor = (code) => ({ + INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, + FAILED_PRECONDITION: grpcStatus.FAILED_PRECONDITION, + PERMISSION_DENIED: grpcStatus.PERMISSION_DENIED, + UNAVAILABLE: grpcStatus.UNAVAILABLE, + DEADLINE_EXCEEDED: grpcStatus.DEADLINE_EXCEEDED, + UNAUTHENTICATED: grpcStatus.UNAUTHENTICATED, + NOT_FOUND: grpcStatus.NOT_FOUND, +})[code] ?? grpcStatus.UNKNOWN; + +const errorWithCode = (code, message) => { + const err = new GrpcError(grpcCodeFor(code), `${code}: ${message}`); + err.legacyCode = code; + return err; +}; + +const firstDefined = (...vals) => vals.find((v) => v !== undefined && v !== null); + +const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj ?? {}, key); + +const normalizeBaseUrl = (url) => { + const base = String(url || '').trim(); + if (!/^https?:\/\//i.test(base)) return null; + return base.replace(/\/$/, ''); +}; + +const toPositiveInt = (val) => { + if (val === undefined || val === null) return null; + if (typeof val === 'object') { + if ('value' in val) return toPositiveInt(val.value); + return null; + } + const n = Number(val); + if (!Number.isInteger(n) || Number.isNaN(n)) return null; + return n; +}; + +const mergedBindings = (ctx = {}) => ({ + ...(ctx?.config ?? {}), + ...(ctx?.secret ?? {}), + ...(ctx?.bindings ?? {}), +}); + +// ========== RSA Encryption for Login ========== + +function rsaEncrypt(plaintext, pubkeyPem) { + // PKCS#1 v1.5 encryption using the public key + const encrypted = crypto.publicEncrypt( + { + key: pubkeyPem, + padding: crypto.constants.RSA_PKCS1_PADDING, + }, + Buffer.from(plaintext, 'utf-8') + ); + return encrypted.toString('base64'); +} + +function md5Hash(str) { + return crypto.createHash('md5').update(str).digest('hex'); +} + +// ========== Session Management ========== + +class EppSession { + constructor() { + this.cookie = null; + this.baseUrl = null; + this.timeoutMs = DEFAULT_TIMEOUT_MS; + this.skipTlsVerify = true; + } + + configure(ctx) { + const bindings = mergedBindings(ctx); + this.baseUrl = normalizeBaseUrl( + bindings.endpoint || bindings.baseUrl || bindings.restBaseUrl || '' + ); + this.timeoutMs = ctx.limits?.timeoutMs || bindings.timeoutMs || DEFAULT_TIMEOUT_MS; + this.skipTlsVerify = Boolean( + bindings.skipTlsVerify || bindings.tlsInsecureSkipVerify || bindings.skip_tls_verify + ); + } + + async login(username, password) { + if (!this.baseUrl) { + throw errorWithCode('INVALID_ARGUMENT', 'endpoint/baseUrl is required (http/https)'); + } + + try { + // Step 1: Get RSA public key + const pubKeyUrl = `${this.baseUrl}/user/getPubKey?username=${encodeURIComponent(username)}`; + const pubKeyRes = await this.fetchWithTimeout(pubKeyUrl, { + method: 'GET', + headers: { + 'Referer': `${this.baseUrl}/dist/`, + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + + if (!pubKeyRes.ok) { + throw errorWithCode('UNAVAILABLE', `getPubKey failed: HTTP ${pubKeyRes.status}`); + } + + const pubKeyData = await pubKeyRes.json(); + if (pubKeyData.errno !== 0) { + throw errorWithCode('UNAUTHENTICATED', `getPubKey error: ${pubKeyData.errmsg}`); + } + + const pubkeyPem = pubKeyData.data?.pubkey; + if (!pubkeyPem) { + throw errorWithCode('UNAVAILABLE', 'getPubKey returned no public key'); + } + + // Step 2: Encrypt password + const passwordMd5 = md5Hash(password); + const rPassword = rsaEncrypt(password, pubkeyPem); + + // Step 3: Login + const loginBody = new URLSearchParams({ + username, + password: passwordMd5, + rPassword, + }).toString(); + + const loginRes = await this.fetchWithTimeout(`${this.baseUrl}/user/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Referer': `${this.baseUrl}/dist/`, + 'X-Requested-With': 'XMLHttpRequest', + }, + body: loginBody, + }); + + if (!loginRes.ok) { + throw errorWithCode('UNAVAILABLE', `login failed: HTTP ${loginRes.status}`); + } + + const setCookie = loginRes.headers.get('set-cookie'); + if (setCookie) { + this.cookie = setCookie.split(';')[0]; // Extract "PN=value" + } + + // Also parse from response headers + if (!this.cookie) { + const cookies = loginRes.headers.get('set-cookie') || ''; + const match = cookies.match(/PN=([^;]+)/); + if (match) this.cookie = `PN=${match[1]}`; + } + + if (!this.cookie) { + // Try to read login response + const loginData = await loginRes.json().catch(() => ({})); + if (loginData.errno !== 0) { + throw errorWithCode('UNAUTHENTICATED', `login failed: ${loginData.errmsg}`); + } + throw errorWithCode('UNAUTHENTICATED', 'login succeeded but no session cookie received'); + } + } catch (e) { + if (e instanceof GrpcError) throw e; + throw errorWithCode('UNAVAILABLE', `login error: ${e.message}`); + } + } + + async apiGet(path, queryParams = {}) { + if (!this.cookie) { + throw errorWithCode('UNAUTHENTICATED', 'not logged in'); + } + + const url = new URL(`${this.baseUrl}${path}`); + Object.entries(queryParams).forEach(([k, v]) => { + if (v !== undefined && v !== null && v !== '') { + url.searchParams.set(k, String(v)); + } + }); + + const res = await this.fetchWithTimeout(url.toString(), { + method: 'GET', + headers: { + 'Cookie': this.cookie, + 'Referer': `${this.baseUrl}/dist/`, + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + + if (!res.ok) { + throw errorWithCode('UNAVAILABLE', `API request failed: HTTP ${res.status}`); + } + + const data = await res.json(); + if (data.errno === 10401) { + // Session expired, need re-login (handled upstream) + throw errorWithCode('UNAUTHENTICATED', `session expired: ${data.errmsg}`); + } + if (data.errno !== 0 && data.errno !== undefined) { + throw errorWithCode('FAILED_PRECONDITION', `API error: ${data.errmsg} (errno=${data.errno})`); + } + + return data; + } + + async apiPost(path, body = {}) { + if (!this.cookie) { + throw errorWithCode('UNAUTHENTICATED', 'not logged in'); + } + + const res = await this.fetchWithTimeout(`${this.baseUrl}${path}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Cookie': this.cookie, + 'Referer': `${this.baseUrl}/dist/`, + 'X-Requested-With': 'XMLHttpRequest', + }, + body: new URLSearchParams(body).toString(), + }); + + if (!res.ok) { + throw errorWithCode('UNAVAILABLE', `API request failed: HTTP ${res.status}`); + } + + const data = await res.json(); + if (data.errno !== 0 && data.errno !== undefined) { + throw errorWithCode('FAILED_PRECONDITION', `API error: ${data.errmsg} (errno=${data.errno})`); + } + + return data; + } + + async fetchWithTimeout(url, options = {}) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + + try { + const res = await fetch(url, { + ...options, + signal: controller.signal, + ...(this.skipTlsVerify ? { tlsInsecureSkipVerify: true } : {}), + }); + return res; + } catch (e) { + if (e.name === 'AbortError') { + throw errorWithCode('DEADLINE_EXCEEDED', `request timeout after ${this.timeoutMs}ms`); + } + throw errorWithCode('UNAVAILABLE', `fetch failed: ${e.message}`); + } finally { + clearTimeout(timeout); + } + } +} + +// Global session cache keyed by instanceId +const sessions = new Map(); + +function getSession(ctx) { + const bindings = mergedBindings(ctx); + const instanceId = ctx.meta?.instance_id || ctx.meta?.instanceId || 'default'; + const key = `${instanceId}`; + + if (!sessions.has(key)) { + sessions.set(key, new EppSession()); + } + const session = sessions.get(key); + session.configure(ctx); + return session; +} + +async function ensureLogin(ctx) { + const session = getSession(ctx); + const bindings = mergedBindings(ctx); + const username = bindings.username; + const password = bindings.password; + + if (!username || !password) { + throw errorWithCode('INVALID_ARGUMENT', 'username and password are required (configure via secret)'); + } + + if (!session.cookie) { + await session.login(username, password); + } + return session; +} + +// ========== RPC Handlers ========== + +async function handleGetDashboardInfo(ctx) { + const session = await ensureLogin(ctx); + const data = await session.apiGet('/daping/general/info'); + return { data: toStructValue(data.data ?? {}) }; +} + +async function handleListTerminals(ctx) { + const session = await ensureLogin(ctx); + const req = ctx.req || {}; + + const page = toPositiveInt(firstDefined(req?.page, req?.Page)) || 1; + const pageSize = toPositiveInt(firstDefined(req?.page_size, req?.pageSize, req?.PageSize)) || 20; + const keyword = firstDefined(req?.keyword, req?.Keyword) || ''; + const groupId = firstDefined(req?.group_id, req?.groupId) || ''; + const status = firstDefined(req?.status, req?.Status) || ''; + + const data = await session.apiGet('/api/v2/terminal/list', { + page, + rows: pageSize, + keyword, + group_id: groupId, + status, + }); + + const list = Array.isArray(data.data?.list) ? data.data.list : Array.isArray(data.data) ? data.data : []; + const terminals = list.map(mapTerminalInfo); + const total = data.data?.total || data.data?.count || terminals.length; + + return { terminals, total }; +} + +async function handleGetTerminalDetail(ctx) { + const session = await ensureLogin(ctx); + const req = ctx.req || {}; + const terminalId = firstDefined(req?.terminal_id, req?.terminalId, req?.TerminalId); + + if (!terminalId) { + throw errorWithCode('INVALID_ARGUMENT', 'terminal_id is required'); + } + + const data = await session.apiGet('/api/v2/terminal/detail', { id: terminalId }); + const detail = data.data || {}; + + return { + id: String(detail.id ?? terminalId), + name: detail.name ?? detail.hostname ?? '', + ip: detail.ip ?? '', + mac: detail.mac ?? '', + os: detail.os ?? '', + os_version: detail.os_version ?? detail.osVersion ?? '', + status: detail.status ?? '', + group_name: detail.group_name ?? detail.groupName ?? '', + last_online_time: detail.last_online_time ?? detail.lastOnlineTime ?? '', + antivirus_version: detail.antivirus_version ?? '', + antivirus_status: detail.antivirus_status ?? '', + cpu: detail.cpu ?? '', + memory: detail.memory ?? '', + disk: detail.disk ?? '', + raw: toStructValue(detail), + }; +} + +async function handleListAlarms(ctx) { + const session = await ensureLogin(ctx); + const req = ctx.req || {}; + + const page = toPositiveInt(firstDefined(req?.page, req?.Page)) || 1; + const pageSize = toPositiveInt(firstDefined(req?.page_size, req?.pageSize, req?.PageSize)) || 20; + const alarmType = firstDefined(req?.alarm_type, req?.alarmType) || ''; + const startTime = firstDefined(req?.start_time, req?.startTime) || ''; + const endTime = firstDefined(req?.end_time, req?.endTime) || ''; + const severity = firstDefined(req?.severity, req?.Severity) || ''; + + const data = await session.apiGet('/alarmcenter/getloglist', { + page, + rows: pageSize, + type: alarmType, + start_time: startTime, + end_time: endTime, + severity, + }); + + const list = Array.isArray(data.data?.list) ? data.data.list : []; + const alarms = list.map(mapAlarmInfo); + const total = data.data?.total || alarms.length; + const statistics = toStructValue(data.data?.statistics ?? {}); + + return { alarms, total, statistics }; +} + +async function handleGetVirusStats(ctx) { + const session = await ensureLogin(ctx); + const data = await session.apiGet('/daping/Virus/info'); + return { data: toStructValue(data.data ?? {}) }; +} + +async function handleGetLeakFixStats(ctx) { + const session = await ensureLogin(ctx); + const data = await session.apiGet('/daping/Leakfix/info'); + return { data: toStructValue(data.data ?? {}) }; +} + +async function handleGetTerminalHardware(ctx) { + const session = await ensureLogin(ctx); + const req = ctx.req || {}; + const terminalId = firstDefined(req?.terminal_id, req?.terminalId, req?.TerminalId); + + if (!terminalId) { + throw errorWithCode('INVALID_ARGUMENT', 'terminal_id is required'); + } + + const data = await session.apiGet('/api/v2/terminal/hardware', { id: terminalId }); + const hw = data.data || {}; + + return { + terminal_id: String(terminalId), + cpu_model: hw.cpu_model ?? hw.cpuModel ?? '', + cpu_cores: String(hw.cpu_cores ?? hw.cpuCores ?? ''), + memory_size: hw.memory_size ?? hw.memorySize ?? '', + disk_size: hw.disk_size ?? hw.diskSize ?? '', + gpu_model: hw.gpu_model ?? hw.gpuModel ?? '', + motherboard: hw.motherboard ?? '', + network_adapters: Array.isArray(hw.network_adapters) ? hw.network_adapters : [], + raw: toStructValue(hw), + }; +} + +// ========== Value Mapping ========== + +function toStructValue(val) { + if (val === undefined || val === null) return null; + if (typeof val !== 'object') return { stringValue: String(val) }; + if (Array.isArray(val)) { + return { + listValue: { + values: val.map((v) => toStructValue(v)).filter(Boolean), + }, + }; + } + const fields = {}; + for (const [k, v] of Object.entries(val)) { + const mapped = toStructValue(v); + if (mapped !== null) { + fields[k] = mapped; + } + } + return { structValue: { fields } }; +} + +function mapTerminalInfo(item) { + return { + id: String(item.id ?? ''), + name: item.name ?? item.hostname ?? '', + ip: item.ip ?? '', + mac: item.mac ?? '', + os: item.os ?? '', + status: item.status ?? '', + group_name: item.group_name ?? item.groupName ?? '', + last_online_time: item.last_online_time ?? item.lastOnlineTime ?? '', + antivirus_status: item.antivirus_status ?? item.antivirusStatus ?? '', + }; +} + +function mapAlarmInfo(item) { + return { + id: String(item.id ?? ''), + type: item.type ?? item.alarm_type ?? '', + severity: item.severity ?? item.level ?? '', + title: item.title ?? item.name ?? '', + description: item.description ?? item.desc ?? '', + terminal_name: item.terminal_name ?? item.hostname ?? '', + terminal_ip: item.terminal_ip ?? item.ip ?? '', + created_time: item.created_time ?? item.time ?? '', + status: item.status ?? '', + }; +} + +// ========== Method Path Constants ========== + +const GET_DASHBOARD_INFO = `${METHOD_PREFIX}/GetDashboardInfo`; +const LIST_TERMINALS = `${METHOD_PREFIX}/ListTerminals`; +const GET_TERMINAL_DETAIL = `${METHOD_PREFIX}/GetTerminalDetail`; +const LIST_ALARMS = `${METHOD_PREFIX}/ListAlarms`; +const GET_VIRUS_STATS = `${METHOD_PREFIX}/GetVirusStats`; +const GET_LEAKFIX_STATS = `${METHOD_PREFIX}/GetLeakFixStats`; +const GET_TERMINAL_HARDWARE = `${METHOD_PREFIX}/GetTerminalHardware`; + +// ========== RPC Definition ========== + +export function rpcdef(ctx) { + return { + [GET_DASHBOARD_INFO]: async () => handleGetDashboardInfo(ctx), + [LIST_TERMINALS]: async () => handleListTerminals(ctx), + [GET_TERMINAL_DETAIL]: async () => handleGetTerminalDetail(ctx), + [LIST_ALARMS]: async () => handleListAlarms(ctx), + [GET_VIRUS_STATS]: async () => handleGetVirusStats(ctx), + [GET_LEAKFIX_STATS]: async () => handleGetLeakFixStats(ctx), + [GET_TERMINAL_HARDWARE]: async () => handleGetTerminalHardware(ctx), + }; +} + +// ========== Handler Registration ========== + +function wrapHandler(methodPath) { + return async (ctx) => { + const methods = rpcdef(ctx); + return methods[methodPath](); + }; +} + +export const handlers = { + [GET_DASHBOARD_INFO]: wrapHandler(GET_DASHBOARD_INFO), + [LIST_TERMINALS]: wrapHandler(LIST_TERMINALS), + [GET_TERMINAL_DETAIL]: wrapHandler(GET_TERMINAL_DETAIL), + [LIST_ALARMS]: wrapHandler(LIST_ALARMS), + [GET_VIRUS_STATS]: wrapHandler(GET_VIRUS_STATS), + [GET_LEAKFIX_STATS]: wrapHandler(GET_LEAKFIX_STATS), + [GET_TERMINAL_HARDWARE]: wrapHandler(GET_TERMINAL_HARDWARE), +}; + +export const METHOD_GET_DASHBOARD_INFO = GET_DASHBOARD_INFO; +export const METHOD_LIST_TERMINALS = LIST_TERMINALS; +export const METHOD_GET_TERMINAL_DETAIL = GET_TERMINAL_DETAIL; +export const METHOD_LIST_ALARMS = LIST_ALARMS; +export const METHOD_GET_VIRUS_STATS = GET_VIRUS_STATS; +export const METHOD_GET_LEAKFIX_STATS = GET_LEAKFIX_STATS; +export const METHOD_GET_TERMINAL_HARDWARE = GET_TERMINAL_HARDWARE; diff --git a/services/360__360-epp_v10.0.0.08331/src/service.js b/services/360__360-epp_v10.0.0.08331/src/service.js new file mode 100644 index 00000000..f4c667ea --- /dev/null +++ b/services/360__360-epp_v10.0.0.08331/src/service.js @@ -0,0 +1,7 @@ +import { defineService } from "@chaitin-ai/octobus-sdk"; + +import { handlers } from "./360-epp.js"; + +export { handlers } from "./360-epp.js"; + +export const service = defineService({ handlers }); diff --git a/services/360__360-epp_v10.0.0.08331/test/360-epp.test.js b/services/360__360-epp_v10.0.0.08331/test/360-epp.test.js new file mode 100644 index 00000000..d31aecf5 --- /dev/null +++ b/services/360__360-epp_v10.0.0.08331/test/360-epp.test.js @@ -0,0 +1,145 @@ +// 360 EPP service test: parameter validation, API mapping, error mapping, auth flow + +import { describe, it, before, after } from 'node:test'; +import assert from 'node:assert'; +import { fork } from 'child_process'; +import http from 'http'; + +import { _test, rpcdef } from '../src/360-epp.js'; + +const mockUrl = (port) => `http://127.0.0.1:${port}`; + +function startMock() { + return new Promise((resolve, reject) => { + const child = fork(new URL('mock_upstream.js', import.meta.url), [], { + stdio: ['ignore', 'pipe', 'pipe', 'ipc'], + }); + child.on('message', (msg) => { + if (msg?.port) resolve({ child, port: msg.port }); + }); + child.on('error', reject); + child.on('exit', (code) => { + if (code !== 0 && code !== null) reject(new Error(`mock exited with ${code}`)); + }); + setTimeout(() => reject(new Error('mock start timeout')), 10000); + }); +} + +describe('360 EPP Service', () => { + let mockServer; + let mockPort; + + before(async () => { + const mock = await startMock(); + mockServer = mock.child; + mockPort = mock.port; + }); + + after(() => { + if (mockServer) mockServer.kill(); + }); + + function makeCtx(overrides = {}, reqOverrides = {}) { + return { + req: { ...reqOverrides }, + config: {}, + secret: {}, + bindings: { + endpoint: mockUrl(mockPort), + username: 'eppadmin', + password: 'Chaitin123..', + skipTlsVerify: true, + ...overrides, + }, + meta: { instance_id: 'test-instance' }, + limits: {}, + }; + } + + describe('rpcdef', () => { + it('should return handlers for all methods', () => { + const ctx = makeCtx(); + const methods = rpcdef(ctx); + const keys = Object.keys(methods); + assert.ok(keys.length >= 7, `expected >=7 methods, got ${keys.length}`); + assert.ok(keys.every((k) => typeof methods[k] === 'function')); + }); + + it('should throw for missing endpoint', async () => { + const ctx = makeCtx({ endpoint: '' }); + try { + await rpcdef(ctx)['Qihoo360_EPP.Qihoo360_EPP/GetDashboardInfo'](); + assert.fail('should have thrown'); + } catch (e) { + assert.ok(e.message.includes('endpoint') || e.message.includes('baseUrl')); + } + }); + + it('should throw for missing credentials', async () => { + const ctx = makeCtx({ username: '', password: '' }); + try { + await rpcdef(ctx)['Qihoo360_EPP.Qihoo360_EPP/GetDashboardInfo'](); + assert.fail('should have thrown'); + } catch (e) { + assert.ok(e.message.includes('username') || e.message.includes('password')); + } + }); + }); + + describe('GetDashboardInfo', () => { + it('should fetch dashboard info', async () => { + const ctx = makeCtx(); + const result = await rpcdef(ctx)['Qihoo360_EPP.Qihoo360_EPP/GetDashboardInfo'](); + assert.ok(result.data); + }); + }); + + describe('ListAlarms', () => { + it('should fetch alarm list', async () => { + const ctx = makeCtx(); + const result = await rpcdef(ctx)['Qihoo360_EPP.Qihoo360_EPP/ListAlarms'](); + assert.ok(result.alarms); + assert.ok(Array.isArray(result.alarms)); + assert.ok(result.total >= 0); + }); + }); + + describe('GetVirusStats', () => { + it('should fetch virus stats', async () => { + const ctx = makeCtx(); + const result = await rpcdef(ctx)['Qihoo360_EPP.Qihoo360_EPP/GetVirusStats'](); + assert.ok(result.data); + }); + }); + + describe('GetLeakFixStats', () => { + it('should fetch leakfix stats', async () => { + const ctx = makeCtx(); + const result = await rpcdef(ctx)['Qihoo360_EPP.Qihoo360_EPP/GetLeakFixStats'](); + assert.ok(result.data); + }); + }); + + describe('Login flow', () => { + it('should login and cache session', async () => { + const ctx = makeCtx(); + const result1 = await rpcdef(ctx)['Qihoo360_EPP.Qihoo360_EPP/GetDashboardInfo'](); + assert.ok(result1.data); + // Second call should use cached session + const result2 = await rpcdef(ctx)['Qihoo360_EPP.Qihoo360_EPP/GetVirusStats'](); + assert.ok(result2.data); + }); + }); + + describe('Error handling', () => { + it('should handle login failure', async () => { + const ctx = makeCtx({ password: 'wrong_password' }); + try { + await rpcdef(ctx)['Qihoo360_EPP.Qihoo360_EPP/GetDashboardInfo'](); + assert.fail('should have thrown'); + } catch (e) { + assert.ok(e.message.includes('login') || e.message.includes('鉴权') || e.message.includes('auth')); + } + }); + }); +}); diff --git a/services/360__360-epp_v10.0.0.08331/test/mock_upstream.js b/services/360__360-epp_v10.0.0.08331/test/mock_upstream.js new file mode 100644 index 00000000..c997a205 --- /dev/null +++ b/services/360__360-epp_v10.0.0.08331/test/mock_upstream.js @@ -0,0 +1,153 @@ +// Mock upstream server for 360 EPP local testing +// Simulates login and API responses + +import http from 'http'; +import crypto from 'crypto'; + +const PORT = process.env.MOCK_PORT || 0; +const RSA_KEY = crypto.generateKeyPairSync('rsa', { + modulusLength: 1024, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, +}); + +function parseBody(req) { + return new Promise((resolve) => { + let body = ''; + req.on('data', (chunk) => { body += chunk; }); + req.on('end', () => { + try { + resolve(JSON.parse(body)); + } catch { + // URL encoded + const params = new URLSearchParams(body); + const obj = {}; + for (const [k, v] of params) obj[k] = v; + resolve(obj); + } + }); + }); +} + +const server = http.createServer(async (req, res) => { + const url = new URL(req.url, `http://localhost:${PORT}`); + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + + // GET /user/getPubKey + if (url.pathname === '/user/getPubKey' && req.method === 'GET') { + res.end(JSON.stringify({ + errno: 0, + data: { pubkey: RSA_KEY.publicKey }, + errmsg: '成功', + timestamp: Math.floor(Date.now() / 1000), + })); + return; + } + + // POST /user/login + if (url.pathname === '/user/login' && req.method === 'POST') { + const body = await parseBody(req); + const pwdHash = crypto.createHash('md5').update('Chaitin123..').digest('hex'); + + if (body.username === 'eppadmin' && body.password === pwdHash) { + res.setHeader('Set-Cookie', 'PN=mock_session_token_360_epp; Path=/; HttpOnly'); + res.end(JSON.stringify({ + errno: 0, + data: null, + errmsg: '成功', + timestamp: Math.floor(Date.now() / 1000), + })); + } else { + res.end(JSON.stringify({ + errno: 100, + data: null, + errmsg: '鉴权失败', + timestamp: Math.floor(Date.now() / 1000), + })); + } + return; + } + + // GET /daping/general/info + if (url.pathname === '/daping/general/info' && req.method === 'GET') { + if (!req.headers.cookie?.includes('PN=')) { + res.end(JSON.stringify({ errno: 10401, data: null, errmsg: '非法来源' })); + return; + } + res.end(JSON.stringify({ + errno: 0, + data: { terminal_count: 100, online_count: 85, virus_count: 5, leak_count: 12 }, + errmsg: '', + })); + return; + } + + // GET /alarmcenter/getloglist + if (url.pathname === '/alarmcenter/getloglist' && req.method === 'GET') { + if (!req.headers.cookie?.includes('PN=')) { + res.end(JSON.stringify({ errno: 10401, data: null, errmsg: '非法来源' })); + return; + } + res.end(JSON.stringify({ + errno: 0, + data: { + list: [ + { id: 1, type: 'virus', severity: 'high', title: '检测到恶意软件', terminal_name: 'PC-01', terminal_ip: '192.168.1.10', created_time: '2026-06-25 10:00:00', status: 'unhandled' }, + ], + total: 1, + statistics: { '0': 1, '1': 0, '2': 0, '-1': 0 }, + }, + errmsg: 'success', + })); + return; + } + + // GET /daping/Virus/info + if (url.pathname === '/daping/Virus/info' && req.method === 'GET') { + if (!req.headers.cookie?.includes('PN=')) { + res.end(JSON.stringify({ errno: 10401, data: null, errmsg: '非法来源' })); + return; + } + res.end(JSON.stringify({ + errno: 0, + data: { total_virus: 5, cleaned: 3, pending: 2 }, + errmsg: '', + })); + return; + } + + // GET /daping/Leakfix/info + if (url.pathname === '/daping/Leakfix/info' && req.method === 'GET') { + if (!req.headers.cookie?.includes('PN=')) { + res.end(JSON.stringify({ errno: 10401, data: null, errmsg: '非法来源' })); + return; + } + res.end(JSON.stringify({ + errno: 0, + data: { total_leaks: 12, fixed: 8, pending: 4 }, + errmsg: '', + })); + return; + } + + // GET /user/isLogin + if (url.pathname === '/user/isLogin' && req.method === 'GET') { + const loggedIn = req.headers.cookie?.includes('PN='); + res.end(JSON.stringify({ + errno: 0, + data: { is_login: loggedIn, url: '', licalarm: [], auth_check: { alarm: false, alarmmsg: [] }, type: '' }, + errmsg: '成功', + })); + return; + } + + // Default 404 + res.statusCode = 404; + res.end(JSON.stringify({ errno: 404, errmsg: 'not found' })); +}); + +server.listen(PORT, () => { + const addr = server.address(); + console.log(`Mock 360 EPP upstream listening on port ${addr.port}`); + if (process.send) process.send({ port: addr.port }); +}); From df0f1775cb2d83826c49f5b73f08a57b881924ce Mon Sep 17 00:00:00 2001 From: "dong.yan" Date: Tue, 30 Jun 2026 16:59:30 +0800 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20address=20MonkeyScan=20findings=20?= =?UTF-8?q?=E2=80=94=20TLS=20default,=20session=20expiry=20retry,=20=5Ftes?= =?UTF-8?q?t=20import,=20TLS=20verification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config.schema.json: change skipTlsVerify default from true to false (MITM risk) - 360-epp.js: clear cookie and auto-relogin/retry on session expiry (errno=10401) - 360-epp.js: use NODE_TLS_REJECT_UNAUTHORIZED instead of unsupported tlsInsecureSkipVerify option for native fetch - 360-epp.test.js: remove unused _test import and http import (ESM load error) --- .../config.schema.json | 2 +- .../360__360-epp_v10.0.0.08331/src/360-epp.js | 44 ++++++++++++++++--- .../test/360-epp.test.js | 3 +- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/services/360__360-epp_v10.0.0.08331/config.schema.json b/services/360__360-epp_v10.0.0.08331/config.schema.json index 230b3659..cd350aa6 100644 --- a/services/360__360-epp_v10.0.0.08331/config.schema.json +++ b/services/360__360-epp_v10.0.0.08331/config.schema.json @@ -19,7 +19,7 @@ }, "skipTlsVerify": { "type": "boolean", - "default": true, + "default": false, "description": "跳过 TLS 证书验证(适用于自签名证书部署)" } }, diff --git a/services/360__360-epp_v10.0.0.08331/src/360-epp.js b/services/360__360-epp_v10.0.0.08331/src/360-epp.js index 525b5815..517ead44 100644 --- a/services/360__360-epp_v10.0.0.08331/src/360-epp.js +++ b/services/360__360-epp_v10.0.0.08331/src/360-epp.js @@ -79,7 +79,9 @@ class EppSession { this.cookie = null; this.baseUrl = null; this.timeoutMs = DEFAULT_TIMEOUT_MS; - this.skipTlsVerify = true; + this.skipTlsVerify = false; + this.username = null; + this.password = null; } configure(ctx) { @@ -168,6 +170,9 @@ class EppSession { } throw errorWithCode('UNAUTHENTICATED', 'login succeeded but no session cookie received'); } + + this.username = username; + this.password = password; } catch (e) { if (e instanceof GrpcError) throw e; throw errorWithCode('UNAVAILABLE', `login error: ${e.message}`); @@ -201,7 +206,11 @@ class EppSession { const data = await res.json(); if (data.errno === 10401) { - // Session expired, need re-login (handled upstream) + this.cookie = null; + if (this.username && this.password) { + await this.login(this.username, this.password); + return this.apiGet(path, queryParams); + } throw errorWithCode('UNAUTHENTICATED', `session expired: ${data.errmsg}`); } if (data.errno !== 0 && data.errno !== undefined) { @@ -232,6 +241,14 @@ class EppSession { } const data = await res.json(); + if (data.errno === 10401) { + this.cookie = null; + if (this.username && this.password) { + await this.login(this.username, this.password); + return this.apiPost(path, body); + } + throw errorWithCode('UNAUTHENTICATED', `session expired: ${data.errmsg}`); + } if (data.errno !== 0 && data.errno !== undefined) { throw errorWithCode('FAILED_PRECONDITION', `API error: ${data.errmsg} (errno=${data.errno})`); } @@ -244,12 +261,27 @@ class EppSession { const timeout = setTimeout(() => controller.abort(), this.timeoutMs); try { - const res = await fetch(url, { + const fetchOptions = { ...options, signal: controller.signal, - ...(this.skipTlsVerify ? { tlsInsecureSkipVerify: true } : {}), - }); - return res; + }; + // Node.js native fetch (undici) does not support tlsInsecureSkipVerify. + // Use NODE_TLS_REJECT_UNAUTHORIZED for cross-version TLS skipping. + let prevRejectUnauthorized; + if (this.skipTlsVerify) { + prevRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED; + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + } + try { + const res = await fetch(url, fetchOptions); + return res; + } finally { + if (this.skipTlsVerify && prevRejectUnauthorized !== undefined) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = prevRejectUnauthorized; + } else if (this.skipTlsVerify) { + delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; + } + } } catch (e) { if (e.name === 'AbortError') { throw errorWithCode('DEADLINE_EXCEEDED', `request timeout after ${this.timeoutMs}ms`); diff --git a/services/360__360-epp_v10.0.0.08331/test/360-epp.test.js b/services/360__360-epp_v10.0.0.08331/test/360-epp.test.js index d31aecf5..0cb8e1dd 100644 --- a/services/360__360-epp_v10.0.0.08331/test/360-epp.test.js +++ b/services/360__360-epp_v10.0.0.08331/test/360-epp.test.js @@ -3,9 +3,8 @@ import { describe, it, before, after } from 'node:test'; import assert from 'node:assert'; import { fork } from 'child_process'; -import http from 'http'; -import { _test, rpcdef } from '../src/360-epp.js'; +import { rpcdef } from '../src/360-epp.js'; const mockUrl = (port) => `http://127.0.0.1:${port}`; From 4c7be27092f6212a76a844045c500b0d0da84c8f Mon Sep 17 00:00:00 2001 From: "dong.yan" Date: Tue, 30 Jun 2026 17:53:20 +0800 Subject: [PATCH 3/7] fix: set NODE_TLS_REJECT_UNAUTHORIZED once at configure time, add relogin retry limit - Set NODE_TLS_REJECT_UNAUTHORIZED once per session configure call instead of per-request toggle, eliminating race condition on concurrent fetch calls. - Add _reloginAttempts counter with max 1 retry to prevent infinite recursion when the upstream keeps returning errno=10401. --- .../360__360-epp_v10.0.0.08331/src/360-epp.js | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/services/360__360-epp_v10.0.0.08331/src/360-epp.js b/services/360__360-epp_v10.0.0.08331/src/360-epp.js index 517ead44..64b92694 100644 --- a/services/360__360-epp_v10.0.0.08331/src/360-epp.js +++ b/services/360__360-epp_v10.0.0.08331/src/360-epp.js @@ -82,6 +82,7 @@ class EppSession { this.skipTlsVerify = false; this.username = null; this.password = null; + this._reloginAttempts = 0; } configure(ctx) { @@ -90,9 +91,17 @@ class EppSession { bindings.endpoint || bindings.baseUrl || bindings.restBaseUrl || '' ); this.timeoutMs = ctx.limits?.timeoutMs || bindings.timeoutMs || DEFAULT_TIMEOUT_MS; - this.skipTlsVerify = Boolean( + const skipTls = Boolean( bindings.skipTlsVerify || bindings.tlsInsecureSkipVerify || bindings.skip_tls_verify ); + if (skipTls !== this.skipTlsVerify) { + this.skipTlsVerify = skipTls; + // Node.js native fetch (undici) does not accept an agent option. + // Set once per session at configure time; avoids per-request race. + if (skipTls) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + } + } } async login(username, password) { @@ -173,6 +182,7 @@ class EppSession { this.username = username; this.password = password; + this._reloginAttempts = 0; } catch (e) { if (e instanceof GrpcError) throw e; throw errorWithCode('UNAVAILABLE', `login error: ${e.message}`); @@ -207,7 +217,8 @@ class EppSession { const data = await res.json(); if (data.errno === 10401) { this.cookie = null; - if (this.username && this.password) { + if (this.username && this.password && this._reloginAttempts < 1) { + this._reloginAttempts++; await this.login(this.username, this.password); return this.apiGet(path, queryParams); } @@ -243,7 +254,8 @@ class EppSession { const data = await res.json(); if (data.errno === 10401) { this.cookie = null; - if (this.username && this.password) { + if (this.username && this.password && this._reloginAttempts < 1) { + this._reloginAttempts++; await this.login(this.username, this.password); return this.apiPost(path, body); } @@ -261,27 +273,11 @@ class EppSession { const timeout = setTimeout(() => controller.abort(), this.timeoutMs); try { - const fetchOptions = { + const res = await fetch(url, { ...options, signal: controller.signal, - }; - // Node.js native fetch (undici) does not support tlsInsecureSkipVerify. - // Use NODE_TLS_REJECT_UNAUTHORIZED for cross-version TLS skipping. - let prevRejectUnauthorized; - if (this.skipTlsVerify) { - prevRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED; - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - } - try { - const res = await fetch(url, fetchOptions); - return res; - } finally { - if (this.skipTlsVerify && prevRejectUnauthorized !== undefined) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = prevRejectUnauthorized; - } else if (this.skipTlsVerify) { - delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; - } - } + }); + return res; } catch (e) { if (e.name === 'AbortError') { throw errorWithCode('DEADLINE_EXCEEDED', `request timeout after ${this.timeoutMs}ms`); From c17501510253f61f25c7c8a2fcc923327b84d796 Mon Sep 17 00:00:00 2001 From: "dong.yan" Date: Tue, 30 Jun 2026 18:16:27 +0800 Subject: [PATCH 4/7] fix: scope TLS skip to per-connection Agent, use parameter for relogin guard - Replace NODE_TLS_REJECT_UNAUTHORIZED (global, permanent) with https.Agent({ rejectUnauthorized: false }) used only by _requestHttps when skipTlsVerify is true. TLS skipping is now scoped to individual connections and cannot affect the rest of the process. - Replace _reloginAttempts instance counter with a retried parameter in apiGet/apiPost. The old counter was reset to 0 inside login(), making the retry limit ineffective and allowing infinite recursion when the upstream persistently returns errno=10401. The parameter is immune to side-effects from login(). --- .../360__360-epp_v10.0.0.08331/src/360-epp.js | 83 ++++++++++++++----- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/services/360__360-epp_v10.0.0.08331/src/360-epp.js b/services/360__360-epp_v10.0.0.08331/src/360-epp.js index 64b92694..a46681af 100644 --- a/services/360__360-epp_v10.0.0.08331/src/360-epp.js +++ b/services/360__360-epp_v10.0.0.08331/src/360-epp.js @@ -4,6 +4,7 @@ import { GrpcError, grpcStatus } from '@chaitin-ai/octobus-sdk'; import crypto from 'crypto'; +import https from 'node:https'; const DEFAULT_TIMEOUT_MS = 30000; @@ -74,6 +75,9 @@ function md5Hash(str) { // ========== Session Management ========== +// Per-connection TLS skip agent; scoped to connections, not global. +const insecureAgent = new https.Agent({ rejectUnauthorized: false }); + class EppSession { constructor() { this.cookie = null; @@ -82,7 +86,6 @@ class EppSession { this.skipTlsVerify = false; this.username = null; this.password = null; - this._reloginAttempts = 0; } configure(ctx) { @@ -91,17 +94,9 @@ class EppSession { bindings.endpoint || bindings.baseUrl || bindings.restBaseUrl || '' ); this.timeoutMs = ctx.limits?.timeoutMs || bindings.timeoutMs || DEFAULT_TIMEOUT_MS; - const skipTls = Boolean( + this.skipTlsVerify = Boolean( bindings.skipTlsVerify || bindings.tlsInsecureSkipVerify || bindings.skip_tls_verify ); - if (skipTls !== this.skipTlsVerify) { - this.skipTlsVerify = skipTls; - // Node.js native fetch (undici) does not accept an agent option. - // Set once per session at configure time; avoids per-request race. - if (skipTls) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - } - } } async login(username, password) { @@ -182,14 +177,13 @@ class EppSession { this.username = username; this.password = password; - this._reloginAttempts = 0; } catch (e) { if (e instanceof GrpcError) throw e; throw errorWithCode('UNAVAILABLE', `login error: ${e.message}`); } } - async apiGet(path, queryParams = {}) { + async apiGet(path, queryParams = {}, retried = false) { if (!this.cookie) { throw errorWithCode('UNAUTHENTICATED', 'not logged in'); } @@ -217,10 +211,9 @@ class EppSession { const data = await res.json(); if (data.errno === 10401) { this.cookie = null; - if (this.username && this.password && this._reloginAttempts < 1) { - this._reloginAttempts++; + if (this.username && this.password && !retried) { await this.login(this.username, this.password); - return this.apiGet(path, queryParams); + return this.apiGet(path, queryParams, true); } throw errorWithCode('UNAUTHENTICATED', `session expired: ${data.errmsg}`); } @@ -231,7 +224,7 @@ class EppSession { return data; } - async apiPost(path, body = {}) { + async apiPost(path, body = {}, retried = false) { if (!this.cookie) { throw errorWithCode('UNAUTHENTICATED', 'not logged in'); } @@ -254,10 +247,9 @@ class EppSession { const data = await res.json(); if (data.errno === 10401) { this.cookie = null; - if (this.username && this.password && this._reloginAttempts < 1) { - this._reloginAttempts++; + if (this.username && this.password && !retried) { await this.login(this.username, this.password); - return this.apiPost(path, body); + return this.apiPost(path, body, true); } throw errorWithCode('UNAUTHENTICATED', `session expired: ${data.errmsg}`); } @@ -268,15 +260,60 @@ class EppSession { return data; } + // Wraps https.request with a custom agent to produce a fetch-like Response. + // Used only when skipTlsVerify is true, so TLS skipping is scoped to these + // connections and does not affect the global process. + _requestHttps(url, options) { + return new Promise((resolve, reject) => { + const u = new URL(url); + const req = https.request({ + hostname: u.hostname, + port: u.port || 443, + path: u.pathname + u.search, + method: options.method || 'GET', + headers: options.headers || {}, + agent: insecureAgent, + signal: options.signal, + }, (res) => { + const chunks = []; + res.on('data', (c) => chunks.push(c)); + res.on('end', () => { + const body = Buffer.concat(chunks); + const headers = res.headers; + resolve({ + ok: res.statusCode >= 200 && res.statusCode < 300, + status: res.statusCode, + headers: { + get: (name) => { + const lc = name.toLowerCase(); + return headers[lc] ?? null; + }, + }, + json: async () => JSON.parse(body.toString()), + text: async () => body.toString(), + }); + }); + }); + req.on('error', reject); + if (options.body) req.write(options.body); + req.end(); + }); + } + async fetchWithTimeout(url, options = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.timeoutMs); try { - const res = await fetch(url, { - ...options, - signal: controller.signal, - }); + const opts = { ...options, signal: controller.signal }; + // Native fetch (undici) does not support a custom HTTPS agent. + // When TLS verification must be skipped, use https.request with a + // per-connection insecureAgent so the process-wide TLS setting is + // never touched. + const useInsecure = this.skipTlsVerify && url.startsWith('https:'); + const res = useInsecure + ? await this._requestHttps(url, opts) + : await fetch(url, opts); return res; } catch (e) { if (e.name === 'AbortError') { From 4049595c4dfd5c8b00fdd8b3a16fc442fbaa77f0 Mon Sep 17 00:00:00 2001 From: "dong.yan" Date: Tue, 30 Jun 2026 18:21:26 +0800 Subject: [PATCH 5/7] fix: add res.on('error') handler in _requestHttps to prevent hanging Promise Missing error listener on the response stream could leave the Promise unresolved when the upstream resets the connection mid-response. --- services/360__360-epp_v10.0.0.08331/src/360-epp.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/360__360-epp_v10.0.0.08331/src/360-epp.js b/services/360__360-epp_v10.0.0.08331/src/360-epp.js index a46681af..1c67dbc1 100644 --- a/services/360__360-epp_v10.0.0.08331/src/360-epp.js +++ b/services/360__360-epp_v10.0.0.08331/src/360-epp.js @@ -277,6 +277,7 @@ class EppSession { }, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); + res.on('error', reject); res.on('end', () => { const body = Buffer.concat(chunks); const headers = res.headers; From d2943ab1fceedf38d346b297a02833465ed5f46a Mon Sep 17 00:00:00 2001 From: "dong.yan" Date: Wed, 1 Jul 2026 08:51:29 +0800 Subject: [PATCH 6/7] fix: rename service dir, add bin wrapper and dispatcher entry for validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename 360__360-epp_v10.0.0.08331 → 360__360-epp_v10-0-0-08331 for dir regex compliance - Add bin/360-epp.js wrapper and octobus-tentacles dispatcher entry - Update package.json bin and files for 360-epp service - Fix session reuse across tests by clearing cookie on credential change - Update README with corrected directory references --- .../README.md | 6 +++--- .../bin/360-epp.js | 0 .../config.schema.json | 0 .../package.json | 0 .../proto/360_epp.proto | 0 .../secret.schema.json | 0 .../service.json | 0 .../src/360-epp.js | 13 +++++++++++++ .../src/service.js | 0 .../test/360-epp.test.js | 0 .../test/mock_upstream.js | 0 services/bin/360-epp.js | 10 ++++++++++ services/bin/octobus-tentacles.js | 4 ++++ services/package.json | 3 +++ 14 files changed, 33 insertions(+), 3 deletions(-) rename services/{360__360-epp_v10.0.0.08331 => 360__360-epp_v10-0-0-08331}/README.md (94%) rename services/{360__360-epp_v10.0.0.08331 => 360__360-epp_v10-0-0-08331}/bin/360-epp.js (100%) rename services/{360__360-epp_v10.0.0.08331 => 360__360-epp_v10-0-0-08331}/config.schema.json (100%) rename services/{360__360-epp_v10.0.0.08331 => 360__360-epp_v10-0-0-08331}/package.json (100%) rename services/{360__360-epp_v10.0.0.08331 => 360__360-epp_v10-0-0-08331}/proto/360_epp.proto (100%) rename services/{360__360-epp_v10.0.0.08331 => 360__360-epp_v10-0-0-08331}/secret.schema.json (100%) rename services/{360__360-epp_v10.0.0.08331 => 360__360-epp_v10-0-0-08331}/service.json (100%) rename services/{360__360-epp_v10.0.0.08331 => 360__360-epp_v10-0-0-08331}/src/360-epp.js (97%) rename services/{360__360-epp_v10.0.0.08331 => 360__360-epp_v10-0-0-08331}/src/service.js (100%) rename services/{360__360-epp_v10.0.0.08331 => 360__360-epp_v10-0-0-08331}/test/360-epp.test.js (100%) rename services/{360__360-epp_v10.0.0.08331 => 360__360-epp_v10-0-0-08331}/test/mock_upstream.js (100%) create mode 100755 services/bin/360-epp.js diff --git a/services/360__360-epp_v10.0.0.08331/README.md b/services/360__360-epp_v10-0-0-08331/README.md similarity index 94% rename from services/360__360-epp_v10.0.0.08331/README.md rename to services/360__360-epp_v10-0-0-08331/README.md index fe0aca12..6aca7990 100644 --- a/services/360__360-epp_v10.0.0.08331/README.md +++ b/services/360__360-epp_v10-0-0-08331/README.md @@ -5,7 +5,7 @@ ## 导入 OctoBus ```bash -octobus service import --id 360-epp ./services/360__360-epp_v10.0.0.08331 +octobus service import --id 360-epp ./services/360__360-epp_v10-0-0-08331 ``` ## 包文件说明 @@ -102,7 +102,7 @@ octobus service import --id 360-epp ./services/360__360-epp_v10.0.0.08331 ```bash cd services -npm run validate -- --service-dir 360__360-epp_v10.0.0.08331 -npm test -- --service-dir 360__360-epp_v10.0.0.08331 +npm run validate -- --service-dir 360__360-epp_v10-0-0-08331 +npm test -- --service-dir 360__360-epp_v10-0-0-08331 npm run pack:check ``` diff --git a/services/360__360-epp_v10.0.0.08331/bin/360-epp.js b/services/360__360-epp_v10-0-0-08331/bin/360-epp.js similarity index 100% rename from services/360__360-epp_v10.0.0.08331/bin/360-epp.js rename to services/360__360-epp_v10-0-0-08331/bin/360-epp.js diff --git a/services/360__360-epp_v10.0.0.08331/config.schema.json b/services/360__360-epp_v10-0-0-08331/config.schema.json similarity index 100% rename from services/360__360-epp_v10.0.0.08331/config.schema.json rename to services/360__360-epp_v10-0-0-08331/config.schema.json diff --git a/services/360__360-epp_v10.0.0.08331/package.json b/services/360__360-epp_v10-0-0-08331/package.json similarity index 100% rename from services/360__360-epp_v10.0.0.08331/package.json rename to services/360__360-epp_v10-0-0-08331/package.json diff --git a/services/360__360-epp_v10.0.0.08331/proto/360_epp.proto b/services/360__360-epp_v10-0-0-08331/proto/360_epp.proto similarity index 100% rename from services/360__360-epp_v10.0.0.08331/proto/360_epp.proto rename to services/360__360-epp_v10-0-0-08331/proto/360_epp.proto diff --git a/services/360__360-epp_v10.0.0.08331/secret.schema.json b/services/360__360-epp_v10-0-0-08331/secret.schema.json similarity index 100% rename from services/360__360-epp_v10.0.0.08331/secret.schema.json rename to services/360__360-epp_v10-0-0-08331/secret.schema.json diff --git a/services/360__360-epp_v10.0.0.08331/service.json b/services/360__360-epp_v10-0-0-08331/service.json similarity index 100% rename from services/360__360-epp_v10.0.0.08331/service.json rename to services/360__360-epp_v10-0-0-08331/service.json diff --git a/services/360__360-epp_v10.0.0.08331/src/360-epp.js b/services/360__360-epp_v10-0-0-08331/src/360-epp.js similarity index 97% rename from services/360__360-epp_v10.0.0.08331/src/360-epp.js rename to services/360__360-epp_v10-0-0-08331/src/360-epp.js index 1c67dbc1..e46eff47 100644 --- a/services/360__360-epp_v10.0.0.08331/src/360-epp.js +++ b/services/360__360-epp_v10-0-0-08331/src/360-epp.js @@ -86,6 +86,8 @@ class EppSession { this.skipTlsVerify = false; this.username = null; this.password = null; + this.configuredUsername = ''; + this.configuredPassword = ''; } configure(ctx) { @@ -93,6 +95,17 @@ class EppSession { this.baseUrl = normalizeBaseUrl( bindings.endpoint || bindings.baseUrl || bindings.restBaseUrl || '' ); + + const newUsername = bindings.username || ''; + const newPassword = bindings.password || ''; + + if ((newUsername && this.configuredUsername && newUsername !== this.configuredUsername) || + (newPassword && this.configuredPassword && newPassword !== this.configuredPassword)) { + this.cookie = null; + } + this.configuredUsername = newUsername; + this.configuredPassword = newPassword; + this.timeoutMs = ctx.limits?.timeoutMs || bindings.timeoutMs || DEFAULT_TIMEOUT_MS; this.skipTlsVerify = Boolean( bindings.skipTlsVerify || bindings.tlsInsecureSkipVerify || bindings.skip_tls_verify diff --git a/services/360__360-epp_v10.0.0.08331/src/service.js b/services/360__360-epp_v10-0-0-08331/src/service.js similarity index 100% rename from services/360__360-epp_v10.0.0.08331/src/service.js rename to services/360__360-epp_v10-0-0-08331/src/service.js diff --git a/services/360__360-epp_v10.0.0.08331/test/360-epp.test.js b/services/360__360-epp_v10-0-0-08331/test/360-epp.test.js similarity index 100% rename from services/360__360-epp_v10.0.0.08331/test/360-epp.test.js rename to services/360__360-epp_v10-0-0-08331/test/360-epp.test.js diff --git a/services/360__360-epp_v10.0.0.08331/test/mock_upstream.js b/services/360__360-epp_v10-0-0-08331/test/mock_upstream.js similarity index 100% rename from services/360__360-epp_v10.0.0.08331/test/mock_upstream.js rename to services/360__360-epp_v10-0-0-08331/test/mock_upstream.js diff --git a/services/bin/360-epp.js b/services/bin/360-epp.js new file mode 100755 index 00000000..431dd544 --- /dev/null +++ b/services/bin/360-epp.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../360__360-epp_v10-0-0-08331/src/service.js"; + +runServiceMain(service, { + entryFile: fileURLToPath(new URL("../360__360-epp_v10-0-0-08331/bin/360-epp.js", import.meta.url)), +}); diff --git a/services/bin/octobus-tentacles.js b/services/bin/octobus-tentacles.js index 6138109e..734c225f 100755 --- a/services/bin/octobus-tentacles.js +++ b/services/bin/octobus-tentacles.js @@ -5,6 +5,10 @@ import { runServiceMain } from "@chaitin-ai/octobus-sdk"; import { Command } from "commander"; const services = { + "360-epp": { + entryFile: "../360__360-epp_v10-0-0-08331/bin/360-epp.js", + serviceModule: "../360__360-epp_v10-0-0-08331/src/service.js", + }, "alibaba-cloud-simple-application-server-firewall": { entryFile: "../alibaba-cloud__simple-application-server-firewall/bin/alibaba-cloud-simple-application-server-firewall.js", serviceModule: "../alibaba-cloud__simple-application-server-firewall/src/service.js", diff --git a/services/package.json b/services/package.json index dd6a1015..f20d0388 100644 --- a/services/package.json +++ b/services/package.json @@ -5,6 +5,7 @@ "type": "module", "bin": { "octobus-tentacles": "bin/octobus-tentacles.js", + "360-epp": "bin/360-epp.js", "alibaba-cloud-simple-application-server-firewall": "bin/alibaba-cloud-simple-application-server-firewall.js", "das-gateway-v3": "bin/das-gateway-v3.js", "das-tgfw-v6": "bin/das-tgfw-v6.js", @@ -53,6 +54,7 @@ "wd-k01": "bin/wd-k01.js" }, "files": [ + "bin/360-epp.js", "bin/alibaba-cloud-simple-application-server-firewall.js", "bin/das-gateway-v3.js", "bin/das-tgfw-v6.js", @@ -99,6 +101,7 @@ "bin/venus-ads-v3-6.js", "bin/wd-k01.js", "bin/wangsu-label-ip.js", + "360__360-epp_v10-0-0-08331", "alibaba-cloud__simple-application-server-firewall", "das__gateway_v3", "das__tgfw_v6", From 77133a97e281eee5f7b50d4ab711e9b6e825835a Mon Sep 17 00:00:00 2001 From: "dong.yan" Date: Wed, 1 Jul 2026 09:04:55 +0800 Subject: [PATCH 7/7] fix: simplify credential change detection to handle empty-to-nonempty transitions The previous condition required both new and configured values to be truthy before comparing, which missed the case where credentials change from non-empty to empty (e.g. user removes username/password from config). Replace with direct string comparison that covers all transition scenarios. --- services/360__360-epp_v10-0-0-08331/src/360-epp.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/360__360-epp_v10-0-0-08331/src/360-epp.js b/services/360__360-epp_v10-0-0-08331/src/360-epp.js index e46eff47..d5baf934 100644 --- a/services/360__360-epp_v10-0-0-08331/src/360-epp.js +++ b/services/360__360-epp_v10-0-0-08331/src/360-epp.js @@ -99,8 +99,7 @@ class EppSession { const newUsername = bindings.username || ''; const newPassword = bindings.password || ''; - if ((newUsername && this.configuredUsername && newUsername !== this.configuredUsername) || - (newPassword && this.configuredPassword && newPassword !== this.configuredPassword)) { + if (newUsername !== this.configuredUsername || newPassword !== this.configuredPassword) { this.cookie = null; } this.configuredUsername = newUsername;