1- package common ;
2-
1+ package common ;
2+
33import org .jetbrains .annotations .NotNull ;
44
55import java .io .*;
1414import java .util .Objects ;
1515import java .util .Optional ;
1616import java .util .logging .Logger ;
17+ import java .util .logging .Level ;
1718import java .util .zip .ZipEntry ;
1819import java .util .zip .ZipException ;
1920import java .util .zip .ZipFile ;
3233import java .nio .file .SimpleFileVisitor ;
3334import java .nio .file .attribute .BasicFileAttributes ;
3435import java .nio .file .FileVisitResult ;
35-
36+
3637public class PluginDownloader {
3738
3839 private final Logger logger ;
3940 private static java .util .Map <String , String > extraHeaders = new java .util .HashMap <>();
4041 private static String overrideUserAgent = null ;
41-
42+
4243 public PluginDownloader (Logger logger ) {
4344 this .logger = logger ;
4445 }
@@ -47,7 +48,7 @@ public static void setHttpHeaders(java.util.Map<String, String> headers, String
4748 extraHeaders = headers != null ? new java .util .HashMap <>(headers ) : new java .util .HashMap <>();
4849 overrideUserAgent = (userAgent != null && !userAgent .trim ().isEmpty ()) ? userAgent .trim () : null ;
4950 }
50-
51+
5152 public boolean downloadPlugin (String link , String fileName , String githubToken ) throws IOException {
5253 boolean requiresAuth = link .toLowerCase ().contains ("actions" )
5354 && link .toLowerCase ().contains ("github" )
@@ -66,17 +67,35 @@ public boolean downloadPlugin(String link, String fileName, String githubToken)
6667 return false ;
6768 }
6869
69- for (int attempt = 1 ; attempt <= 2 ; attempt ++) {
70+ for (int attempt = 1 ; attempt <= Math . max ( 2 , common . UpdateOptions . maxRetries ) ; attempt ++) {
7071 File rawTmp = new File (rawTempPath );
7172 File outTmp = new File (outputTempPath );
7273 cleanupQuietly (rawTmp );
7374 cleanupQuietly (outTmp );
7475 try {
7576 HttpURLConnection connection = openConnection (link , githubToken , requiresAuth );
76- if (!downloadWithVerification (rawTmp , connection )) {
77- logger .warning ("Download failed (attempt " + attempt + ")" );
77+ int code = 0 ;
78+ try { code = connection .getResponseCode (); } catch (IOException ignored ) {}
79+ if (code == 403 || code == 429 || (code >= 500 && code < 600 )) {
80+ int base = Math .max (0 , common .UpdateOptions .backoffBaseMs );
81+ int max = Math .max (base , common .UpdateOptions .backoffMaxMs );
82+ int delay = Math .min (max , base * (1 << Math .min (attempt , 10 ))) + new java .util .Random ().nextInt (250 );
83+ if (common .UpdateOptions .debug ) logger .info ("[DEBUG] HTTP " + code + " for " + link + ", retry in ~" + delay + "ms (attempt " + attempt + ")" );
84+ try { Thread .sleep (delay ); } catch (InterruptedException ignored2 ) { Thread .currentThread ().interrupt (); }
7885 continue ;
7986 }
87+ if (!downloadWithVerification (rawTmp , connection )) {
88+ logger .warning ("Download failed (attempt " + attempt + ") — retrying lenient mode (old-plugin behavior)" );
89+ try {
90+ connection = openConnection (link , githubToken , requiresAuth );
91+ if (!downloadLenient (rawTmp , connection )) {
92+ continue ;
93+ }
94+ } catch (IOException ex ) {
95+ continue ;
96+ }
97+ }
98+
8099
81100 if (isZipFile (rawTempPath )) {
82101 boolean extracted = extractFirstJarFromZip (rawTempPath , outputTempPath );
@@ -98,6 +117,7 @@ public boolean downloadPlugin(String link, String fileName, String githubToken)
98117 }
99118
100119 File target = new File (outputFilePath );
120+ if (common .UpdateOptions .debug ) logger .info ("[DEBUG] Ready to install: temp=" + outTmp .getAbsolutePath () + " -> target=" + target .getAbsolutePath ());
101121 if (common .UpdateOptions .ignoreDuplicates && target .exists () && sameDigest (target , outTmp , "MD5" )) {
102122 cleanupQuietly (outTmp );
103123 cleanupQuietly (rawTmp );
@@ -115,10 +135,10 @@ public boolean downloadPlugin(String link, String fileName, String githubToken)
115135 }
116136 return false ;
117137 }
118-
119-
120-
121-
138+
139+
140+
141+
122142 private String getString (String fileName ) {
123143 String basePlugins = "plugins/" ;
124144 String configuredFilePath = common .UpdateOptions .filePath ;
@@ -153,39 +173,12 @@ private String ensureDir(String dir) {
153173 new File (dir ).mkdirs ();
154174 return dir ;
155175 }
156-
176+
157177 private boolean downloadPluginToFile (String outputFilePath , HttpURLConnection connection ) throws IOException {
158178 return downloadWithVerification (new File (outputFilePath ), connection );
159179 }
160180
161- private boolean downloadWithVerification (File outFile , HttpURLConnection connection ) throws IOException {
162- long expected = connection .getContentLengthLong ();
163- long written = 0L ;
164- try (InputStream in = connection .getInputStream (); FileOutputStream out = new FileOutputStream (outFile )) {
165- byte [] buffer = new byte [8192 ];
166- int bytesRead ;
167- while ((bytesRead = in .read (buffer )) != -1 ) {
168- out .write (buffer , 0 , bytesRead );
169- written += bytesRead ;
170- }
171- out .getFD ().sync ();
172- }
173- if (expected >= 0 && written != expected ) {
174- logger .warning ("Content-Length mismatch: expected=" + expected + ", got=" + written );
175- cleanupQuietly (outFile );
176- return false ;
177- }
178- if (isZipFile (outFile .getPath ())) {
179- try (ZipFile zf = new ZipFile (outFile )) {
180- } catch (IOException ex ) {
181- logger .warning ("Downloaded ZIP appears corrupt: " + ex .getMessage ());
182- cleanupQuietly (outFile );
183- return false ;
184- }
185- }
186- return true ;
187- }
188-
181+
189182 private boolean extractFirstJarFromZip (String zipFilePath , String outputFilePath ) throws IOException {
190183 try (ZipFile zipFile = new ZipFile (zipFilePath )) {
191184 List <? extends ZipEntry > entries = Collections .list (zipFile .entries ());
@@ -213,7 +206,7 @@ private boolean extractFirstJarFromZip(String zipFilePath, String outputFilePath
213206 return true ;
214207 }
215208 }
216-
209+
217210 public boolean downloadJenkinsPlugin (String link , String fileName ){
218211 String tempBase = common .UpdateOptions .tempPath != null && !common .UpdateOptions .tempPath .isEmpty () ? ensureDir (common .UpdateOptions .tempPath ) : "plugins/" ;
219212 String rawTempPath = tempBase + fileName + ".download.tmp" ;
@@ -228,13 +221,23 @@ public boolean downloadJenkinsPlugin(String link, String fileName){
228221 return false ;
229222 }
230223
231- for (int attempt = 1 ; attempt <= 2 ; attempt ++) {
224+ for (int attempt = 1 ; attempt <= Math . max ( 2 , common . UpdateOptions . maxRetries ) ; attempt ++) {
232225 File rawTmp = new File (rawTempPath );
233226 File outTmp = new File (outputTempPath );
234227 cleanupQuietly (rawTmp );
235228 cleanupQuietly (outTmp );
236229 try {
237230 HttpURLConnection connection = openConnection (link , null , false );
231+ int code = 0 ;
232+ try { code = connection .getResponseCode (); } catch (IOException ignored ) {}
233+ if (code == 403 || code == 429 || (code >= 500 && code < 600 )) {
234+ int base = Math .max (0 , common .UpdateOptions .backoffBaseMs );
235+ int max = Math .max (base , common .UpdateOptions .backoffMaxMs );
236+ int delay = Math .min (max , base * (1 << Math .min (attempt , 10 ))) + new java .util .Random ().nextInt (250 );
237+ if (common .UpdateOptions .debug ) logger .info ("[DEBUG] HTTP " + code + " for " + link + ", retry in ~" + delay + "ms (attempt " + attempt + ")" );
238+ try { Thread .sleep (delay ); } catch (InterruptedException ignored2 ) { Thread .currentThread ().interrupt (); }
239+ continue ;
240+ }
238241 if (!downloadWithVerification (rawTmp , connection )) {
239242 logger .info ("Download failed (attempt " + attempt + ")" );
240243 continue ;
@@ -256,6 +259,7 @@ public boolean downloadJenkinsPlugin(String link, String fileName){
256259 continue ;
257260 }
258261 File target = new File (outputFilePath );
262+ if (common .UpdateOptions .debug ) logger .info ("[DEBUG] Ready to install: temp=" + outTmp .getAbsolutePath () + " -> target=" + target .getAbsolutePath ());
259263 if (target .exists () && sameDigest (target , outTmp , "MD5" )) {
260264 cleanupQuietly (outTmp );
261265 cleanupQuietly (rawTmp );
@@ -273,8 +277,8 @@ public boolean downloadJenkinsPlugin(String link, String fileName){
273277 }
274278 return false ;
275279 }
276-
277-
280+
281+
278282 public static boolean isZipFile (String filePath ) {
279283 Objects .requireNonNull (filePath , "filePath" );
280284 Path path = Paths .get (filePath );
@@ -301,22 +305,128 @@ private void moveReplace(File from, File to) throws IOException {
301305
302306 private HttpURLConnection openConnection (String link , String githubToken , boolean requiresAuth ) throws IOException {
303307 HttpURLConnection connection = (HttpURLConnection ) new URL (link ).openConnection ();
304- connection .setRequestProperty ("User-Agent" , overrideUserAgent != null ? overrideUserAgent : "AutoUpdatePlugins" );
305- connection .setConnectTimeout (common .UpdateOptions .connectTimeoutMs );
306- connection .setReadTimeout (common .UpdateOptions .readTimeoutMs );
308+ connection .setInstanceFollowRedirects (true );
309+
310+ String ua = (overrideUserAgent != null && !overrideUserAgent .trim ().isEmpty ()
311+ && !"AutoUpdatePlugins" .equalsIgnoreCase (overrideUserAgent ))
312+ ? overrideUserAgent .trim () : null ;
313+ if (ua == null && common .UpdateOptions .userAgents != null && !common .UpdateOptions .userAgents .isEmpty ()) {
314+ ua = common .UpdateOptions .userAgents .get (new java .util .Random ().nextInt (common .UpdateOptions .userAgents .size ()));
315+ }
316+ if (ua == null ) {
317+ ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" ;
318+ }
319+
320+ connection .setRequestProperty ("User-Agent" , ua );
321+ connection .setRequestProperty ("Accept-Encoding" , "identity" );
322+ connection .setRequestProperty ("Accept" , "application/octet-stream, */*" );
323+ connection .setRequestProperty ("Accept-Language" , "en-US,en;q=0.9" );
324+ connection .setRequestProperty ("Connection" , "keep-alive" );
325+
307326 if (requiresAuth && githubToken != null && !githubToken .isEmpty ()) {
308327 connection .setRequestProperty ("Authorization" , "Bearer " + githubToken );
309328 }
329+ try {
330+ java .net .URI uri = java .net .URI .create (link );
331+ String origin = uri .getScheme () + "://" + uri .getHost () + (uri .getPort () > 0 ? (":" + uri .getPort ()) : "" );
332+ connection .setRequestProperty ("Referer" , origin );
333+ } catch (Throwable ignored ) {}
334+
310335 if (extraHeaders != null ) {
311336 for (java .util .Map .Entry <String , String > e : extraHeaders .entrySet ()) {
312337 if (e .getKey () != null && e .getValue () != null ) {
313338 connection .setRequestProperty (e .getKey (), e .getValue ());
314339 }
315340 }
316341 }
342+
343+ connection .setConnectTimeout (common .UpdateOptions .connectTimeoutMs );
344+ connection .setReadTimeout (common .UpdateOptions .readTimeoutMs );
345+
346+ if (common .UpdateOptions .debug ) {
347+ logger .info ("[DEBUG] OpenConnection url=" + link + ", auth=" + requiresAuth + ", ua=" + ua );
348+ }
317349 return connection ;
318350 }
319351
352+
353+ private static boolean isGithubishHost (String host ) {
354+ if (host == null ) return false ;
355+ String h = host .toLowerCase (java .util .Locale .ROOT );
356+ return h .endsWith ("github.com" )
357+ || h .endsWith ("githubusercontent.com" )
358+ || h .endsWith ("codeload.github.com" )
359+ || h .endsWith ("objects.githubusercontent.com" );
360+ }
361+ private boolean downloadWithVerification (File outFile , HttpURLConnection connection ) throws IOException {
362+ long expected = -1L ;
363+ boolean canTrustLength = true ;
364+
365+ try {
366+ expected = connection .getContentLengthLong ();
367+ String ce = connection .getHeaderField ("Content-Encoding" );
368+ String te = connection .getHeaderField ("Transfer-Encoding" );
369+ if (expected < 0 || (ce != null && !"identity" .equalsIgnoreCase (ce )) || (te != null && "chunked" .equalsIgnoreCase (te ))) {
370+ canTrustLength = false ;
371+ }
372+ } catch (Throwable ignored ) {}
373+
374+ long written = 0L ;
375+ try (InputStream in = connection .getInputStream (); FileOutputStream out = new FileOutputStream (outFile )) {
376+ if (common .UpdateOptions .debug ) {
377+ try {
378+ logger .info ("[DEBUG] HTTP code=" + connection .getResponseCode ()
379+ + ", type=" + connection .getContentType ()
380+ + ", length=" + expected
381+ + (connection .getHeaderField ("Content-Encoding" ) != null
382+ ? ", enc=" + connection .getHeaderField ("Content-Encoding" ) : "" ));
383+ } catch (IOException ignored ) {}
384+ }
385+ byte [] buffer = new byte [8192 ];
386+ int n ;
387+ while ((n = in .read (buffer )) != -1 ) {
388+ out .write (buffer , 0 , n );
389+ written += n ;
390+ }
391+ out .getFD ().sync ();
392+ }
393+
394+ if (canTrustLength && expected >= 0 && written != expected ) {
395+ logger .warning ("Content-Length mismatch: expected=" + expected + ", got=" + written );
396+ cleanupQuietly (outFile );
397+ return false ;
398+ }
399+ try (FileInputStream fis = new FileInputStream (outFile )) {
400+ byte [] probe = new byte [64 ];
401+ int n = fis .read (probe );
402+ String head = (n > 0 ) ? new String (probe , 0 , n , java .nio .charset .StandardCharsets .ISO_8859_1 ) : "" ;
403+ String t = head .trim ().toLowerCase (java .util .Locale .ROOT );
404+ if (t .startsWith ("<!doctype html" ) || t .startsWith ("<html" )) {
405+ cleanupQuietly (outFile );
406+ return false ;
407+ }
408+ } catch (Throwable ignored ) {}
409+
410+ return true ;
411+ }
412+
413+
414+
415+
416+ private boolean downloadLenient (File outFile , HttpURLConnection connection ) {
417+ try (InputStream in = connection .getInputStream (); FileOutputStream out = new FileOutputStream (outFile )) {
418+ byte [] buffer = new byte [8192 ];
419+ int r ;
420+ while ((r = in .read (buffer )) != -1 ) out .write (buffer , 0 , r );
421+ out .getFD ().sync ();
422+ return true ;
423+ } catch (IOException e ) {
424+ return false ;
425+ }
426+ }
427+
428+
429+
320430 public boolean buildFromGitHubRepo (String repoPath , String fileName , String githubToken ) throws IOException {
321431 String repoApi = "https://api.github.com/repos" + repoPath ;
322432 String defaultBranch = "main" ;
@@ -395,14 +505,17 @@ public boolean buildFromGitHubRepo(String repoPath, String fileName, String gith
395505 cleanupQuietly (outTmp );
396506 Files .createDirectories (outTmp .getParentFile ().toPath ());
397507 Files .copy (jar , outTmp .toPath (), StandardCopyOption .REPLACE_EXISTING );
508+ if (common .UpdateOptions .debug ) logger .info ("[DEBUG] Built jar selected: " + jar .toAbsolutePath ());
398509 if (!validateJar (outTmp )) {
399510 logger .info ("Built jar is not a valid plugin jar." );
400511 cleanupQuietly (outTmp );
401512 cleanupTreeQuietly (workDir );
402513 cleanupQuietly (rawZipFile );
403514 return false ;
404515 }
405- moveReplace (outTmp , new File (getString (fileName )));
516+ File target = new File (getString (fileName ));
517+ if (common .UpdateOptions .debug ) logger .info ("[DEBUG] Installing built jar: temp=" + outTmp .getAbsolutePath () + " -> target=" + target .getAbsolutePath ());
518+ moveReplace (outTmp , target );
406519 cleanupTreeQuietly (workDir );
407520 cleanupQuietly (rawZipFile );
408521 return true ;
0 commit comments