001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.q2.install; 020 021import java.io.File; 022import java.io.IOException; 023import java.net.JarURLConnection; 024import java.net.URI; 025import java.net.URISyntaxException; 026import java.net.URL; 027import java.security.MessageDigest; 028import java.security.NoSuchAlgorithmException; 029import java.util.*; 030import java.util.jar.JarEntry; 031import java.util.jar.JarFile; 032import java.util.stream.Collectors; 033 034/** 035 * Helpers that enumerate jPOS module metadata embedded under 036 * {@code META-INF/modules/} on the runtime classpath. 037 * 038 * @author vsalaman 039 */ 040public class ModuleUtils 041{ 042 /** Default constructor; no instance state to initialise. */ 043 public ModuleUtils() {} 044 private static final String MODULES_UUID_DIR = "META-INF/modules/uuids/"; 045 private static final String MODULES_RKEYS_DIR = "META-INF/modules/rkeys/"; 046 047 /** 048 * Enumerates every classpath resource located under {@code prefix}, traversing 049 * both directory- and JAR-based class-loader entries. 050 * 051 * @param prefix classpath prefix to scan (e.g. {@code "META-INF/modules/uuids/"}) 052 * @return resource paths relative to the classpath root 053 * @throws IOException if classpath enumeration fails 054 */ 055 public static List<String> getModuleEntries(String prefix) throws IOException { 056 List<String> result = new ArrayList<>(); 057 058 Enumeration<URL> urls = ModuleUtils.class.getClassLoader().getResources(prefix); 059 while (urls.hasMoreElements()) { 060 URL url = urls.nextElement(); 061 if (url == null) continue; 062 063 try { 064 List<String> entries; 065 String protocol = url.getProtocol(); 066 if ("jar".equals(protocol)) { 067 entries = resolveModuleEntriesFromJar(url, prefix); 068 } else if ("file".equals(protocol)) { 069 entries = resolveModuleEntriesFromFiles(url, prefix); 070 } else { 071 // Unsupported protocol, skip with optional logging 072 continue; 073 } 074 result.addAll(entries); 075 } catch (URISyntaxException e) { 076 throw new IOException("Bad URL: " + url, e); 077 } 078 } 079 return result; 080 } 081 082 /** 083 * Returns the sorted list of module UUIDs discovered under 084 * {@code META-INF/modules/uuids/}. 085 * 086 * @return module UUIDs (one per registered module) 087 * @throws IOException if classpath enumeration fails 088 */ 089 public static List<String> getModulesUUIDs() throws IOException { 090 return getModuleEntries(MODULES_UUID_DIR).stream() 091 .sorted() 092 .map(p -> p.substring(MODULES_UUID_DIR.length())) 093 .collect(Collectors.toList()); 094 } 095 096 /** 097 * Returns the sorted list of revocation-key identifiers discovered under 098 * {@code META-INF/modules/rkeys/}. 099 * 100 * @return revocation key identifiers 101 * @throws IOException if classpath enumeration fails 102 */ 103 public static List<String> getRKeys () throws IOException { 104 return ModuleUtils.getModuleEntries(MODULES_RKEYS_DIR) 105 .stream() 106 .sorted() 107 .map(p -> p.substring(MODULES_RKEYS_DIR.length())) 108 .collect(Collectors.toList()); 109 } 110 111 /** 112 * Returns the Base64-encoded SHA-256 hash of the concatenated module UUIDs, 113 * used as a per-installation fingerprint by license verification. 114 * 115 * @return Base64 SHA-256 hash, or empty string when no UUIDs are registered 116 * @throws IOException if classpath enumeration fails 117 * @throws NoSuchAlgorithmException if SHA-256 is unavailable 118 */ 119 public static String getSystemHash() throws IOException, NoSuchAlgorithmException { 120 MessageDigest digest = MessageDigest.getInstance("SHA-256"); 121 List<String> uuids = getModulesUUIDs(); 122 if (uuids.isEmpty()) return ""; 123 124 uuids.forEach(uuid -> digest.update(uuid.getBytes())); 125 return Base64.getEncoder().encodeToString(digest.digest()); 126 } 127 128 private static List<String> resolveModuleEntriesFromFiles(URL url, String prefix) 129 throws URISyntaxException { 130 String normalizedPrefix = prefix.endsWith("/") ? prefix : prefix + "/"; 131 List<String> resourceList = new ArrayList<>(); 132 File dir = new File(url.toURI()); 133 addFiles(dir, normalizedPrefix, resourceList); 134 return resourceList; 135 } 136 137 private static void addFiles(File dir, String prefix, List<String> resourceList) { 138 File[] files = dir.listFiles(); 139 if (files == null) return; 140 141 for (File file : files) { 142 if (file.isDirectory()) { 143 addFiles(file, prefix + file.getName() + "/", resourceList); 144 } else { 145 resourceList.add(prefix + file.getName()); 146 } 147 } 148 } 149 150 private static List<String> resolveModuleEntriesFromJar(URL url, String prefix) 151 throws IOException { 152 String normalizedPrefix = prefix.endsWith("/") ? prefix : prefix + "/"; 153 List<String> resourceList = new ArrayList<>(); 154 155 JarURLConnection conn = (JarURLConnection) url.openConnection(); 156 try (JarFile jarFile = conn.getJarFile()) { 157 Enumeration<JarEntry> entries = jarFile.entries(); 158 while (entries.hasMoreElements()) { 159 JarEntry entry = entries.nextElement(); 160 String name = entry.getName(); 161 if (name.startsWith(normalizedPrefix) && !entry.isDirectory()) { 162 resourceList.add(name); 163 } 164 } 165 } 166 return resourceList; 167 } 168}