diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 840afc1..8bc0482 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,12 @@ commons-compress 1.25.0 + + + org.apache.httpcomponents + httpclient + 4.5.14 + diff --git a/src/main/java/lion/Config/Config.java b/src/main/java/lion/Config/Config.java new file mode 100644 index 0000000..8462194 --- /dev/null +++ b/src/main/java/lion/Config/Config.java @@ -0,0 +1,23 @@ +package lion.Config; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public class Config { + public static String DouNaiV2ray; + public static String DouNaiClash; + + public static void loadConfig(){ + Properties prop = new Properties(); + + try (InputStream input = new FileInputStream("/root/gallery/storageNode/config.properties")) { + prop.load(input); + DouNaiV2ray = prop.getProperty("DouNaiV2ray"); + DouNaiClash = prop.getProperty("DouNaiClash"); + } catch (IOException ex) { + ex.printStackTrace(); + } + } +} diff --git a/src/main/java/lion/Externel/BackupSubServer.java b/src/main/java/lion/Externel/BackupSubServer.java new file mode 100644 index 0000000..71b5fb9 --- /dev/null +++ b/src/main/java/lion/Externel/BackupSubServer.java @@ -0,0 +1,295 @@ +package lion.Externel; + +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; + +import java.io.*; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static lion.Config.Config.DouNaiClash; +import static lion.Config.Config.DouNaiV2ray; + +@Slf4j +public class BackupSubServer { + + public static void main(String[] args) { + updateSub(); + ExecutorService threadPool = Executors.newFixedThreadPool(3600); + threadPool.submit(() -> { + if(LocalDateTime.now().getHour() == 0){ + updateSub(); + } + }); + + String ip = ""; + try(ServerSocket serverSocket = new ServerSocket(8889)) { + log.info("Sub Server listening on port {}", 8889); + while (true) { + Socket clientSocket = serverSocket.accept(); + ip = clientSocket.getInetAddress().getHostAddress(); + log.info("Client connected:{}", ip); + // 线程池处理下载请求 + handleClientRequest(clientSocket); + } + } catch (IOException e) { + log.error("处理http请求时出错,IP:{},ERROR:{}", ip, e.getMessage()); + } + } + + public static void updateSub(){ + File DouNaiClashFile = new File("sub/DouNaiClash.txt"); + File DouNaiV2rayFile = new File("sub/DouNaiV2ray.txt"); + File directory = new File("sub"); + + if(!directory.isDirectory()) + try { + Files.createDirectory(Paths.get("sub")); + } catch (IOException e) { + log.error("create directory error:{}", e.getMessage()); + } + + List DouNaiClash_profile; + + //下载豆奶v2ray订阅 + try(FileWriter writer = new FileWriter(DouNaiV2rayFile)) { + String DouNaiV2rayRaw = Get(DouNaiV2ray).getFirst(); + String[] v2rayPlain = new String(Base64.getDecoder().decode(DouNaiV2rayRaw)).split("\n"); + StringBuilder stringBuilder = new StringBuilder(); + Pattern pattern = Pattern.compile("-?\\d+(\\.\\d+)?"); + + //过滤高倍率节点 + for(String node: v2rayPlain){ + String name = URLDecoder.decode(node.split("#")[1], StandardCharsets.UTF_8); + if(name.contains("流量")){ + Matcher matcher = pattern.matcher(name.substring(name.indexOf("(") + 1, name.indexOf(")"))); + + if (matcher.find()) { + // 将匹配到的数字添加到列表中 + float ratio = Float.parseFloat(matcher.group()); + if(ratio <= 2) { + stringBuilder.append(node).append("\n"); + continue; + } + } + stringBuilder.append(node).append("\n"); + } + else{ + stringBuilder.append(node).append("\n"); + } + } + writer.write(new String(Base64.getEncoder().encode(stringBuilder.toString().getBytes(StandardCharsets.UTF_8)))); + + log.info("load DouNai v2ray complete"); + }catch (IOException e){ + log.error("load DouNai v2ray failure: {}", e.getMessage()); + } + + //下载豆奶clash订阅 + try(FileWriter writer = new FileWriter(DouNaiClashFile)) { + DouNaiClash_profile = Get(DouNaiClash); + //过滤高倍率节点 + ArrayList clashProcessed = new ArrayList<>(); + boolean isProxies = false; + boolean skip = false; + for(String line: DouNaiClash_profile){ + if(line.equals("proxies:")) + isProxies = true; + else if(line.equals("proxy-groups:") && isProxies) + isProxies = false; + + if(isProxies) { + if (line.contains("name")) + skip = line.contains("流量"); + if (!skip) + clashProcessed.add(line); + } + else + if (!line.contains("流量")) + clashProcessed.add(line); + } + + for(String line: clashProcessed) + writer.write(line + "\n"); + + log.info("load DouNai clash complete"); + }catch (IOException e){ + log.error("load DouNai clash failure: {}", e.getMessage()); + } + } + + + public static ArrayList Get(String url) throws IOException { + CloseableHttpClient httpClient = HttpClients.createDefault(); + CloseableHttpResponse httpResponse; + HttpGet httpGet = new HttpGet(url); + + httpResponse = httpClient.execute(httpGet); + + HttpEntity responseEntity = httpResponse.getEntity(); + int statusCode = httpResponse.getStatusLine().getStatusCode(); + ArrayList temp = new ArrayList<>(); + + if (statusCode == 200) { + BufferedReader reader = new BufferedReader(new InputStreamReader(responseEntity.getContent())); + String str; + while ((str = reader.readLine()) != null) + temp.add(str); + } + + httpClient.close(); + httpResponse.close(); + return temp; + } + + private static void handleClientRequest(Socket clientSocket) { + String fileName = ""; + try { + BufferedReader requestReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); + String requestLine = requestReader.readLine(); + + // Parse the request line to get the method and path + String[] requestParts = requestLine.split(" "); + String method = requestParts[0]; + Map paramMap = parseRequestLine(requestParts[1]);//path + if(paramMap == null){ + sendErrorResponse(clientSocket, "404"); + return; + } + log.info(Arrays.toString(requestParts)); + + // Only handle GET requests + if (method.equals("GET")) { + // Set the file path for download + File file = new File(fileName); + switch (paramMap.get("Client")) { + case "v2" -> file = new File("sub/DouNaiV2ray.txt"); + case "cat" -> file = new File("sub/DouNaiClash.txt"); + } + fileName = file.getName(); + log.info(file.getAbsolutePath()); + // Check if the file exists and is readable + if (file.exists() && file.isFile() && file.canRead()) { + // Get the file length + long fileLength = file.length(); + + // Get the range information for resuming download + long startByte = 0; + long endByte = fileLength - 1; + String rangeHeader = getRequestHeader(requestReader); + if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { + String[] rangeValues = rangeHeader.substring(6).split("-"); + startByte = Long.parseLong(rangeValues[0]); + if (rangeValues.length > 1 && !rangeValues[1].isEmpty()) { + endByte = Long.parseLong(rangeValues[1]); + } + } + + // Send the HTTP response headers + OutputStream responseStream = clientSocket.getOutputStream(); + PrintWriter responseWriter = new PrintWriter(responseStream, true); + responseWriter.println("HTTP/1.1 206 Partial Content"); + responseWriter.println("Content-Type: application/octet-stream"); + responseWriter.println("Accept-Ranges: bytes"); + responseWriter.println("Content-Length: " + (endByte - startByte + 1)); + responseWriter.println("Content-Range: bytes " + startByte + "-" + endByte + "/" + fileLength); + responseWriter.println(); + + // Send the file content + try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) { + randomAccessFile.seek(startByte); + byte[] buffer = new byte[1024]; + int bytesRead; + long bytesRemaining = endByte - startByte + 1; + while (bytesRemaining > 0 && (bytesRead = randomAccessFile.read(buffer, 0, (int) Math.min(buffer.length, bytesRemaining))) != -1) { + responseStream.write(buffer, 0, bytesRead); + bytesRemaining -= bytesRead; + } + }catch (SocketException ignore){ + + } + + // Close the response output stream + responseStream.close(); + } else { + // File not found or not readable, send 404 response + sendErrorResponse(clientSocket, "404 Not Found"); + } + } else { + // Non-GET requests, send 501 response + sendErrorResponse(clientSocket, "501 Not Implemented"); + } + + // Close the request reader and client socket + requestReader.close(); + clientSocket.close(); + } catch (IOException e) { + log.error("处理文件下载时出错,IP:{}, 文件:{}, ERROR:{}", clientSocket.getInetAddress().getHostAddress(), fileName, e.getMessage()); + } + } + + private static String getRequestHeader(BufferedReader requestReader) throws IOException { + String line; + while ((line = requestReader.readLine()) != null) { + if (line.trim().isEmpty()) { + break; + } + + if (line.startsWith("Range" + ":")) { + return line.substring("Range".length() + 1).trim(); + } + } + return null; + } + + public static Map parseRequestLine(String requestLine) { + Map pathParams = new HashMap<>(); + + if(requestLine == null) + return null; + + String path; + if(requestLine.contains("?")) + path = requestLine.split("\\?")[0]; + else + path = requestLine; + + String[] vars = path.split("/"); + + if(vars.length < 4) + return null; + + pathParams.put("Key", vars[3]); + pathParams.put("Client", vars[2]); + + if(!pathParams.get("Client").equals("cat") && !pathParams.get("Client").equals("v2")) + return null; + + if(pathParams.get("Key").length()<6) + return null; + + return pathParams; + } + + private static void sendErrorResponse(Socket clientSocket, String statusCode) throws IOException { + OutputStream responseStream = clientSocket.getOutputStream(); + PrintWriter responseWriter = new PrintWriter(responseStream, true); + responseWriter.println("HTTP/1.1 " + statusCode); + responseWriter.println("Content-Type: text/html"); + responseWriter.println(); + responseWriter.println("

" + statusCode + "

"); + responseStream.close(); + } +} diff --git a/src/main/java/lion/Extranel/AESUtils.java b/src/main/java/lion/Extranel/AESUtils.java deleted file mode 100644 index 8797821..0000000 --- a/src/main/java/lion/Extranel/AESUtils.java +++ /dev/null @@ -1,26 +0,0 @@ -package lion.Extranel; - -import javax.crypto.Cipher; -import javax.crypto.spec.SecretKeySpec; -import java.security.Key; - -public class AESUtils { - private static final String ALGORITHM = "AES"; - private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding"; - - private static final byte[] keyBytes = "ThisIsA128BitKey".getBytes(); - - public static byte[] encrypt(byte[] data) throws Exception { - Key key = new SecretKeySpec(keyBytes, ALGORITHM); - Cipher cipher = Cipher.getInstance(TRANSFORMATION); - cipher.init(Cipher.ENCRYPT_MODE, key); - return cipher.doFinal(data); - } - - public static byte[] decrypt(byte[] encryptedData) throws Exception { - Key key = new SecretKeySpec(keyBytes, ALGORITHM); - Cipher cipher = Cipher.getInstance(TRANSFORMATION); - cipher.init(Cipher.DECRYPT_MODE, key); - return cipher.doFinal(encryptedData); - } -} diff --git a/src/main/java/lion/Extranel/Server.java b/src/main/java/lion/Extranel/Server.java deleted file mode 100644 index 770b0c4..0000000 --- a/src/main/java/lion/Extranel/Server.java +++ /dev/null @@ -1,65 +0,0 @@ -package lion.Extranel; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lion.CustomUtil; -import lombok.extern.slf4j.Slf4j; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.URI; -import java.net.URL; -import java.util.HashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -@Slf4j -public class Server { - - ExecutorService thread_pool; - - ObjectMapper objectMapper; - - public static void main(String[] args) { - new Server(); - } - - public Server(){ - log.info("开始监听:55555"); - objectMapper = CustomUtil.objectMapper; - thread_pool = Executors.newFixedThreadPool(2); - thread_pool.submit(() -> { - try(ServerSocket server = new ServerSocket(55555)){ - server.setSoTimeout(1000000); - while (true){ - Socket socket = server.accept(); - thread_pool.submit(() -> handleSocket(socket)); - } - }catch (IOException e){ - log.error(e.getMessage()); - } - }); - } - - - public void handleSocket(Socket socket){ - try{ - BufferedInputStream inputStream = new BufferedInputStream(socket.getInputStream()); - Thread.sleep(500); - byte[] buf = new byte[inputStream.available()]; - inputStream.read(buf); - byte[] bytes = AESUtils.decrypt(buf); - - HashMap map = (HashMap) objectMapper.readValue(bytes, HashMap.class); - URL url = new URI(map.get("path")).toURL(); - log.info("处理反代 ip: {},路径: {}", socket.getInetAddress().getHostAddress(), map.get("path")); - bytes = url.openConnection().getInputStream().readAllBytes(); - socket.getOutputStream().write(AESUtils.encrypt(bytes)); - socket.getOutputStream().flush(); - socket.getOutputStream().close(); - } catch (Exception e) { - e.printStackTrace(); - } - } -} diff --git a/src/main/java/lion/Main.java b/src/main/java/lion/Main.java index af58463..235ab32 100644 --- a/src/main/java/lion/Main.java +++ b/src/main/java/lion/Main.java @@ -1,7 +1,8 @@ package lion; import io.netty.bootstrap.Bootstrap; -import lion.Extranel.Server; +import lion.Config.Config; +import lion.Externel.BackupSubServer; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -9,8 +10,9 @@ public class Main { public static void main(String[] args) { boot(); + Config.loadConfig(); + new Thread(() -> BackupSubServer.main(null)).start(); new Thread(() -> MultiThreadedHTTPServer.main(null)).start(); - new Thread(() -> Server.main(null)).start(); new storageNode(); } diff --git a/src/main/resources/config.properties b/src/main/resources/config.properties new file mode 100644 index 0000000..427b6b4 --- /dev/null +++ b/src/main/resources/config.properties @@ -0,0 +1,2 @@ +DouNaiV2ray=https://aaaa.gay/link/X7zEqkIx5gtIGugO?client=v2 +DouNaiClash=https://aaaa.gay/link/X7zEqkIx5gtIGugO?client=clashmeta \ No newline at end of file