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