gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

← Commits

๐Ÿ› (native): Fix GraalVM runtime failures in JGit and SSHD, widen IT coverage

ac37e97b076ebda855d829f49ff11805e06d1d16 ยท Michael Hainz ยท 2026-07-07T11:43:48Z

Changes

8 files changed, +1278 -26

MODIFY README.md +17 -2
diff --git a/README.md b/README.md
index aa42cda..0af4df0 100644
--- a/README.md
+++ b/README.md
@@ -82,11 +82,26 @@
82 82 ```shell script
83 83 ./mvnw quarkus:dev # dev mode; PostgreSQL + Keycloak via Dev Services (Docker/Podman required)
84 84 ./mvnw test # JVM tests
85 -./mvnw verify -Dnative # native build + integration smoke tests (HTTP health, SSH banner)
85 +./mvnw verify -Dnative # native build + integration tests (HTTP, Git protocol, SSH)
86 86 ```
87 87
88 88 Native build uses a container build automatically when no local GraalVM is present
89 89 (`-Dquarkus.native.container-build=true` to force). Binary: `target/git-shark-1.0-SNAPSHOT-runner`.
90 +Native-specific fixes: JGit's NLS classes (`JGitText`, `HttpServerText`) and its config enums are
91 +registered in `META-INF/native-image/reflect-config.json`, and BouncyCastle is registered at build
92 +time via `quarkus.security.security-providers=BC` (Apache SSHD's runtime provider registration
93 +doesn't work in native images).
94 +
95 +Integration tests (Failsafe, `*IT`) run against the packaged app โ€” JVM jar or native binary alike:
96 +
97 +- `SmokeIT` โ€” health endpoint, landing page, explore page, OpenAPI document, SSH banner
98 +- `GitHttpIT` โ€” anonymous JGit clone of the seeded `alice/demo` repo over smart HTTP, anonymous
99 + push rejection (refs unchanged), repository overview page, raw file serving
100 +- `SshHandshakeIT` โ€” full SSH handshake (KEX/host key/cipher via Apache SSHD + BouncyCastle),
101 + asserting clean `publickey`-auth rejection for an unregistered key
102 +
103 +ITs seed demo data (`GITSHARK_DEV_SEED_DATA=true`) and isolate runtime data under
104 +`target/it-data` (see Failsafe `systemPropertyVariables` in `pom.xml`).
90 105
91 106 ### Dev mode seed data
92 107
@@ -105,5 +120,5 @@
105 120
106 121 ## CI
107 122
108 -`.github/workflows/ci.yml`: JVM tests on every push/PR; native build + native smoke tests
123 +`.github/workflows/ci.yml`: JVM tests on every push/PR; native build + integration tests
109 124 gate main-branch builds.
MODIFY pom.xml +5 -0
diff --git a/pom.xml b/pom.xml
index 6aab0b5..143562f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -191,6 +191,11 @@
191 191 </native.image.path>
192 192 <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
193 193 <maven.home>${maven.home}</maven.home>
194 + <!-- Integration tests launch the packaged app (prod profile): seed alice/demo
195 + so transport tests have a repository, and keep runtime data out of ./data -->
196 + <quarkus.test.env.GITSHARK_DEV_SEED_DATA>true</quarkus.test.env.GITSHARK_DEV_SEED_DATA>
197 + <quarkus.test.env.GITSHARK_STORAGE_ROOT>${project.build.directory}/it-data/repositories</quarkus.test.env.GITSHARK_STORAGE_ROOT>
198 + <quarkus.test.env.GITSHARK_SSH_HOST_KEY>${project.build.directory}/it-data/ssh/host-key</quarkus.test.env.GITSHARK_SSH_HOST_KEY>
194 199 </systemPropertyVariables>
195 200 </configuration>
196 201 </plugin>
MODIFY src/main/resources/META-INF/native-image/de.workaround/git-shark/reflect-config.json +1033 -22
diff --git a/src/main/resources/META-INF/native-image/de.workaround/git-shark/reflect-config.json b/src/main/resources/META-INF/native-image/de.workaround/git-shark/reflect-config.json
index 143cbf6..a34aea5 100644
--- a/src/main/resources/META-INF/native-image/de.workaround/git-shark/reflect-config.json
+++ b/src/main/resources/META-INF/native-image/de.workaround/git-shark/reflect-config.json
@@ -3,68 +3,1079 @@
3 3 "name": "java.security.KeyPairGenerator",
4 4 "queryAllDeclaredMethods": true,
5 5 "methods": [
6 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.lang.String"]},
7 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.security.Provider"]},
8 - {"name": "getInstance", "parameterTypes": ["java.lang.String"]}
6 + {
7 + "name": "getInstance",
8 + "parameterTypes": [
9 + "java.lang.String",
10 + "java.lang.String"
11 + ]
12 + },
13 + {
14 + "name": "getInstance",
15 + "parameterTypes": [
16 + "java.lang.String",
17 + "java.security.Provider"
18 + ]
19 + },
20 + {
21 + "name": "getInstance",
22 + "parameterTypes": [
23 + "java.lang.String"
24 + ]
25 + }
9 26 ]
10 27 },
11 28 {
12 29 "name": "java.security.KeyFactory",
13 30 "queryAllDeclaredMethods": true,
14 31 "methods": [
15 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.lang.String"]},
16 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.security.Provider"]},
17 - {"name": "getInstance", "parameterTypes": ["java.lang.String"]}
32 + {
33 + "name": "getInstance",
34 + "parameterTypes": [
35 + "java.lang.String",
36 + "java.lang.String"
37 + ]
38 + },
39 + {
40 + "name": "getInstance",
41 + "parameterTypes": [
42 + "java.lang.String",
43 + "java.security.Provider"
44 + ]
45 + },
46 + {
47 + "name": "getInstance",
48 + "parameterTypes": [
49 + "java.lang.String"
50 + ]
51 + }
18 52 ]
19 53 },
20 54 {
21 55 "name": "javax.crypto.KeyAgreement",
22 56 "queryAllDeclaredMethods": true,
23 57 "methods": [
24 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.lang.String"]},
25 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.security.Provider"]},
26 - {"name": "getInstance", "parameterTypes": ["java.lang.String"]}
58 + {
59 + "name": "getInstance",
60 + "parameterTypes": [
61 + "java.lang.String",
62 + "java.lang.String"
63 + ]
64 + },
65 + {
66 + "name": "getInstance",
67 + "parameterTypes": [
68 + "java.lang.String",
69 + "java.security.Provider"
70 + ]
71 + },
72 + {
73 + "name": "getInstance",
74 + "parameterTypes": [
75 + "java.lang.String"
76 + ]
77 + }
27 78 ]
28 79 },
29 80 {
30 81 "name": "java.security.Signature",
31 82 "queryAllDeclaredMethods": true,
32 83 "methods": [
33 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.lang.String"]},
34 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.security.Provider"]},
35 - {"name": "getInstance", "parameterTypes": ["java.lang.String"]}
84 + {
85 + "name": "getInstance",
86 + "parameterTypes": [
87 + "java.lang.String",
88 + "java.lang.String"
89 + ]
90 + },
91 + {
92 + "name": "getInstance",
93 + "parameterTypes": [
94 + "java.lang.String",
95 + "java.security.Provider"
96 + ]
97 + },
98 + {
99 + "name": "getInstance",
100 + "parameterTypes": [
101 + "java.lang.String"
102 + ]
103 + }
36 104 ]
37 105 },
38 106 {
39 107 "name": "javax.crypto.Cipher",
40 108 "queryAllDeclaredMethods": true,
41 109 "methods": [
42 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.lang.String"]},
43 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.security.Provider"]},
44 - {"name": "getInstance", "parameterTypes": ["java.lang.String"]}
110 + {
111 + "name": "getInstance",
112 + "parameterTypes": [
113 + "java.lang.String",
114 + "java.lang.String"
115 + ]
116 + },
117 + {
118 + "name": "getInstance",
119 + "parameterTypes": [
120 + "java.lang.String",
121 + "java.security.Provider"
122 + ]
123 + },
124 + {
125 + "name": "getInstance",
126 + "parameterTypes": [
127 + "java.lang.String"
128 + ]
129 + }
45 130 ]
46 131 },
47 132 {
48 133 "name": "java.security.MessageDigest",
49 134 "queryAllDeclaredMethods": true,
50 135 "methods": [
51 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.lang.String"]},
52 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.security.Provider"]},
53 - {"name": "getInstance", "parameterTypes": ["java.lang.String"]}
136 + {
137 + "name": "getInstance",
138 + "parameterTypes": [
139 + "java.lang.String",
140 + "java.lang.String"
141 + ]
142 + },
143 + {
144 + "name": "getInstance",
145 + "parameterTypes": [
146 + "java.lang.String",
147 + "java.security.Provider"
148 + ]
149 + },
150 + {
151 + "name": "getInstance",
152 + "parameterTypes": [
153 + "java.lang.String"
154 + ]
155 + }
54 156 ]
55 157 },
56 158 {
57 159 "name": "javax.crypto.Mac",
58 160 "queryAllDeclaredMethods": true,
59 161 "methods": [
60 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.lang.String"]},
61 - {"name": "getInstance", "parameterTypes": ["java.lang.String", "java.security.Provider"]},
62 - {"name": "getInstance", "parameterTypes": ["java.lang.String"]}
162 + {
163 + "name": "getInstance",
164 + "parameterTypes": [
165 + "java.lang.String",
166 + "java.lang.String"
167 + ]
168 + },
169 + {
170 + "name": "getInstance",
171 + "parameterTypes": [
172 + "java.lang.String",
173 + "java.security.Provider"
174 + ]
175 + },
176 + {
177 + "name": "getInstance",
178 + "parameterTypes": [
179 + "java.lang.String"
180 + ]
181 + }
63 182 ]
64 183 },
65 184 {
66 185 "name": "org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory",
67 186 "allDeclaredConstructors": true,
68 187 "queryAllDeclaredConstructors": true
188 + },
189 + {
190 + "name": "org.eclipse.jgit.internal.JGitText",
191 + "allDeclaredConstructors": true,
192 + "allDeclaredFields": true
193 + },
194 + {
195 + "name": "org.eclipse.jgit.http.server.HttpServerText",
196 + "allDeclaredConstructors": true,
197 + "allDeclaredFields": true
198 + },
199 + {
200 + "name": "org.eclipse.jgit.api.CheckoutCommand$Stage",
201 + "methods": [
202 + {
203 + "name": "values",
204 + "parameterTypes": []
205 + }
206 + ]
207 + },
208 + {
209 + "name": "org.eclipse.jgit.api.CheckoutResult$Status",
210 + "methods": [
211 + {
212 + "name": "values",
213 + "parameterTypes": []
214 + }
215 + ]
216 + },
217 + {
218 + "name": "org.eclipse.jgit.api.CherryPickResult$CherryPickStatus",
219 + "methods": [
220 + {
221 + "name": "values",
222 + "parameterTypes": []
223 + }
224 + ]
225 + },
226 + {
227 + "name": "org.eclipse.jgit.api.CloneCommand$FETCH_TYPE",
228 + "methods": [
229 + {
230 + "name": "values",
231 + "parameterTypes": []
232 + }
233 + ]
234 + },
235 + {
236 + "name": "org.eclipse.jgit.api.CreateBranchCommand$SetupUpstreamMode",
237 + "methods": [
238 + {
239 + "name": "values",
240 + "parameterTypes": []
241 + }
242 + ]
243 + },
244 + {
245 + "name": "org.eclipse.jgit.api.ListBranchCommand$ListMode",
246 + "methods": [
247 + {
248 + "name": "values",
249 + "parameterTypes": []
250 + }
251 + ]
252 + },
253 + {
254 + "name": "org.eclipse.jgit.api.MergeCommand$ConflictStyle",
255 + "methods": [
256 + {
257 + "name": "values",
258 + "parameterTypes": []
259 + }
260 + ]
261 + },
262 + {
263 + "name": "org.eclipse.jgit.api.MergeCommand$FastForwardMode",
264 + "methods": [
265 + {
266 + "name": "values",
267 + "parameterTypes": []
268 + }
269 + ]
270 + },
271 + {
272 + "name": "org.eclipse.jgit.api.MergeCommand$FastForwardMode$Merge",
273 + "methods": [
274 + {
275 + "name": "values",
276 + "parameterTypes": []
277 + }
278 + ]
279 + },
280 + {
281 + "name": "org.eclipse.jgit.api.MergeResult$MergeStatus",
282 + "methods": [
283 + {
284 + "name": "values",
285 + "parameterTypes": []
286 + }
287 + ]
288 + },
289 + {
290 + "name": "org.eclipse.jgit.api.RebaseCommand$Operation",
291 + "methods": [
292 + {
293 + "name": "values",
294 + "parameterTypes": []
295 + }
296 + ]
297 + },
298 + {
299 + "name": "org.eclipse.jgit.api.RebaseResult$Status",
300 + "methods": [
301 + {
302 + "name": "values",
303 + "parameterTypes": []
304 + }
305 + ]
306 + },
307 + {
308 + "name": "org.eclipse.jgit.api.RemoteSetUrlCommand$UriType",
309 + "methods": [
310 + {
311 + "name": "values",
312 + "parameterTypes": []
313 + }
314 + ]
315 + },
316 + {
317 + "name": "org.eclipse.jgit.api.ResetCommand$ResetType",
318 + "methods": [
319 + {
320 + "name": "values",
321 + "parameterTypes": []
322 + }
323 + ]
324 + },
325 + {
326 + "name": "org.eclipse.jgit.api.SubmoduleDeinitCommand$SubmoduleDeinitStatus",
327 + "methods": [
328 + {
329 + "name": "values",
330 + "parameterTypes": []
331 + }
332 + ]
333 + },
334 + {
335 + "name": "org.eclipse.jgit.api.VerifySignatureCommand$VerifyMode",
336 + "methods": [
337 + {
338 + "name": "values",
339 + "parameterTypes": []
340 + }
341 + ]
342 + },
343 + {
344 + "name": "org.eclipse.jgit.attributes.Attribute$State",
345 + "methods": [
346 + {
347 + "name": "values",
348 + "parameterTypes": []
349 + }
350 + ]
351 + },
352 + {
353 + "name": "org.eclipse.jgit.diff.DiffAlgorithm$SupportedAlgorithm",
354 + "methods": [
355 + {
356 + "name": "values",
357 + "parameterTypes": []
358 + }
359 + ]
360 + },
361 + {
362 + "name": "org.eclipse.jgit.diff.DiffConfig$RenameDetectionType",
363 + "methods": [
364 + {
365 + "name": "values",
366 + "parameterTypes": []
367 + }
368 + ]
369 + },
370 + {
371 + "name": "org.eclipse.jgit.diff.DiffDriver",
372 + "methods": [
373 + {
374 + "name": "values",
375 + "parameterTypes": []
376 + }
377 + ]
378 + },
379 + {
380 + "name": "org.eclipse.jgit.diff.DiffEntry$ChangeType",
381 + "methods": [
382 + {
383 + "name": "values",
384 + "parameterTypes": []
385 + }
386 + ]
387 + },
388 + {
389 + "name": "org.eclipse.jgit.diff.DiffEntry$Side",
390 + "methods": [
391 + {
392 + "name": "values",
393 + "parameterTypes": []
394 + }
395 + ]
396 + },
397 + {
398 + "name": "org.eclipse.jgit.diff.Edit$Type",
399 + "methods": [
400 + {
401 + "name": "values",
402 + "parameterTypes": []
403 + }
404 + ]
405 + },
406 + {
407 + "name": "org.eclipse.jgit.dircache.DirCache$DirCacheVersion",
408 + "methods": [
409 + {
410 + "name": "values",
411 + "parameterTypes": []
412 + }
413 + ]
414 + },
415 + {
416 + "name": "org.eclipse.jgit.errors.CorruptPackIndexException$ErrorType",
417 + "methods": [
418 + {
419 + "name": "values",
420 + "parameterTypes": []
421 + }
422 + ]
423 + },
424 + {
425 + "name": "org.eclipse.jgit.errors.NoMergeBaseException$MergeBaseFailureReason",
426 + "methods": [
427 + {
428 + "name": "values",
429 + "parameterTypes": []
430 + }
431 + ]
432 + },
433 + {
434 + "name": "org.eclipse.jgit.ignore.IgnoreNode$MatchResult",
435 + "methods": [
436 + {
437 + "name": "values",
438 + "parameterTypes": []
439 + }
440 + ]
441 + },
442 + {
443 + "name": "org.eclipse.jgit.ignore.internal.Strings$PatternState",
444 + "methods": [
445 + {
446 + "name": "values",
447 + "parameterTypes": []
448 + }
449 + ]
450 + },
451 + {
452 + "name": "org.eclipse.jgit.internal.diffmergetool.CommandLineDiffTool",
453 + "methods": [
454 + {
455 + "name": "values",
456 + "parameterTypes": []
457 + }
458 + ]
459 + },
460 + {
461 + "name": "org.eclipse.jgit.internal.diffmergetool.CommandLineMergeTool",
462 + "methods": [
463 + {
464 + "name": "values",
465 + "parameterTypes": []
466 + }
467 + ]
468 + },
469 + {
470 + "name": "org.eclipse.jgit.internal.diffmergetool.FileElement$Type",
471 + "methods": [
472 + {
473 + "name": "values",
474 + "parameterTypes": []
475 + }
476 + ]
477 + },
478 + {
479 + "name": "org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase$PackSource",
480 + "methods": [
481 + {
482 + "name": "values",
483 + "parameterTypes": []
484 + }
485 + ]
486 + },
487 + {
488 + "name": "org.eclipse.jgit.internal.storage.file.FileObjectDatabase$InsertLooseObjectResult",
489 + "methods": [
490 + {
491 + "name": "values",
492 + "parameterTypes": []
493 + }
494 + ]
495 + },
496 + {
497 + "name": "org.eclipse.jgit.internal.storage.pack.PackExt",
498 + "methods": [
499 + {
500 + "name": "values",
501 + "parameterTypes": []
502 + }
503 + ]
504 + },
505 + {
506 + "name": "org.eclipse.jgit.internal.storage.pack.PackWriter$PackingPhase",
507 + "methods": [
508 + {
509 + "name": "values",
510 + "parameterTypes": []
511 + }
512 + ]
513 + },
514 + {
515 + "name": "org.eclipse.jgit.internal.util.ShutdownHook",
516 + "methods": [
517 + {
518 + "name": "values",
519 + "parameterTypes": []
520 + }
521 + ]
522 + },
523 + {
524 + "name": "org.eclipse.jgit.lib.BranchConfig$BranchRebaseMode",
525 + "methods": [
526 + {
527 + "name": "values",
528 + "parameterTypes": []
529 + }
530 + ]
531 + },
532 + {
533 + "name": "org.eclipse.jgit.lib.CommitConfig$CleanupMode",
534 + "methods": [
535 + {
536 + "name": "values",
537 + "parameterTypes": []
538 + }
539 + ]
540 + },
541 + {
542 + "name": "org.eclipse.jgit.lib.CoreConfig$AutoCRLF",
543 + "methods": [
544 + {
545 + "name": "values",
546 + "parameterTypes": []
547 + }
548 + ]
549 + },
550 + {
551 + "name": "org.eclipse.jgit.lib.CoreConfig$CheckStat",
552 + "methods": [
553 + {
554 + "name": "values",
555 + "parameterTypes": []
556 + }
557 + ]
558 + },
559 + {
560 + "name": "org.eclipse.jgit.lib.CoreConfig$EOL",
561 + "methods": [
562 + {
563 + "name": "values",
564 + "parameterTypes": []
565 + }
566 + ]
567 + },
568 + {
569 + "name": "org.eclipse.jgit.lib.CoreConfig$EolStreamType",
570 + "methods": [
571 + {
572 + "name": "values",
573 + "parameterTypes": []
574 + }
575 + ]
576 + },
577 + {
578 + "name": "org.eclipse.jgit.lib.CoreConfig$HideDotFiles",
579 + "methods": [
580 + {
581 + "name": "values",
582 + "parameterTypes": []
583 + }
584 + ]
585 + },
586 + {
587 + "name": "org.eclipse.jgit.lib.CoreConfig$LogRefUpdates",
588 + "methods": [
589 + {
590 + "name": "values",
591 + "parameterTypes": []
592 + }
593 + ]
594 + },
595 + {
596 + "name": "org.eclipse.jgit.lib.CoreConfig$SymLinks",
597 + "methods": [
598 + {
599 + "name": "values",
600 + "parameterTypes": []
601 + }
602 + ]
603 + },
604 + {
605 + "name": "org.eclipse.jgit.lib.CoreConfig$TrustLooseRefStat",
606 + "methods": [
607 + {
608 + "name": "values",
609 + "parameterTypes": []
610 + }
611 + ]
612 + },
613 + {
614 + "name": "org.eclipse.jgit.lib.CoreConfig$TrustPackedRefsStat",
615 + "methods": [
616 + {
617 + "name": "values",
618 + "parameterTypes": []
619 + }
620 + ]
621 + },
622 + {
623 + "name": "org.eclipse.jgit.lib.CoreConfig$TrustStat",
624 + "methods": [
625 + {
626 + "name": "values",
627 + "parameterTypes": []
628 + }
629 + ]
630 + },
631 + {
632 + "name": "org.eclipse.jgit.lib.GpgConfig$GpgFormat",
633 + "methods": [
634 + {
635 + "name": "values",
636 + "parameterTypes": []
637 + }
638 + ]
639 + },
640 + {
641 + "name": "org.eclipse.jgit.lib.IndexDiff$StageState",
642 + "methods": [
643 + {
644 + "name": "values",
645 + "parameterTypes": []
646 + }
647 + ]
648 + },
649 + {
650 + "name": "org.eclipse.jgit.lib.ObjectChecker$ErrorType",
651 + "methods": [
652 + {
653 + "name": "values",
654 + "parameterTypes": []
655 + }
656 + ]
657 + },
658 + {
659 + "name": "org.eclipse.jgit.lib.RebaseTodoLine$Action",
660 + "methods": [
661 + {
662 + "name": "values",
663 + "parameterTypes": []
664 + }
665 + ]
666 + },
667 + {
668 + "name": "org.eclipse.jgit.lib.Ref$Storage",
669 + "methods": [
670 + {
671 + "name": "values",
672 + "parameterTypes": []
673 + }
674 + ]
675 + },
676 + {
677 + "name": "org.eclipse.jgit.lib.RefUpdate$Result",
678 + "methods": [
679 + {
680 + "name": "values",
681 + "parameterTypes": []
682 + }
683 + ]
684 + },
685 + {
686 + "name": "org.eclipse.jgit.lib.RepositoryState",
687 + "methods": [
688 + {
689 + "name": "values",
690 + "parameterTypes": []
691 + }
692 + ]
693 + },
694 + {
695 + "name": "org.eclipse.jgit.lib.SignatureVerifier$TrustLevel",
696 + "methods": [
697 + {
698 + "name": "values",
699 + "parameterTypes": []
700 + }
701 + ]
702 + },
703 + {
704 + "name": "org.eclipse.jgit.lib.SubmoduleConfig$FetchRecurseSubmodulesMode",
705 + "methods": [
706 + {
707 + "name": "values",
708 + "parameterTypes": []
709 + }
710 + ]
711 + },
712 + {
713 + "name": "org.eclipse.jgit.lib.internal.BooleanTriState",
714 + "methods": [
715 + {
716 + "name": "values",
717 + "parameterTypes": []
718 + }
719 + ]
720 + },
721 + {
722 + "name": "org.eclipse.jgit.merge.ContentMergeStrategy",
723 + "methods": [
724 + {
725 + "name": "values",
726 + "parameterTypes": []
727 + }
728 + ]
729 + },
730 + {
731 + "name": "org.eclipse.jgit.merge.MergeChunk$ConflictState",
732 + "methods": [
733 + {
734 + "name": "values",
735 + "parameterTypes": []
736 + }
737 + ]
738 + },
739 + {
740 + "name": "org.eclipse.jgit.merge.ResolveMerger$MergeFailureReason",
741 + "methods": [
742 + {
743 + "name": "values",
744 + "parameterTypes": []
745 + }
746 + ]
747 + },
748 + {
749 + "name": "org.eclipse.jgit.patch.BinaryHunk$Type",
750 + "methods": [
751 + {
752 + "name": "values",
753 + "parameterTypes": []
754 + }
755 + ]
756 + },
757 + {
758 + "name": "org.eclipse.jgit.patch.FileHeader$PatchType",
759 + "methods": [
760 + {
761 + "name": "values",
762 + "parameterTypes": []
763 + }
764 + ]
765 + },
766 + {
767 + "name": "org.eclipse.jgit.patch.FormatError$Severity",
768 + "methods": [
769 + {
770 + "name": "values",
771 + "parameterTypes": []
772 + }
773 + ]
774 + },
775 + {
776 + "name": "org.eclipse.jgit.revwalk.RevSort",
777 + "methods": [
778 + {
779 + "name": "values",
780 + "parameterTypes": []
781 + }
782 + ]
783 + },
784 + {
785 + "name": "org.eclipse.jgit.revwalk.RevWalk$GetMergedIntoStrategy",
786 + "methods": [
787 + {
788 + "name": "values",
789 + "parameterTypes": []
790 + }
791 + ]
792 + },
793 + {
794 + "name": "org.eclipse.jgit.submodule.SubmoduleStatusType",
795 + "methods": [
796 + {
797 + "name": "values",
798 + "parameterTypes": []
799 + }
800 + ]
801 + },
802 + {
803 + "name": "org.eclipse.jgit.submodule.SubmoduleWalk$IgnoreSubmoduleMode",
804 + "methods": [
805 + {
806 + "name": "values",
807 + "parameterTypes": []
808 + }
809 + ]
810 + },
811 + {
812 + "name": "org.eclipse.jgit.transport.GitProtocolConstants$MultiAck",
813 + "methods": [
814 + {
815 + "name": "values",
816 + "parameterTypes": []
817 + }
818 + ]
819 + },
820 + {
821 + "name": "org.eclipse.jgit.transport.HttpAuthMethod$Type",
822 + "methods": [
823 + {
824 + "name": "values",
825 + "parameterTypes": []
826 + }
827 + ]
828 + },
829 + {
830 + "name": "org.eclipse.jgit.transport.HttpConfig$HttpRedirectMode",
831 + "methods": [
832 + {
833 + "name": "values",
834 + "parameterTypes": []
835 + }
836 + ]
837 + },
838 + {
839 + "name": "org.eclipse.jgit.transport.NetRC$State",
840 + "methods": [
841 + {
842 + "name": "values",
843 + "parameterTypes": []
844 + }
845 + ]
846 + },
847 + {
848 + "name": "org.eclipse.jgit.transport.PackParser$Source",
849 + "methods": [
850 + {
851 + "name": "values",
852 + "parameterTypes": []
853 + }
854 + ]
855 + },
856 + {
857 + "name": "org.eclipse.jgit.transport.PacketLineIn$AckNackResult",
858 + "methods": [
859 + {
860 + "name": "values",
861 + "parameterTypes": []
862 + }
863 + ]
864 + },
865 + {
866 + "name": "org.eclipse.jgit.transport.PushCertificate$NonceStatus",
867 + "methods": [
868 + {
869 + "name": "values",
870 + "parameterTypes": []
871 + }
872 + ]
873 + },
874 + {
875 + "name": "org.eclipse.jgit.transport.PushConfig$PushDefault",
876 + "methods": [
877 + {
878 + "name": "values",
879 + "parameterTypes": []
880 + }
881 + ]
882 + },
883 + {
884 + "name": "org.eclipse.jgit.transport.PushConfig$PushRecurseSubmodulesMode",
885 + "methods": [
886 + {
887 + "name": "values",
888 + "parameterTypes": []
889 + }
890 + ]
891 + },
892 + {
893 + "name": "org.eclipse.jgit.transport.ReceiveCommand$Result",
894 + "methods": [
895 + {
896 + "name": "values",
897 + "parameterTypes": []
898 + }
899 + ]
900 + },
901 + {
902 + "name": "org.eclipse.jgit.transport.ReceiveCommand$Type",
903 + "methods": [
904 + {
905 + "name": "values",
906 + "parameterTypes": []
907 + }
908 + ]
909 + },
910 + {
911 + "name": "org.eclipse.jgit.transport.RefSpec$WildcardMode",
912 + "methods": [
913 + {
914 + "name": "values",
915 + "parameterTypes": []
916 + }
917 + ]
918 + },
919 + {
920 + "name": "org.eclipse.jgit.transport.RemoteRefUpdate$Status",
921 + "methods": [
922 + {
923 + "name": "values",
924 + "parameterTypes": []
925 + }
926 + ]
927 + },
928 + {
929 + "name": "org.eclipse.jgit.transport.TagOpt",
930 + "methods": [
931 + {
932 + "name": "values",
933 + "parameterTypes": []
934 + }
935 + ]
936 + },
937 + {
938 + "name": "org.eclipse.jgit.transport.TransferConfig$FsckMode",
939 + "methods": [
940 + {
941 + "name": "values",
942 + "parameterTypes": []
943 + }
944 + ]
945 + },
946 + {
947 + "name": "org.eclipse.jgit.transport.TransferConfig$ProtocolVersion",
948 + "methods": [
949 + {
950 + "name": "values",
951 + "parameterTypes": []
952 + }
953 + ]
954 + },
955 + {
956 + "name": "org.eclipse.jgit.transport.Transport$Operation",
957 + "methods": [
958 + {
959 + "name": "values",
960 + "parameterTypes": []
961 + }
962 + ]
963 + },
964 + {
965 + "name": "org.eclipse.jgit.transport.TransportHttp$AcceptEncoding",
966 + "methods": [
967 + {
968 + "name": "values",
969 + "parameterTypes": []
970 + }
971 + ]
972 + },
973 + {
974 + "name": "org.eclipse.jgit.transport.TransportProtocol$URIishField",
975 + "methods": [
976 + {
977 + "name": "values",
978 + "parameterTypes": []
979 + }
980 + ]
981 + },
982 + {
983 + "name": "org.eclipse.jgit.transport.UploadPack$Option",
984 + "methods": [
985 + {
986 + "name": "values",
987 + "parameterTypes": []
988 + }
989 + ]
990 + },
991 + {
992 + "name": "org.eclipse.jgit.transport.UploadPack$RequestPolicy",
993 + "methods": [
994 + {
995 + "name": "values",
996 + "parameterTypes": []
997 + }
998 + ]
999 + },
1000 + {
1001 + "name": "org.eclipse.jgit.treewalk.TreeWalk$OperationType",
1002 + "methods": [
1003 + {
1004 + "name": "values",
1005 + "parameterTypes": []
1006 + }
1007 + ]
1008 + },
1009 + {
1010 + "name": "org.eclipse.jgit.treewalk.WorkingTreeIterator$MetadataDiff",
1011 + "methods": [
1012 + {
1013 + "name": "values",
1014 + "parameterTypes": []
1015 + }
1016 + ]
1017 + },
1018 + {
1019 + "name": "org.eclipse.jgit.util.FS_POSIX$AtomicFileCreation",
1020 + "methods": [
1021 + {
1022 + "name": "values",
1023 + "parameterTypes": []
1024 + }
1025 + ]
1026 + },
1027 + {
1028 + "name": "org.eclipse.jgit.util.GitDateFormatter$Format",
1029 + "methods": [
1030 + {
1031 + "name": "values",
1032 + "parameterTypes": []
1033 + }
1034 + ]
1035 + },
1036 + {
1037 + "name": "org.eclipse.jgit.util.GitDateParser$ParseableSimpleDateFormat",
1038 + "methods": [
1039 + {
1040 + "name": "values",
1041 + "parameterTypes": []
1042 + }
1043 + ]
1044 + },
1045 + {
1046 + "name": "org.eclipse.jgit.util.GitTimeParser$ParseableSimpleDateFormat",
1047 + "methods": [
1048 + {
1049 + "name": "values",
1050 + "parameterTypes": []
1051 + }
1052 + ]
1053 + },
1054 + {
1055 + "name": "org.eclipse.jgit.util.ProcessResult$Status",
1056 + "methods": [
1057 + {
1058 + "name": "values",
1059 + "parameterTypes": []
1060 + }
1061 + ]
1062 + },
1063 + {
1064 + "name": "org.eclipse.jgit.util.io.AutoLFInputStream$StreamFlag",
1065 + "methods": [
1066 + {
1067 + "name": "values",
1068 + "parameterTypes": []
1069 + }
1070 + ]
1071 + },
1072 + {
1073 + "name": "org.eclipse.jgit.util.sha1.SHA1$Sha1Implementation",
1074 + "methods": [
1075 + {
1076 + "name": "values",
1077 + "parameterTypes": []
1078 + }
1079 + ]
69 1080 }
70 -]
1081 +]
\ No newline at end of file
MODIFY src/main/resources/META-INF/native-image/de.workaround/git-shark/resource-config.json +2 -1
diff --git a/src/main/resources/META-INF/native-image/de.workaround/git-shark/resource-config.json b/src/main/resources/META-INF/native-image/de.workaround/git-shark/resource-config.json
index fb742c5..8e18423 100644
--- a/src/main/resources/META-INF/native-image/de.workaround/git-shark/resource-config.json
+++ b/src/main/resources/META-INF/native-image/de.workaround/git-shark/resource-config.json
@@ -2,7 +2,8 @@
2 2 "resources": {
3 3 "includes": [
4 4 {"pattern": "org/eclipse/jgit/internal/JGitText\\.properties"},
5 - {"pattern": ".*JGitText.*\\.properties"}
5 + {"pattern": ".*JGitText.*\\.properties"},
6 + {"pattern": "org/eclipse/jgit/.*\\.properties"}
6 7 ]
7 8 },
8 9 "bundles": [
MODIFY src/main/resources/application.properties +3 -0
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index fcc5ad2..b281771 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -54,6 +54,9 @@
54 54
55 55 # Native image: JGit holds static Random/lazy state that must not be build-time initialized
56 56 quarkus.native.additional-build-args=--initialize-at-run-time=org.eclipse.jgit,--initialize-at-run-time=org.apache.sshd.common.random,--initialize-at-run-time=org.apache.sshd.common.util.security.bouncycastle.BouncyCastleEncryptedPrivateKeyInfoDecryptor
57 +# Register BouncyCastle at build time: SSHD's runtime registrar (Security.addProvider via reflection)
58 +# does not work in a native image, leaving host-key generation without the BC provider.
59 +quarkus.security.security-providers=BC
57 60
58 61 # Git repository storage
59 62 gitshark.storage.root=${GITSHARK_STORAGE_ROOT:data/repositories}
MODIFY src/test/java/de/workaround/SmokeIT.java +29 -1
diff --git a/src/test/java/de/workaround/SmokeIT.java b/src/test/java/de/workaround/SmokeIT.java
index 62aa6c7..4d674c2 100644
--- a/src/test/java/de/workaround/SmokeIT.java
+++ b/src/test/java/de/workaround/SmokeIT.java
@@ -8,7 +8,8 @@
8 8
9 9 /**
10 10 * Boots the packaged application (JVM jar or native binary) and verifies the service is alive:
11 - * HTTP health endpoint up and the embedded SSH server accepting connections.
11 + * HTTP health endpoint up, public pages (Qute templates) rendering, the OpenAPI document
12 + * served, and the embedded SSH server accepting connections.
12 13 */
13 14 @QuarkusIntegrationTest
14 15 class SmokeIT
@@ -24,6 +25,33 @@
24 25 }
25 26
26 27 @Test
28 + void landingPageRenders()
29 + {
30 + given()
31 + .when().get("/")
32 + .then()
33 + .statusCode(200);
34 + }
35 +
36 + @Test
37 + void explorePageRenders()
38 + {
39 + given()
40 + .when().get("/explore")
41 + .then()
42 + .statusCode(200);
43 + }
44 +
45 + @Test
46 + void openApiDocumentIsServed()
47 + {
48 + given()
49 + .when().get("/q/openapi")
50 + .then()
51 + .statusCode(200);
52 + }
53 +
54 + @Test
27 55 void sshPortPresentsSshBanner() throws Exception
28 56 {
29 57 int port = Integer.getInteger("gitshark.ssh.port", 2222);
ADD src/test/java/de/workaround/http/GitHttpIT.java +107 -0
diff --git a/src/test/java/de/workaround/http/GitHttpIT.java b/src/test/java/de/workaround/http/GitHttpIT.java
new file mode 100644
index 0000000..53d6ec5
--- /dev/null
+++ b/src/test/java/de/workaround/http/GitHttpIT.java
@@ -0,0 +1,107 @@
1 +package de.workaround.http;
2 +
3 +import java.nio.file.Files;
4 +import java.nio.file.Path;
5 +
6 +import org.eclipse.jgit.api.Git;
7 +import org.eclipse.jgit.api.errors.TransportException;
8 +import org.eclipse.jgit.transport.RefSpec;
9 +import org.junit.jupiter.api.Test;
10 +
11 +import io.quarkus.test.junit.QuarkusIntegrationTest;
12 +
13 +import static io.restassured.RestAssured.given;
14 +import static org.hamcrest.CoreMatchers.containsString;
15 +import static org.junit.jupiter.api.Assertions.assertFalse;
16 +import static org.junit.jupiter.api.Assertions.assertThrows;
17 +import static org.junit.jupiter.api.Assertions.assertTrue;
18 +
19 +/**
20 + * Exercises the git smart-HTTP transport and repository browsing against the packaged
21 + * application (JVM jar or native binary). The seeded alice/demo repository (enabled via
22 + * GITSHARK_DEV_SEED_DATA in the failsafe config) provides the data; JGit acts as a real
23 + * client, so this covers the servlet, UploadPack, JGit object I/O and the DB lookup path
24 + * in the native image.
25 + */
26 +@QuarkusIntegrationTest
27 +class GitHttpIT
28 +{
29 + private static String cloneUrl()
30 + {
31 + int port = Integer.getInteger("quarkus.http.test-port", 8081);
32 + return "http://localhost:" + port + "/git/alice/demo.git";
33 + }
34 +
35 + @Test
36 + void anonymousCloneOfSeededPublicRepositorySucceeds() throws Exception
37 + {
38 + Path target = Files.createTempDirectory("it-clone");
39 + try (Git clone = Git.cloneRepository().setURI(cloneUrl()).setDirectory(target.toFile()).call())
40 + {
41 + assertTrue(Files.exists(target.resolve("README.md")), "clone must contain the seeded README");
42 + assertTrue(Files.readString(target.resolve("README.md")).contains("demo"));
43 + }
44 + }
45 +
46 + @Test
47 + void anonymousPushIsRejectedAndDoesNotAlterTheRepository() throws Exception
48 + {
49 + Path work = Files.createTempDirectory("it-push");
50 + try (Git git = Git.cloneRepository().setURI(cloneUrl()).setDirectory(work.toFile()).call())
51 + {
52 + Files.writeString(work.resolve("evil.txt"), "anonymous write\n");
53 + git.add().addFilepattern(".").call();
54 + git.commit().setMessage("anonymous push attempt").setSign(false)
55 + .setAuthor("t", "t@example.com").setCommitter("t", "t@example.com").call();
56 + assertThrows(TransportException.class,
57 + () -> git.push().setRefSpecs(new RefSpec("HEAD:refs/heads/main")).call());
58 + }
59 +
60 + Path recheck = Files.createTempDirectory("it-recheck");
61 + try (Git clone = Git.cloneRepository().setURI(cloneUrl()).setDirectory(recheck.toFile()).call())
62 + {
63 + assertFalse(Files.exists(recheck.resolve("evil.txt")), "rejected push must not change refs");
64 + }
65 + }
66 +
67 + @Test
68 + void repositoryOverviewPageRenders()
69 + {
70 + given()
71 + .when().get("/repos/alice/demo")
72 + .then()
73 + .statusCode(200)
74 + .body(containsString("demo"));
75 + }
76 +
77 + @Test
78 + void rawFileIsServedFromTheSeededRepository()
79 + {
80 + given()
81 + .when().get("/repos/alice/demo/raw/main/README.md")
82 + .then()
83 + .statusCode(200)
84 + .body(containsString("demo"));
85 + }
86 +
87 + @Test
88 + void blobPageRendersSeededFileWithHighlightMarkup()
89 + {
90 + given()
91 + .when().get("/repos/alice/demo/tree/main/README.md")
92 + .then()
93 + .statusCode(200)
94 + .body(containsString("demo"))
95 + .body(containsString("<code"));
96 + }
97 +
98 + @Test
99 + void vendoredHighlightAssetIsServed()
100 + {
101 + given()
102 + .when().get("/highlight.min.js")
103 + .then()
104 + .statusCode(200);
105 + }
106 +
107 +}
ADD src/test/java/de/workaround/ssh/SshHandshakeIT.java +82 -0
diff --git a/src/test/java/de/workaround/ssh/SshHandshakeIT.java b/src/test/java/de/workaround/ssh/SshHandshakeIT.java
new file mode 100644
index 0000000..bfce845
--- /dev/null
+++ b/src/test/java/de/workaround/ssh/SshHandshakeIT.java
@@ -0,0 +1,82 @@
1 +package de.workaround.ssh;
2 +
3 +import java.nio.file.Files;
4 +import java.nio.file.Path;
5 +import java.security.KeyPair;
6 +import java.security.KeyPairGenerator;
7 +import java.util.List;
8 +
9 +import org.eclipse.jgit.api.Git;
10 +import org.eclipse.jgit.api.errors.TransportException;
11 +import org.eclipse.jgit.transport.SshTransport;
12 +import org.eclipse.jgit.transport.sshd.JGitKeyCache;
13 +import org.eclipse.jgit.transport.sshd.ServerKeyDatabase;
14 +import org.eclipse.jgit.transport.sshd.SshdSessionFactory;
15 +import org.eclipse.jgit.transport.sshd.SshdSessionFactoryBuilder;
16 +import org.junit.jupiter.api.Test;
17 +
18 +import io.quarkus.test.junit.QuarkusIntegrationTest;
19 +
20 +import static org.junit.jupiter.api.Assertions.assertNotNull;
21 +import static org.junit.jupiter.api.Assertions.assertThrows;
22 +import static org.junit.jupiter.api.Assertions.assertTrue;
23 +
24 +/**
25 + * Drives a complete SSH handshake (key exchange, host key signature, cipher negotiation)
26 + * against the packaged application and expects a clean publickey-auth rejection for an
27 + * unregistered key. KEX and signatures are the classic native-image failure zone for
28 + * Apache SSHD + BouncyCastle, so a crypto regression fails this test with a negotiation
29 + * error instead of the expected auth failure.
30 + */
31 +@QuarkusIntegrationTest
32 +class SshHandshakeIT
33 +{
34 + @Test
35 + void fullHandshakeCompletesAndUnregisteredKeyIsRejectedAtAuth() throws Exception
36 + {
37 + int port = Integer.getInteger("gitshark.ssh.port", 2222);
38 + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
39 + generator.initialize(2048);
40 + KeyPair unregistered = generator.generateKeyPair();
41 +
42 + Path home = Files.createTempDirectory("it-ssh-home");
43 + Files.createDirectories(home.resolve(".ssh"));
44 + try (SshdSessionFactory factory = new SshdSessionFactoryBuilder()
45 + .setPreferredAuthentications("publickey")
46 + .setHomeDirectory(home.toFile())
47 + .setSshDirectory(home.resolve(".ssh").toFile())
48 + .setDefaultKeysProvider(dir -> List.of(unregistered))
49 + .setServerKeyDatabase((ignoredHome, ignoredSsh) -> new ServerKeyDatabase()
50 + {
51 + @Override
52 + public List<java.security.PublicKey> lookup(String connectAddress,
53 + java.net.InetSocketAddress remoteAddress, Configuration config)
54 + {
55 + return List.of();
56 + }
57 +
58 + @Override
59 + public boolean accept(String connectAddress, java.net.InetSocketAddress remoteAddress,
60 + java.security.PublicKey serverKey, Configuration config,
61 + org.eclipse.jgit.transport.CredentialsProvider provider)
62 + {
63 + return true;
64 + }
65 + })
66 + .build(new JGitKeyCache()))
67 + {
68 + Path target = Files.createTempDirectory("it-ssh-clone");
69 + TransportException e = assertThrows(TransportException.class, () -> Git.cloneRepository()
70 + .setURI("ssh://git@localhost:" + port + "/alice/demo.git")
71 + .setDirectory(target.toFile())
72 + .setTransportConfigCallback(t -> ((SshTransport) t).setSshSessionFactory(factory))
73 + .call());
74 +
75 + assertNotNull(e.getMessage());
76 + String message = e.getMessage().toLowerCase();
77 + assertTrue(message.contains("auth") || message.contains("log in") || message.contains("refused"),
78 + "handshake must reach the auth stage (crypto negotiation intact), but was: " + e.getMessage());
79 + }
80 + }
81 +
82 +}

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog