| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- 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
- * <p>
- * \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<String> 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');
|