fix_softphone_all_utf8.mjs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
  2. import { join, dirname } from 'node:path';
  3. import { fileURLToPath } from 'node:url';
  4. const root = join(dirname(fileURLToPath(import.meta.url)), '..');
  5. const softphoneDir = join(root, 'src/main/java/com/fs/comm/softphone');
  6. /** Use \\u escapes in templates; write UTF-8 no BOM; normalize CRLF to LF. */
  7. const files = {
  8. 'SoftPhoneClientIpResolver.java': `package com.fs.comm.softphone;
  9. import com.fs.common.utils.StringUtils;
  10. import com.fs.common.utils.ip.IpUtils;
  11. import org.springframework.http.HttpHeaders;
  12. import org.springframework.http.server.ServerHttpRequest;
  13. import org.springframework.http.server.ServletServerHttpRequest;
  14. import javax.servlet.http.HttpServletRequest;
  15. /**
  16. * \u89e3\u6790\u5750\u5e2d\u771f\u5b9e\u5ba2\u6237\u7aef IP\uff0c\u4f9b\u4e0a\u6e38 IPCC/SIP firewalld \u767d\u540d\u5355\u4f7f\u7528\u3002
  17. * <p>
  18. * \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
  19. * getToolbarBasicParam HTTP \u8bf7\u6c42\u65f6\u91c7\u96c6\u7684 clientIp\u3002
  20. */
  21. public final class SoftPhoneClientIpResolver {
  22. private SoftPhoneClientIpResolver() {
  23. }
  24. public static String resolve(ServerHttpRequest request) {
  25. if (request instanceof ServletServerHttpRequest) {
  26. HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
  27. return IpUtils.getIpAddr(servletRequest);
  28. }
  29. HttpHeaders headers = request.getHeaders();
  30. String ip = firstHeader(headers, "X-Real-IP");
  31. if (StringUtils.isBlank(ip)) {
  32. ip = firstHeader(headers, "X-Forwarded-For");
  33. }
  34. return IpUtils.normalizeClientIp(ip);
  35. }
  36. /**
  37. * \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
  38. */
  39. public static String resolveEffectiveClientIp(ServerHttpRequest request, String ticketClientIp) {
  40. String handshakeIp = normalizeIp(resolve(request));
  41. String storedIp = normalizeIp(ticketClientIp);
  42. if (isPreferredClientIp(handshakeIp)) {
  43. return handshakeIp;
  44. }
  45. if (isPreferredClientIp(storedIp)) {
  46. return storedIp;
  47. }
  48. if (StringUtils.isNotBlank(handshakeIp)) {
  49. return handshakeIp;
  50. }
  51. return storedIp;
  52. }
  53. static boolean isPreferredClientIp(String ip) {
  54. if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip.trim())) {
  55. return false;
  56. }
  57. String value = ip.trim();
  58. if ("127.0.0.1".equals(value) || "0:0:0:0:0:0:0:1".equals(value) || "::1".equals(value)) {
  59. return false;
  60. }
  61. if ("localhost".equalsIgnoreCase(value)) {
  62. return false;
  63. }
  64. return !IpUtils.internalIp(value);
  65. }
  66. private static String normalizeIp(String ip) {
  67. return IpUtils.normalizeClientIp(ip);
  68. }
  69. private static String firstHeader(HttpHeaders headers, String name) {
  70. if (headers == null) {
  71. return null;
  72. }
  73. String value = headers.getFirst(name);
  74. return StringUtils.isBlank(value) ? null : value.trim();
  75. }
  76. }
  77. `,
  78. 'SoftPhoneProxyChannel.java': `package com.fs.comm.softphone;
  79. /**
  80. * \u8f6f\u7535\u8bdd WebSocket \u4ee3\u7406\u901a\u9053\u7c7b\u578b
  81. */
  82. public enum SoftPhoneProxyChannel {
  83. /** \u547c\u53eb\u63a7\u5236 ccPhoneBar\uff08\u7b7e\u5165\u65f6\u89e6\u53d1 IP \u767d\u540d\u5355\uff09 */
  84. IPCC,
  85. /** JsSIP SIP over WSS\uff08WebRTC \u5a92\u4f53\uff09 */
  86. JS_SIP
  87. }
  88. `,
  89. 'SoftPhoneUpstreamWebSocketClient.java': `package com.fs.comm.softphone;
  90. import com.fs.common.utils.StringUtils;
  91. import org.slf4j.Logger;
  92. import org.slf4j.LoggerFactory;
  93. import org.springframework.stereotype.Component;
  94. import org.springframework.web.socket.WebSocketHandler;
  95. import org.springframework.web.socket.WebSocketHttpHeaders;
  96. import org.springframework.web.socket.WebSocketSession;
  97. import org.springframework.web.socket.client.standard.StandardWebSocketClient;
  98. import java.net.URI;
  99. import java.util.Collections;
  100. import java.util.concurrent.TimeUnit;
  101. /**
  102. * \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
  103. */
  104. @Component
  105. public class SoftPhoneUpstreamWebSocketClient {
  106. private static final Logger log = LoggerFactory.getLogger(SoftPhoneUpstreamWebSocketClient.class);
  107. static final String HEADER_X_REAL_IP = "X-Real-IP";
  108. static final String HEADER_X_FORWARDED_FOR = "X-Forwarded-For";
  109. static final String SIP_SUBPROTOCOL = "sip";
  110. private final StandardWebSocketClient webSocketClient = new StandardWebSocketClient();
  111. public WebSocketSession connect(URI uri, WebSocketHandler handler, String clientIp,
  112. long timeoutMs, SoftPhoneProxyChannel channel) throws Exception {
  113. String normalizedIp = normalizeClientIp(clientIp);
  114. WebSocketHttpHeaders headers = buildUpstreamHeaders(normalizedIp, channel);
  115. log.info("\u8f6f\u7535\u8bdd WS \u8fde\u63a5\u4e0a\u6e38: host={}, port={}, channel={}, subprotocol={}, {}={}, {}={}",
  116. uri.getHost(),
  117. uri.getPort() > 0 ? uri.getPort() : "default",
  118. channel,
  119. headers.getSecWebSocketProtocol(),
  120. HEADER_X_REAL_IP,
  121. headers.getFirst(HEADER_X_REAL_IP),
  122. HEADER_X_FORWARDED_FOR,
  123. headers.getFirst(HEADER_X_FORWARDED_FOR));
  124. if (StringUtils.isBlank(normalizedIp)) {
  125. 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());
  126. }
  127. return webSocketClient.doHandshake(handler, headers, uri).get(timeoutMs, TimeUnit.MILLISECONDS);
  128. }
  129. static WebSocketHttpHeaders buildUpstreamHeaders(String clientIp, SoftPhoneProxyChannel channel) {
  130. WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
  131. if (StringUtils.isNotBlank(clientIp) && !"unknown".equalsIgnoreCase(clientIp.trim())) {
  132. String ip = clientIp.trim();
  133. headers.set(HEADER_X_REAL_IP, ip);
  134. headers.set(HEADER_X_FORWARDED_FOR, ip);
  135. }
  136. if (SoftPhoneProxyChannel.JS_SIP == channel) {
  137. headers.setSecWebSocketProtocol(Collections.singletonList(SIP_SUBPROTOCOL));
  138. }
  139. return headers;
  140. }
  141. static String normalizeClientIp(String clientIp) {
  142. if (StringUtils.isBlank(clientIp) || "unknown".equalsIgnoreCase(clientIp.trim())) {
  143. return null;
  144. }
  145. return clientIp.trim();
  146. }
  147. }
  148. `,
  149. 'CommSoftPhoneWsTicket.java': `package com.fs.comm.softphone;
  150. import lombok.Data;
  151. import java.io.Serializable;
  152. import java.util.HashSet;
  153. import java.util.Set;
  154. /**
  155. * \u8f6f\u7535\u8bdd WebSocket \u7f51\u5173\u5c42\u77ed\u65f6\u7968\u636e\uff08\u5b58 Redis\uff0cIPCC / JsSIP \u5404\u6d88\u8d39\u4e00\u6b21\uff09\u3002
  156. */
  157. @Data
  158. public class CommSoftPhoneWsTicket implements Serializable {
  159. private static final long serialVersionUID = 1L;
  160. private String ticketId;
  161. private Long tenantId;
  162. private Long companyId;
  163. private Long companyUserId;
  164. /** \u7ed1\u5b9a\u7684 EasyCall loginToken\uff08\u4ec5\u7f51\u5173\u5185\u90e8\u4f7f\u7528\uff0c\u900f\u4f20 IPCC\uff0c\u4e0d\u4e0b\u53d1\u524d\u7aef\uff09 */
  165. private String loginToken;
  166. /** \u5206\u673a\u53f7\uff08\u53ef\u9009\uff0c\u4fbf\u4e8e\u5ba1\u8ba1\uff09 */
  167. private String extNum;
  168. /** \u5750\u5e2d\u8bbf\u95ee IP\uff08getToolbarBasicParam HTTP \u8bf7\u6c42\u65f6\u91c7\u96c6\uff0c\u4f9b WS \u63e1\u624b\u900f\u4f20 SIP \u767d\u540d\u5355\uff09 */
  169. private String clientIp;
  170. private Long createTime;
  171. /** \u5df2\u63e1\u624b\u7684\u4ee3\u7406\u901a\u9053\uff08IPCC / JS_SIP \u5404\u5141\u8bb8\u4e00\u6b21\uff09 */
  172. private Set<String> usedChannels = new HashSet<>();
  173. }
  174. `,
  175. };
  176. function writeUtf8(path, content) {
  177. const normalized = content.replace(/\r\n/g, '\n');
  178. writeFileSync(path, normalized, { encoding: 'utf8' });
  179. }
  180. for (const [name, content] of Object.entries(files)) {
  181. const path = join(softphoneDir, name);
  182. writeUtf8(path, content);
  183. console.log('fixed', name);
  184. }
  185. function listJavaFiles(dir) {
  186. const result = [];
  187. for (const entry of readdirSync(dir)) {
  188. const full = join(dir, entry);
  189. if (statSync(full).isDirectory()) {
  190. result.push(...listJavaFiles(full));
  191. } else if (entry.endsWith('.java') && !Object.prototype.hasOwnProperty.call(files, entry)) {
  192. result.push(full);
  193. }
  194. }
  195. return result;
  196. }
  197. for (const path of listJavaFiles(softphoneDir)) {
  198. const raw = readFileSync(path);
  199. const bom = raw.length >= 3 && raw[0] === 0xef && raw[1] === 0xbb && raw[2] === 0xbf;
  200. let text = raw.toString('utf8');
  201. if (bom) {
  202. text = raw.slice(3).toString('utf8');
  203. }
  204. const normalized = text.replace(/\r\n/g, '\n');
  205. if (bom || text !== normalized || raw.toString('utf8') !== normalized) {
  206. writeUtf8(path, normalized);
  207. console.log('normalized', path.split(/[/\\]/).pop());
  208. }
  209. }
  210. console.log('done');