import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = join(dirname(fileURLToPath(import.meta.url)), '..'); const softphoneDir = join(root, 'src/main/java/com/fs/comm/softphone'); /** Use \\u escapes in templates; write UTF-8 no BOM; normalize CRLF to LF. */ const files = { 'SoftPhoneClientIpResolver.java': `package com.fs.comm.softphone; import com.fs.common.utils.StringUtils; import com.fs.common.utils.ip.IpUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServletServerHttpRequest; import javax.servlet.http.HttpServletRequest; /** * \u89e3\u6790\u5750\u5e2d\u771f\u5b9e\u5ba2\u6237\u7aef IP\uff0c\u4f9b\u4e0a\u6e38 IPCC/SIP firewalld \u767d\u540d\u5355\u4f7f\u7528\u3002 *

* \u4f18\u5148 WS \u63e1\u624b\u5934 X-Real-IP / X-Forwarded-For\uff1b\u82e5\u4e3a\u56de\u73af\u5730\u5740\u5219\u56de\u9000\u5230 wsTicket \u4e2d * getToolbarBasicParam HTTP \u8bf7\u6c42\u65f6\u91c7\u96c6\u7684 clientIp\u3002 */ public final class SoftPhoneClientIpResolver { private SoftPhoneClientIpResolver() { } public static String resolve(ServerHttpRequest request) { if (request instanceof ServletServerHttpRequest) { HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); return IpUtils.getIpAddr(servletRequest); } HttpHeaders headers = request.getHeaders(); String ip = firstHeader(headers, "X-Real-IP"); if (StringUtils.isBlank(ip)) { ip = firstHeader(headers, "X-Forwarded-For"); } return IpUtils.normalizeClientIp(ip); } /** * \u5408\u5e76 WS \u63e1\u624b IP \u4e0e\u7968\u636e\u4e2d\u9884\u5b58\u7684\u5750\u5e2d\u8bbf\u95ee IP\uff0c\u53d6\u53ef\u7528\u4e8e SIP \u52a0\u767d\u7684\u516c\u7f51/\u771f\u5b9e\u5730\u5740\u3002 */ public static String resolveEffectiveClientIp(ServerHttpRequest request, String ticketClientIp) { String handshakeIp = normalizeIp(resolve(request)); String storedIp = normalizeIp(ticketClientIp); if (isPreferredClientIp(handshakeIp)) { return handshakeIp; } if (isPreferredClientIp(storedIp)) { return storedIp; } if (StringUtils.isNotBlank(handshakeIp)) { return handshakeIp; } return storedIp; } static boolean isPreferredClientIp(String ip) { if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip.trim())) { return false; } String value = ip.trim(); if ("127.0.0.1".equals(value) || "0:0:0:0:0:0:0:1".equals(value) || "::1".equals(value)) { return false; } if ("localhost".equalsIgnoreCase(value)) { return false; } return !IpUtils.internalIp(value); } private static String normalizeIp(String ip) { return IpUtils.normalizeClientIp(ip); } private static String firstHeader(HttpHeaders headers, String name) { if (headers == null) { return null; } String value = headers.getFirst(name); return StringUtils.isBlank(value) ? null : value.trim(); } } `, 'SoftPhoneProxyChannel.java': `package com.fs.comm.softphone; /** * \u8f6f\u7535\u8bdd WebSocket \u4ee3\u7406\u901a\u9053\u7c7b\u578b */ public enum SoftPhoneProxyChannel { /** \u547c\u53eb\u63a7\u5236 ccPhoneBar\uff08\u7b7e\u5165\u65f6\u89e6\u53d1 IP \u767d\u540d\u5355\uff09 */ IPCC, /** JsSIP SIP over WSS\uff08WebRTC \u5a92\u4f53\uff09 */ JS_SIP } `, 'SoftPhoneUpstreamWebSocketClient.java': `package com.fs.comm.softphone; import com.fs.common.utils.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketHttpHeaders; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.client.standard.StandardWebSocketClient; import java.net.URI; import java.util.Collections; import java.util.concurrent.TimeUnit; /** * \u8fde\u63a5 SIP / IPCC \u4e0a\u6e38 WebSocket\uff0c\u900f\u4f20 X-Real-IP / X-Forwarded-For\uff1bJsSIP \u901a\u9053\u534f\u5546 sip \u5b50\u534f\u8bae\u3002 */ @Component public class SoftPhoneUpstreamWebSocketClient { private static final Logger log = LoggerFactory.getLogger(SoftPhoneUpstreamWebSocketClient.class); static final String HEADER_X_REAL_IP = "X-Real-IP"; static final String HEADER_X_FORWARDED_FOR = "X-Forwarded-For"; static final String SIP_SUBPROTOCOL = "sip"; private final StandardWebSocketClient webSocketClient = new StandardWebSocketClient(); public WebSocketSession connect(URI uri, WebSocketHandler handler, String clientIp, long timeoutMs, SoftPhoneProxyChannel channel) throws Exception { String normalizedIp = normalizeClientIp(clientIp); WebSocketHttpHeaders headers = buildUpstreamHeaders(normalizedIp, channel); log.info("\u8f6f\u7535\u8bdd WS \u8fde\u63a5\u4e0a\u6e38: host={}, port={}, channel={}, subprotocol={}, {}={}, {}={}", uri.getHost(), uri.getPort() > 0 ? uri.getPort() : "default", channel, headers.getSecWebSocketProtocol(), HEADER_X_REAL_IP, headers.getFirst(HEADER_X_REAL_IP), HEADER_X_FORWARDED_FOR, headers.getFirst(HEADER_X_FORWARDED_FOR)); if (StringUtils.isBlank(normalizedIp)) { log.warn("\u8f6f\u7535\u8bdd WS \u4e0a\u6e38\u8fde\u63a5\u7f3a\u5c11\u6709\u6548 clientIp\uff0cSIP \u4fa7\u53ef\u80fd\u65e0\u6cd5\u52a0\u767d: host={}", uri.getHost()); } return webSocketClient.doHandshake(handler, headers, uri).get(timeoutMs, TimeUnit.MILLISECONDS); } static WebSocketHttpHeaders buildUpstreamHeaders(String clientIp, SoftPhoneProxyChannel channel) { WebSocketHttpHeaders headers = new WebSocketHttpHeaders(); if (StringUtils.isNotBlank(clientIp) && !"unknown".equalsIgnoreCase(clientIp.trim())) { String ip = clientIp.trim(); headers.set(HEADER_X_REAL_IP, ip); headers.set(HEADER_X_FORWARDED_FOR, ip); } if (SoftPhoneProxyChannel.JS_SIP == channel) { headers.setSecWebSocketProtocol(Collections.singletonList(SIP_SUBPROTOCOL)); } return headers; } static String normalizeClientIp(String clientIp) { if (StringUtils.isBlank(clientIp) || "unknown".equalsIgnoreCase(clientIp.trim())) { return null; } return clientIp.trim(); } } `, 'CommSoftPhoneWsTicket.java': `package com.fs.comm.softphone; import lombok.Data; import java.io.Serializable; import java.util.HashSet; import java.util.Set; /** * \u8f6f\u7535\u8bdd WebSocket \u7f51\u5173\u5c42\u77ed\u65f6\u7968\u636e\uff08\u5b58 Redis\uff0cIPCC / JsSIP \u5404\u6d88\u8d39\u4e00\u6b21\uff09\u3002 */ @Data public class CommSoftPhoneWsTicket implements Serializable { private static final long serialVersionUID = 1L; private String ticketId; private Long tenantId; private Long companyId; private Long companyUserId; /** \u7ed1\u5b9a\u7684 EasyCall loginToken\uff08\u4ec5\u7f51\u5173\u5185\u90e8\u4f7f\u7528\uff0c\u900f\u4f20 IPCC\uff0c\u4e0d\u4e0b\u53d1\u524d\u7aef\uff09 */ private String loginToken; /** \u5206\u673a\u53f7\uff08\u53ef\u9009\uff0c\u4fbf\u4e8e\u5ba1\u8ba1\uff09 */ private String extNum; /** \u5750\u5e2d\u8bbf\u95ee IP\uff08getToolbarBasicParam HTTP \u8bf7\u6c42\u65f6\u91c7\u96c6\uff0c\u4f9b WS \u63e1\u624b\u900f\u4f20 SIP \u767d\u540d\u5355\uff09 */ private String clientIp; private Long createTime; /** \u5df2\u63e1\u624b\u7684\u4ee3\u7406\u901a\u9053\uff08IPCC / JS_SIP \u5404\u5141\u8bb8\u4e00\u6b21\uff09 */ private Set usedChannels = new HashSet<>(); } `, }; function writeUtf8(path, content) { const normalized = content.replace(/\r\n/g, '\n'); writeFileSync(path, normalized, { encoding: 'utf8' }); } for (const [name, content] of Object.entries(files)) { const path = join(softphoneDir, name); writeUtf8(path, content); console.log('fixed', name); } function listJavaFiles(dir) { const result = []; for (const entry of readdirSync(dir)) { const full = join(dir, entry); if (statSync(full).isDirectory()) { result.push(...listJavaFiles(full)); } else if (entry.endsWith('.java') && !Object.prototype.hasOwnProperty.call(files, entry)) { result.push(full); } } return result; } for (const path of listJavaFiles(softphoneDir)) { const raw = readFileSync(path); const bom = raw.length >= 3 && raw[0] === 0xef && raw[1] === 0xbb && raw[2] === 0xbf; let text = raw.toString('utf8'); if (bom) { text = raw.slice(3).toString('utf8'); } const normalized = text.replace(/\r\n/g, '\n'); if (bom || text !== normalized || raw.toString('utf8') !== normalized) { writeUtf8(path, normalized); console.log('normalized', path.split(/[/\\]/).pop()); } } console.log('done');