From f18da0345d86db9722dbcf813889f104cfb50578 Mon Sep 17 00:00:00 2001
From: Michael Calmer <michael.calmer@suse.com>
Date: Thu, 5 Dec 2024 10:00:35 +0100
Subject: [PATCH] for new CDN check different hosts and tokens

---
 .../domain/scc/SCCRepositoryCloudRmtAuth.java |  5 ++-
 .../manager/content/ContentSyncManager.java   | 19 ++++-----
 .../rhn/manager/content/MgrSyncUtils.java     | 39 +++++++++++++++----
 .../task/payg/PaygAuthDataProcessor.java      |  8 ++--
 ...lk-java.changes.mcalmer.cdn-change-project |  2 +
 5 files changed, 50 insertions(+), 23 deletions(-)
 create mode 100644 java/spacewalk-java.changes.mcalmer.cdn-change-project

diff --git a/java/code/src/com/redhat/rhn/domain/scc/SCCRepositoryCloudRmtAuth.java b/java/code/src/com/redhat/rhn/domain/scc/SCCRepositoryCloudRmtAuth.java
index 708e2ec3144..6ad8632bff4 100644
--- a/java/code/src/com/redhat/rhn/domain/scc/SCCRepositoryCloudRmtAuth.java
+++ b/java/code/src/com/redhat/rhn/domain/scc/SCCRepositoryCloudRmtAuth.java
@@ -58,14 +58,15 @@ public SCCRepositoryCloudRmtAuth() { }
     public String getUrl() {
         try {
             URI url = new URI(getRepo().getUrl());
-            if (!url.getHost().startsWith(MgrSyncUtils.OFFICIAL_SUSE_UPDATE_HOST)) {
+            if (!url.getHost().endsWith(MgrSyncUtils.OFFICIAL_UPDATE_HOST_DOMAIN)) {
                 /*
                 SCC data contain repositories which point to external server and are free to access.
                 Examples are openSUSE and nVidia repositories. These repos are not available on the RMT servers
-                and the URLs shoudl not be re-written. Creating them at type {@link SCCRepositoryNoAuth}
+                and the URLs should not be re-written. Creating them at type {@link SCCRepositoryNoAuth}
                 requires to get the json definition of the repo somehow as input into
                 {@link ContentSyncManager#refreshRepositoriesAuthentication}. Otherwise the repo will be removed again.
                 Just returning the original URL here is better to understand.
+                We expect that all repositories which have the domain .suse.com are available on the RMT servers.
                 */
                 return getRepo().getUrl();
             }
diff --git a/java/code/src/com/redhat/rhn/manager/content/ContentSyncManager.java b/java/code/src/com/redhat/rhn/manager/content/ContentSyncManager.java
index 9410beb99f8..25fdd6050f8 100644
--- a/java/code/src/com/redhat/rhn/manager/content/ContentSyncManager.java
+++ b/java/code/src/com/redhat/rhn/manager/content/ContentSyncManager.java
@@ -117,8 +117,6 @@
 import java.util.Set;
 import java.util.function.Function;
 import java.util.function.Supplier;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -826,8 +824,6 @@ private Optional<SCCRepositoryAuth> determineAuth(SCCRepositoryJson jrepo, SCCRe
             .map(local -> MgrSyncUtils.urlToFSPath(jrepo.getUrl(), repo.getName(), local.getPath()).toString())
             .orElse(jrepo.getUrl());
 
-
-
         Optional<String> tokenOpt = getTokenFromURL(url);
 
         //SCC
@@ -2256,12 +2252,17 @@ public List<MgrSyncChannelDto> listChannels() {
         )).collect(Collectors.toList());
     }
 
-    private Optional<String> getTokenFromURL(String url) {
+    protected Optional<String> getTokenFromURL(String url) {
         Optional<String> token = Optional.empty();
-        Pattern p = Pattern.compile("/?\\?([^?&=]+)$");
-        Matcher m = p.matcher(url);
-        if (m.find()) {
-            token = Optional.of(m.group(1));
+        try {
+            URI uri = new URI(url);
+            token = Arrays.stream(Optional.ofNullable(uri.getQuery()).orElse("").split("&"))
+                    .filter(p -> !p.isEmpty())
+                    .filter(MgrSyncUtils::isAuthToken)
+                    .findFirst();
+        }
+        catch (URISyntaxException e) {
+            LOG.warn("Unable to parse URL: {}", url);
         }
         return token;
     }
diff --git a/java/code/src/com/redhat/rhn/manager/content/MgrSyncUtils.java b/java/code/src/com/redhat/rhn/manager/content/MgrSyncUtils.java
index e1974aba926..57d90d6ecf1 100644
--- a/java/code/src/com/redhat/rhn/manager/content/MgrSyncUtils.java
+++ b/java/code/src/com/redhat/rhn/manager/content/MgrSyncUtils.java
@@ -52,9 +52,7 @@ public class MgrSyncUtils {
 
     // Source URL handling
     private static final String OFFICIAL_NOVELL_UPDATE_HOST = "nu.novell.com";
-    public static final String OFFICIAL_SUSE_UPDATE_HOST = "updates.suse.com";
-    private static final List<String> OFFICIAL_UPDATE_HOSTS =
-            Arrays.asList(OFFICIAL_SUSE_UPDATE_HOST, OFFICIAL_NOVELL_UPDATE_HOST);
+    public static final String OFFICIAL_UPDATE_HOST_DOMAIN = ".suse.com";
     private static final List<String> PRODUCT_ARCHS = Arrays.asList("i386", "i486", "i586", "i686", "ia64", "ppc64le",
             "ppc64", "ppc", "s390x", "s390", "x86_64", "aarch64", "amd64");
 
@@ -215,12 +213,12 @@ public static URI urlToFSPath(String urlString, String name) {
 
     /**
      * Convert network URL to file system URL.
-     *
+     * <p>
      * 1. URL point to localhost, return the normal URL, we have access
      * 2. URL from updates.suse.com, return the path
      * 3. legacy SMT mirror URL /repo/RPMMD/&lt;repo name&gt; if it exists
      * 4. finally, return host + path as path component
-     *
+     * <p>
      * A mirrorlist URL with query paramater is converted to a path:
      * - key=value => key/value
      * - sort alphabetically
@@ -247,7 +245,8 @@ public static URI urlToFSPath(String urlString, String name, Path sccDataPath) {
                 return uri;
             }
             String qPath = Arrays.stream(Optional.ofNullable(uri.getQuery()).orElse("").split("&"))
-                    .filter(p -> p.contains("=")) // filter out possible auth tokens
+                    .filter(p -> !p.isEmpty())
+                    .filter(p -> !isAuthToken(p)) // filter out possible auth tokens
                     .map(p -> String.join(File.separator, p.split("=", 2)))
                     .sorted()
                     .collect(Collectors.joining(File.separator));
@@ -258,7 +257,7 @@ public static URI urlToFSPath(String urlString, String name, Path sccDataPath) {
         catch (URISyntaxException e) {
             LOG.warn("Unable to parse URL: {}", urlString);
         }
-        //TODO: check if this if is needed
+
         if (sccDataPath == null) {
             throw new ContentSyncException("No local mirror path configured");
         }
@@ -267,7 +266,7 @@ public static URI urlToFSPath(String urlString, String name, Path sccDataPath) {
         File mirrorPath = new File(dataPath.getAbsolutePath(), host + File.separator + path);
 
         // Case 2
-        if (OFFICIAL_UPDATE_HOSTS.contains(host)) {
+        if (host.endsWith(OFFICIAL_UPDATE_HOST_DOMAIN) || host.equals(OFFICIAL_NOVELL_UPDATE_HOST)) {
             mirrorPath = new File(dataPath.getAbsolutePath(), path);
             LOG.info("SCC mirrorpath: {}", mirrorPath);
         }
@@ -304,4 +303,28 @@ else if (name != null) {
         }
         return cleanPath.toUri().normalize();
     }
+
+    /**
+     * Check, if a given string is an authentication token. The given string must not contain '&' signs
+     * which are used to separate query parameters. The expected input is a single query paramater
+     * @param queryParam a single query parameter string to test
+     * @return true if this is likely an authentication token. Otherwise false
+     */
+    public static boolean isAuthToken(String queryParam) {
+        if (queryParam.isBlank()) {
+            LOG.debug("empty queryParam is not an auth token");
+            return false;
+        }
+        else if (queryParam.contains("&")) {
+            throw new ContentSyncException("token must not contain the ampersand sign");
+        }
+        //     Could be an JWT token
+        boolean ret = !queryParam.contains("=") ||
+                // Our CDN tokens use this key
+                queryParam.startsWith("__token__=") ||
+                // typical Akamai token values
+                (queryParam.contains("exp=") && queryParam.contains("hmac="));
+        LOG.debug("{} isAuthToken: {}", queryParam, ret);
+        return ret;
+    }
 }
diff --git a/java/code/src/com/redhat/rhn/taskomatic/task/payg/PaygAuthDataProcessor.java b/java/code/src/com/redhat/rhn/taskomatic/task/payg/PaygAuthDataProcessor.java
index 0b87e6e68aa..8cfaa05e258 100644
--- a/java/code/src/com/redhat/rhn/taskomatic/task/payg/PaygAuthDataProcessor.java
+++ b/java/code/src/com/redhat/rhn/taskomatic/task/payg/PaygAuthDataProcessor.java
@@ -34,6 +34,7 @@
 import com.redhat.rhn.domain.org.OrgFactory;
 import com.redhat.rhn.domain.product.Tuple2;
 import com.redhat.rhn.domain.scc.SCCRepositoryAuth;
+import com.redhat.rhn.manager.content.MgrSyncUtils;
 import com.redhat.rhn.taskomatic.task.payg.beans.PaygInstanceInfo;
 import com.redhat.rhn.taskomatic.task.payg.beans.PaygProductInfo;
 
@@ -206,10 +207,9 @@ else if (filename.endsWith(".crt")) {
 
     private String buildQueryString(String query, String newKey, String newValue) {
         Map<String, String> queryparams = Arrays.stream(
-                        Optional.ofNullable(query)
-                                .orElse("")
-                                .split("&"))
-                .filter(p -> p.contains("=")) // filter out possible auth tokens
+                        Optional.ofNullable(query).orElse("").split("&"))
+                .filter(p -> !p.isEmpty())
+                .filter(p -> !MgrSyncUtils.isAuthToken(p)) // filter out possible auth tokens
                 .map(p -> {
                     String[] s = p.split("=", 2);
                     return new Tuple2<String, String>(s[0], s[1]);
diff --git a/java/spacewalk-java.changes.mcalmer.cdn-change-project b/java/spacewalk-java.changes.mcalmer.cdn-change-project
new file mode 100644
index 00000000000..06355e1a9e7
--- /dev/null
+++ b/java/spacewalk-java.changes.mcalmer.cdn-change-project
@@ -0,0 +1,2 @@
+- Handle new kind of auth tokens
+- Support new official SUSE update hosts
