OXYGENHUB

TLS: oxy serve --tls-cert/--tls-key via rustls; remote.<name>.ca and OXY_CA for self-hosted roots 2b5ee371060e

Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-26 19:11 UTC
Signature
✔ signed by Andy McCall
Hash
2b5ee371060e4359f6bebd5436825c1de4e6fe3cac5cf16774ea6481cafb7c4b
Depends on
5b9c6ad45f6e 750251b21a96 982864068a59 e993a086c13f fce44ae9ad29 ff0af1ee299d
Depended on by
0588de69a84d 569802ebae0e 642da2636de2 544d0541a7e5 c8d9059e4e70 c895bc52e4e0 68a924846379 cb8b8082fa73 42ccf504aa50 2cd40ea1c04c 8d80356371c5 9f9e72b93e4e ff1f62fee654 a07f60a37fb2 11c958021e1a 3e5772ccf9da e7ddd552b619 37c299cec358 75167672ebcd e02bf0cd0533 b3c5a59a875a 2b872d074ddb 2863456a0f6d 135212de5370 252e8cb23fbc ba160f60219e d02e545dc265 83e0677b07fe
In bookmarks
agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
In tags
v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
87 atoms, 0 tree operations, 11 files

Browse the tree at this change

modified Cargo.lock +417 −5
…
73 unchanged lines
74 74
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
75 75
76 76
[[package]]
77 +
name = "arc-swap"
78 +
version = "1.9.2"
79 +
source = "registry+https://github.com/rust-lang/crates.io-index"
80 +
checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
81 +
dependencies = [
82 +
 "rustversion",
83 +
]
84 +
85 +
[[package]]
77 86
name = "arrayvec"
78 87
version = "0.7.8"
79 88
source = "registry+https://github.com/rust-lang/crates.io-index"
80 89
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
81 90
82 91
[[package]]
92 +
name = "asn1-rs"
93 +
version = "0.7.2"
94 +
source = "registry+https://github.com/rust-lang/crates.io-index"
95 +
checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8"
96 +
dependencies = [
97 +
 "asn1-rs-derive",
98 +
 "asn1-rs-impl",
99 +
 "displaydoc",
100 +
 "nom",
101 +
 "num-traits",
102 +
 "rusticata-macros",
103 +
 "thiserror 2.0.21",
104 +
 "time",
105 +
]
106 +
107 +
[[package]]
108 +
name = "asn1-rs-derive"
109 +
version = "0.6.0"
110 +
source = "registry+https://github.com/rust-lang/crates.io-index"
111 +
checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
112 +
dependencies = [
113 +
 "proc-macro2",
114 +
 "quote",
115 +
 "syn 2.0.119",
116 +
 "synstructure 0.13.2",
117 +
]
118 +
119 +
[[package]]
120 +
name = "asn1-rs-impl"
121 +
version = "0.2.0"
122 +
source = "registry+https://github.com/rust-lang/crates.io-index"
123 +
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
124 +
dependencies = [
125 +
 "proc-macro2",
126 +
 "quote",
127 +
 "syn 2.0.119",
128 +
]
129 +
130 +
[[package]]
83 131
name = "atomic-waker"
84 132
version = "1.1.2"
85 133
source = "registry+https://github.com/rust-lang/crates.io-index"
…
58 unchanged lines
144 192
]
145 193
146 194
[[package]]
195 +
name = "axum-server"
196 +
version = "0.8.0"
197 +
source = "registry+https://github.com/rust-lang/crates.io-index"
198 +
checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc"
199 +
dependencies = [
200 +
 "arc-swap",
201 +
 "bytes",
202 +
 "either",
203 +
 "fs-err",
204 +
 "http",
205 +
 "http-body",
206 +
 "hyper",
207 +
 "hyper-util",
208 +
 "pin-project-lite",
209 +
 "rustls",
210 +
 "rustls-pki-types",
211 +
 "tokio",
212 +
 "tokio-rustls",
213 +
 "tower-service",
214 +
]
215 +
216 +
[[package]]
147 217
name = "base64"
148 218
version = "0.22.1"
149 219
source = "registry+https://github.com/rust-lang/crates.io-index"
150 220
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
151 221
152 222
[[package]]
223 +
name = "base64"
224 +
version = "0.23.1"
225 +
source = "registry+https://github.com/rust-lang/crates.io-index"
226 +
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
227 +
228 +
[[package]]
153 229
name = "base64ct"
154 230
version = "1.8.3"
155 231
source = "registry+https://github.com/rust-lang/crates.io-index"
156 232
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
157 233
158 234
[[package]]
235 +
name = "bit-vec"
236 +
version = "0.9.1"
237 +
source = "registry+https://github.com/rust-lang/crates.io-index"
238 +
checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
239 +
dependencies = [
240 +
 "serde",
241 +
]
242 +
243 +
[[package]]
159 244
name = "bitflags"
160 245
version = "2.13.2"
161 246
source = "registry+https://github.com/rust-lang/crates.io-index"
…
208 unchanged lines
370 455
]
371 456
372 457
[[package]]
458 +
name = "data-encoding"
459 +
version = "2.11.1"
460 +
source = "registry+https://github.com/rust-lang/crates.io-index"
461 +
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
462 +
463 +
[[package]]
373 464
name = "der"
374 465
version = "0.8.2"
375 466
source = "registry+https://github.com/rust-lang/crates.io-index"
…
4 unchanged lines
380 471
]
381 472
382 473
[[package]]
474 +
name = "der-parser"
475 +
version = "10.0.0"
476 +
source = "registry+https://github.com/rust-lang/crates.io-index"
477 +
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
478 +
dependencies = [
479 +
 "asn1-rs",
480 +
 "displaydoc",
481 +
 "nom",
482 +
 "num-bigint",
483 +
 "num-traits",
484 +
 "rusticata-macros",
485 +
]
486 +
487 +
[[package]]
488 +
name = "deranged"
489 +
version = "0.5.8"
490 +
source = "registry+https://github.com/rust-lang/crates.io-index"
491 +
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
492 +
493 +
[[package]]
383 494
name = "digest"
384 495
version = "0.11.3"
385 496
source = "registry+https://github.com/rust-lang/crates.io-index"
…
37 unchanged lines
423 534
]
424 535
425 536
[[package]]
537 +
name = "either"
538 +
version = "1.18.0"
539 +
source = "registry+https://github.com/rust-lang/crates.io-index"
540 +
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
541 +
542 +
[[package]]
543 +
name = "equivalent"
544 +
version = "1.0.2"
545 +
source = "registry+https://github.com/rust-lang/crates.io-index"
546 +
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
547 +
548 +
[[package]]
426 549
name = "errno"
427 550
version = "0.3.14"
428 551
source = "registry+https://github.com/rust-lang/crates.io-index"
…
33 unchanged lines
462 585
]
463 586
464 587
[[package]]
588 +
name = "fnv"
589 +
version = "1.0.7"
590 +
source = "registry+https://github.com/rust-lang/crates.io-index"
591 +
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
592 +
593 +
[[package]]
465 594
name = "form_urlencoded"
466 595
version = "1.2.2"
467 596
source = "registry+https://github.com/rust-lang/crates.io-index"
…
3 unchanged lines
471 600
]
472 601
473 602
[[package]]
603 +
name = "fs-err"
604 +
version = "3.3.1"
605 +
source = "registry+https://github.com/rust-lang/crates.io-index"
606 +
checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a"
607 +
dependencies = [
608 +
 "autocfg",
609 +
 "tokio",
610 +
]
611 +
612 +
[[package]]
474 613
name = "futures-channel"
475 614
version = "0.3.34"
476 615
source = "registry+https://github.com/rust-lang/crates.io-index"
…
9 unchanged lines
486 625
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
487 626
488 627
[[package]]
628 +
name = "futures-sink"
629 +
version = "0.3.34"
630 +
source = "registry+https://github.com/rust-lang/crates.io-index"
631 +
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
632 +
633 +
[[package]]
489 634
name = "futures-task"
490 635
version = "0.3.34"
491 636
source = "registry+https://github.com/rust-lang/crates.io-index"
…
48 unchanged lines
540 685
]
541 686
542 687
[[package]]
688 +
name = "h2"
689 +
version = "0.4.19"
690 +
source = "registry+https://github.com/rust-lang/crates.io-index"
691 +
checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
692 +
dependencies = [
693 +
 "atomic-waker",
694 +
 "bytes",
695 +
 "fnv",
696 +
 "futures-core",
697 +
 "futures-sink",
698 +
 "http",
699 +
 "indexmap",
700 +
 "slab",
701 +
 "tokio",
702 +
 "tokio-util",
703 +
 "tracing",
704 +
]
705 +
706 +
[[package]]
707 +
name = "hashbrown"
708 +
version = "0.17.1"
709 +
source = "registry+https://github.com/rust-lang/crates.io-index"
710 +
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
711 +
712 +
[[package]]
543 713
name = "heck"
544 714
version = "0.5.0"
545 715
source = "registry+https://github.com/rust-lang/crates.io-index"
…
70 unchanged lines
616 786
 "bytes",
617 787
 "futures-channel",
618 788
 "futures-core",
789 +
 "h2",
619 790
 "http",
620 791
 "http-body",
621 792
 "httparse",
…
140 unchanged lines
762 933
]
763 934
764 935
[[package]]
936 +
name = "indexmap"
937 +
version = "2.14.2"
938 +
source = "registry+https://github.com/rust-lang/crates.io-index"
939 +
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
940 +
dependencies = [
941 +
 "equivalent",
942 +
 "hashbrown",
943 +
]
944 +
945 +
[[package]]
765 946
name = "is_terminal_polyfill"
766 947
version = "1.70.2"
767 948
source = "registry+https://github.com/rust-lang/crates.io-index"
…
16 unchanged lines
784 965
]
785 966
786 967
[[package]]
968 +
name = "lazy_static"
969 +
version = "1.5.0"
970 +
source = "registry+https://github.com/rust-lang/crates.io-index"
971 +
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
972 +
973 +
[[package]]
787 974
name = "libc"
788 975
version = "0.2.189"
789 976
source = "registry+https://github.com/rust-lang/crates.io-index"
…
36 unchanged lines
826 1013
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
827 1014
828 1015
[[package]]
1016 +
name = "minimal-lexical"
1017 +
version = "0.2.1"
1018 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1019 +
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
1020 +
1021 +
[[package]]
829 1022
name = "miniz_oxide"
830 1023
version = "0.9.1"
831 1024
source = "registry+https://github.com/rust-lang/crates.io-index"
…
42 unchanged lines
874 1067
]
875 1068
876 1069
[[package]]
1070 +
name = "nom"
1071 +
version = "7.1.3"
1072 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1073 +
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
1074 +
dependencies = [
1075 +
 "memchr",
1076 +
 "minimal-lexical",
1077 +
]
1078 +
1079 +
[[package]]
1080 +
name = "num-bigint"
1081 +
version = "0.4.8"
1082 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1083 +
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
1084 +
dependencies = [
1085 +
 "num-integer",
1086 +
 "num-traits",
1087 +
]
1088 +
1089 +
[[package]]
1090 +
name = "num-conv"
1091 +
version = "0.2.2"
1092 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1093 +
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
1094 +
1095 +
[[package]]
1096 +
name = "num-integer"
1097 +
version = "0.1.47"
1098 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1099 +
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
1100 +
dependencies = [
1101 +
 "num-traits",
1102 +
]
1103 +
1104 +
[[package]]
877 1105
name = "num-traits"
878 1106
version = "0.2.19"
879 1107
source = "registry+https://github.com/rust-lang/crates.io-index"
…
3 unchanged lines
883 1111
]
884 1112
885 1113
[[package]]
1114 +
name = "oid-registry"
1115 +
version = "0.8.1"
1116 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1117 +
checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
1118 +
dependencies = [
1119 +
 "asn1-rs",
1120 +
]
1121 +
1122 +
[[package]]
886 1123
name = "once_cell"
887 1124
version = "1.21.4"
888 1125
source = "registry+https://github.com/rust-lang/crates.io-index"
…
27 unchanged lines
916 1153
 "ignore",
917 1154
 "ml-dsa",
918 1155
 "rand",
1156 +
 "rustls",
1157 +
 "rustls-pemfile",
919 1158
 "similar",
920 1159
 "tempfile",
921 -
 "thiserror",
1160 +
 "thiserror 1.0.69",
922 1161
 "ureq",
1162 +
 "webpki-roots 1.0.9",
923 1163
]
924 1164
925 1165
[[package]]
…
1 unchanged lines
927 1167
version = "0.1.0"
928 1168
dependencies = [
929 1169
 "axum",
1170 +
 "axum-server",
930 1171
 "oxy-core",
1172 +
 "rcgen",
1173 +
 "rustls",
931 1174
 "tempfile",
932 1175
 "tokio",
933 1176
]
934 1177
935 1178
[[package]]
1179 +
name = "pem"
1180 +
version = "4.0.0"
1181 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1182 +
checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120"
1183 +
dependencies = [
1184 +
 "base64 0.23.1",
1185 +
 "serde_core",
1186 +
]
1187 +
1188 +
[[package]]
936 1189
name = "percent-encoding"
937 1190
version = "2.3.2"
938 1191
source = "registry+https://github.com/rust-lang/crates.io-index"
…
25 unchanged lines
964 1217
]
965 1218
966 1219
[[package]]
1220 +
name = "powerfmt"
1221 +
version = "0.2.0"
1222 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1223 +
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
1224 +
1225 +
[[package]]
967 1226
name = "ppv-lite86"
968 1227
version = "0.2.21"
969 1228
source = "registry+https://github.com/rust-lang/crates.io-index"
…
63 unchanged lines
1033 1292
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
1034 1293
1035 1294
[[package]]
1295 +
name = "rcgen"
1296 +
version = "0.14.10"
1297 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1298 +
checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8"
1299 +
dependencies = [
1300 +
 "pem",
1301 +
 "ring",
1302 +
 "rustls-pki-types",
1303 +
 "time",
1304 +
 "x509-parser",
1305 +
 "yasna",
1306 +
]
1307 +
1308 +
[[package]]
1036 1309
name = "regex-automata"
1037 1310
version = "0.4.18"
1038 1311
source = "registry+https://github.com/rust-lang/crates.io-index"
…
34 unchanged lines
1073 1346
]
1074 1347
1075 1348
[[package]]
1349 +
name = "rusticata-macros"
1350 +
version = "4.1.0"
1351 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1352 +
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
1353 +
dependencies = [
1354 +
 "nom",
1355 +
]
1356 +
1357 +
[[package]]
1076 1358
name = "rustix"
1077 1359
version = "1.1.5"
1078 1360
source = "registry+https://github.com/rust-lang/crates.io-index"
…
22 unchanged lines
1101 1383
]
1102 1384
1103 1385
[[package]]
1386 +
name = "rustls-pemfile"
1387 +
version = "2.2.0"
1388 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1389 +
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
1390 +
dependencies = [
1391 +
 "rustls-pki-types",
1392 +
]
1393 +
1394 +
[[package]]
1104 1395
name = "rustls-pki-types"
1105 1396
version = "1.15.1"
1106 1397
source = "registry+https://github.com/rust-lang/crates.io-index"
…
14 unchanged lines
1121 1412
]
1122 1413
1123 1414
[[package]]
1415 +
name = "rustversion"
1416 +
version = "1.0.23"
1417 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1418 +
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
1419 +
1420 +
[[package]]
1124 1421
name = "ryu"
1125 1422
version = "1.0.23"
1126 1423
source = "registry+https://github.com/rust-lang/crates.io-index"
…
21 unchanged lines
1148 1445
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
1149 1446
dependencies = [
1150 1447
 "serde_core",
1448 +
 "serde_derive",
1151 1449
]
1152 1450
1153 1451
[[package]]
…
188 unchanged lines
1342 1640
1343 1641
[[package]]
1344 1642
name = "synstructure"
1643 +
version = "0.13.2"
1644 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1645 +
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
1646 +
dependencies = [
1647 +
 "proc-macro2",
1648 +
 "quote",
1649 +
 "syn 2.0.119",
1650 +
]
1651 +
1652 +
[[package]]
1653 +
name = "synstructure"
1345 1654
version = "0.14.0"
1346 1655
source = "registry+https://github.com/rust-lang/crates.io-index"
1347 1656
checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
…
22 unchanged lines
1370 1679
source = "registry+https://github.com/rust-lang/crates.io-index"
1371 1680
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
1372 1681
dependencies = [
1373 -
 "thiserror-impl",
1682 +
 "thiserror-impl 1.0.69",
1374 1683
]
1375 1684
1376 1685
[[package]]
1686 +
name = "thiserror"
1687 +
version = "2.0.21"
1688 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1689 +
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
1690 +
dependencies = [
1691 +
 "thiserror-impl 2.0.21",
1692 +
]
1693 +
1694 +
[[package]]
1377 1695
name = "thiserror-impl"
1378 1696
version = "1.0.69"
1379 1697
source = "registry+https://github.com/rust-lang/crates.io-index"
…
5 unchanged lines
1385 1703
]
1386 1704
1387 1705
[[package]]
1706 +
name = "thiserror-impl"
1707 +
version = "2.0.21"
1708 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1709 +
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
1710 +
dependencies = [
1711 +
 "proc-macro2",
1712 +
 "quote",
1713 +
 "syn 3.0.6",
1714 +
]
1715 +
1716 +
[[package]]
1717 +
name = "time"
1718 +
version = "0.3.55"
1719 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1720 +
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
1721 +
dependencies = [
1722 +
 "deranged",
1723 +
 "num-conv",
1724 +
 "powerfmt",
1725 +
 "serde_core",
1726 +
 "time-core",
1727 +
 "time-macros",
1728 +
]
1729 +
1730 +
[[package]]
1731 +
name = "time-core"
1732 +
version = "0.1.9"
1733 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1734 +
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
1735 +
1736 +
[[package]]
1737 +
name = "time-macros"
1738 +
version = "0.2.32"
1739 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1740 +
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
1741 +
dependencies = [
1742 +
 "num-conv",
1743 +
 "time-core",
1744 +
]
1745 +
1746 +
[[package]]
1388 1747
name = "tinystr"
1389 1748
version = "0.8.4"
1390 1749
source = "registry+https://github.com/rust-lang/crates.io-index"
…
9 unchanged lines
1400 1759
source = "registry+https://github.com/rust-lang/crates.io-index"
1401 1760
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
1402 1761
dependencies = [
1762 +
 "bytes",
1403 1763
 "libc",
1404 1764
 "mio",
1405 1765
 "pin-project-lite",
…
14 unchanged lines
1420 1780
]
1421 1781
1422 1782
[[package]]
1783 +
name = "tokio-rustls"
1784 +
version = "0.26.5"
1785 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1786 +
checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
1787 +
dependencies = [
1788 +
 "rustls",
1789 +
 "tokio",
1790 +
]
1791 +
1792 +
[[package]]
1793 +
name = "tokio-util"
1794 +
version = "0.7.19"
1795 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1796 +
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
1797 +
dependencies = [
1798 +
 "bytes",
1799 +
 "futures-core",
1800 +
 "futures-sink",
1801 +
 "libc",
1802 +
 "pin-project-lite",
1803 +
 "tokio",
1804 +
]
1805 +
1806 +
[[package]]
1423 1807
name = "tower"
1424 1808
version = "0.5.3"
1425 1809
source = "registry+https://github.com/rust-lang/crates.io-index"
…
65 unchanged lines
1491 1875
source = "registry+https://github.com/rust-lang/crates.io-index"
1492 1876
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
1493 1877
dependencies = [
1494 -
 "base64",
1878 +
 "base64 0.22.1",
1495 1879
 "flate2",
1496 1880
 "log",
1497 1881
 "once_cell",
…
165 unchanged lines
1663 2047
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
1664 2048
1665 2049
[[package]]
2050 +
name = "x509-parser"
2051 +
version = "0.18.1"
2052 +
source = "registry+https://github.com/rust-lang/crates.io-index"
2053 +
checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
2054 +
dependencies = [
2055 +
 "asn1-rs",
2056 +
 "data-encoding",
2057 +
 "der-parser",
2058 +
 "lazy_static",
2059 +
 "nom",
2060 +
 "oid-registry",
2061 +
 "ring",
2062 +
 "rusticata-macros",
2063 +
 "thiserror 2.0.21",
2064 +
 "time",
2065 +
]
2066 +
2067 +
[[package]]
2068 +
name = "yasna"
2069 +
version = "0.6.0"
2070 +
source = "registry+https://github.com/rust-lang/crates.io-index"
2071 +
checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282"
2072 +
dependencies = [
2073 +
 "bit-vec",
2074 +
 "time",
2075 +
]
2076 +
2077 +
[[package]]
1666 2078
name = "yoke"
1667 2079
version = "0.8.3"
1668 2080
source = "registry+https://github.com/rust-lang/crates.io-index"
…
13 unchanged lines
1682 2094
 "proc-macro2",
1683 2095
 "quote",
1684 2096
 "syn 3.0.6",
1685 -
 "synstructure",
2097 +
 "synstructure 0.14.0",
1686 2098
]
1687 2099
1688 2100
[[package]]
…
34 unchanged lines
1723 2135
 "proc-macro2",
1724 2136
 "quote",
1725 2137
 "syn 3.0.6",
1726 -
 "synstructure",
2138 +
 "synstructure 0.14.0",
1727 2139
]
1728 2140
1729 2141
[[package]]
…
49 unchanged lines
modified crates/oxy-core/Cargo.toml +3 −0
…
14 unchanged lines
15 15
ml-dsa = "0.1"
16 16
ed25519-dalek = "3"
17 17
ureq = "2"
18 +
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
19 +
rustls-pemfile = "2"
20 +
webpki-roots = "1"
18 21
19 22
[dev-dependencies]
20 23
tempfile = "3"
modified crates/oxy-core/src/protocol.rs +41 −5
…
11 unchanged lines
12 12
use crate::identity::Signature;
13 13
use crate::objects::{self as o, Change, ObjectError};
14 14
use crate::repo::{Repo, RepoError, Result};
15 -
use std::path::Path;
15 +
use std::path::{Path, PathBuf};
16 +
use std::sync::Arc;
16 17
17 18
pub const CONTENT_TYPE: &str = "application/oxy-cbor";
18 19
pub const PROTOCOL_VERSION: u64 = 1;
…
90 unchanged lines
109 110
110 111
// ---------------------------------------------------------------- http
111 112
113 +
/// How to reach a remote: credentials and trust.
114 +
#[derive(Clone, Debug, Default)]
115 +
pub struct RemoteOptions {
116 +
    /// Bearer token for pushes.
117 +
    pub token: Option<String>,
118 +
    /// PEM file with extra root certificates to trust for `https://` remotes
119 +
    /// (self-hosted servers); the system web roots are always trusted.
120 +
    pub ca: Option<PathBuf>,
121 +
}
122 +
112 123
/// A remote served by `oxy-server` at `<base>/oxy/v1/...`.
113 124
pub struct HttpRemote {
114 125
    base: String,
…
1 unchanged lines
116 127
    agent: ureq::Agent,
117 128
}
118 129
130 +
/// A TLS client configuration trusting the web roots plus `ca`, if given.
131 +
fn tls_client_config(ca: Option<&Path>) -> Result<Arc<rustls::ClientConfig>> {
132 +
    let mut roots = rustls::RootCertStore::empty();
133 +
    roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
134 +
    if let Some(path) = ca {
135 +
        let pem = std::fs::read(path).map_err(|e| RepoError::Msg(format!("reading CA file {}: {e}", path.display())))?;
136 +
        let certs = rustls_pemfile::certs(&mut pem.as_slice())
137 +
            .collect::<std::result::Result<Vec<_>, _>>()
138 +
            .map_err(|e| RepoError::Msg(format!("parsing CA file {}: {e}", path.display())))?;
139 +
        if certs.is_empty() {
140 +
            return Err(RepoError::Msg(format!("CA file {} holds no certificates", path.display())));
141 +
        }
142 +
        for c in certs {
143 +
            roots.add(c).map_err(|e| RepoError::Msg(format!("CA file {}: {e}", path.display())))?;
144 +
        }
145 +
    }
146 +
    let cfg = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
147 +
        .with_safe_default_protocol_versions()
148 +
        .map_err(|e| RepoError::Msg(e.to_string()))?
149 +
        .with_root_certificates(roots)
150 +
        .with_no_client_auth();
151 +
    Ok(Arc::new(cfg))
152 +
}
153 +
119 154
impl HttpRemote {
120 -
    pub fn new(base: &str, token: Option<String>) -> HttpRemote {
121 -
        HttpRemote { base: base.trim_end_matches('/').to_string(), token, agent: ureq::Agent::new() }
155 +
    pub fn new(base: &str, options: &RemoteOptions) -> Result<HttpRemote> {
156 +
        let agent = ureq::AgentBuilder::new().tls_config(tls_client_config(options.ca.as_deref())?).build();
157 +
        Ok(HttpRemote { base: base.trim_end_matches('/').to_string(), token: options.token.clone(), agent })
122 158
    }
123 159
124 160
    fn call(&self, post: bool, endpoint: &str, body: Option<&[u8]>) -> Result<Vec<u8>> {
…
41 unchanged lines
166 202
}
167 203
168 204
/// Open a remote by URL: a local path (optionally `file://`), or `http(s)://`.
169 -
pub fn open_remote(url: &str, token: Option<String>) -> Result<Box<dyn Remote>> {
205 +
pub fn open_remote(url: &str, options: &RemoteOptions) -> Result<Box<dyn Remote>> {
170 206
    if url.starts_with("http://") || url.starts_with("https://") {
171 -
        return Ok(Box::new(HttpRemote::new(url, token)));
207 +
        return Ok(Box::new(HttpRemote::new(url, options)?));
172 208
    }
173 209
    let path = url.strip_prefix("file://").unwrap_or(url);
174 210
    if path.contains("://") {
…
288 unchanged lines
modified crates/oxy-core/src/repo/config.rs +15 −6
…
20 unchanged lines
21 21
    pub remotes: BTreeMap<String, String>,
22 22
    /// `remote.<name>.token`: bearer token for HTTP pushes.
23 23
    pub remote_tokens: BTreeMap<String, String>,
24 +
    /// `remote.<name>.ca`: PEM file of extra roots to trust for that remote.
25 +
    pub remote_cas: BTreeMap<String, String>,
24 26
}
25 27
26 28
impl Config {
…
8 unchanged lines
35 37
            bookmark: "main".into(),
36 38
            remotes: BTreeMap::new(),
37 39
            remote_tokens: BTreeMap::new(),
40 +
            remote_cas: BTreeMap::new(),
38 41
        }
39 42
    }
40 43
…
43 unchanged lines
84 87
                }
85 88
            }
86 89
            "workspace.bookmark" => self.bookmark = value.into(),
87 -
            k if k.starts_with("remote.") && (k.ends_with(".url") || k.ends_with(".token")) => {
88 -
                let (name, is_url) = match k.strip_suffix(".url") {
89 -
                    Some(n) => (&n["remote.".len()..], true),
90 -
                    None => (&k["remote.".len()..k.len() - ".token".len()], false),
90 +
            k if k.starts_with("remote.") => {
91 +
                let rest = &k["remote.".len()..];
92 +
                let Some((name, field)) = rest.rsplit_once('.') else {
93 +
                    return fail(format!("unknown configuration key {key:?}"));
91 94
                };
92 95
                if name.is_empty() || name.contains('/') {
93 96
                    return fail(format!("invalid remote name in {key:?}"));
94 97
                }
95 -
                let map = if is_url { &mut self.remotes } else { &mut self.remote_tokens };
98 +
                let map = match field {
99 +
                    "url" => &mut self.remotes,
100 +
                    "token" => &mut self.remote_tokens,
101 +
                    "ca" => &mut self.remote_cas,
102 +
                    _ => return fail(format!("unknown configuration key {key:?}")),
103 +
                };
96 104
                if value.is_empty() {
97 105
                    map.remove(name);
98 -
                    if is_url {
106 +
                    if field == "url" {
99 107
                        self.remote_tokens.remove(name);
108 +
                        self.remote_cas.remove(name);
100 109
                    }
101 110
                } else {
102 111
                    map.insert(name.to_string(), value.into());
…
57 unchanged lines
modified crates/oxy-core/src/repo/mod.rs +1 −1
…
7 unchanged lines
8 8
9 9
use crate::attest::{Attest, AttestBody};
10 10
use crate::diff::{ranges, split_lines, text_edits};
11 -
use crate::protocol::{open_remote, Bundle, RefResult, RefUpdate, RemoteRef, SignedAttest};
11 +
use crate::protocol::{open_remote, Bundle, RefResult, RefUpdate, RemoteOptions, RemoteRef, SignedAttest};
12 12
use crate::hash::{hash_object, ChangeHash, KeyId, ObjectRef, ObjectType};
13 13
use crate::identity::{verify as verify_signature, Domain, Identity, SecretKey, Signature};
14 14
use crate::ids::{ChangeId, NodeId, WorkspaceId};
…
543 unchanged lines
modified crates/oxy-core/src/repo/sync.rs +13 −5
…
279 unchanged lines
280 280
        self.config.remotes.get(name).cloned().ok_or_else(|| RepoError::Msg(format!("no remote named {name:?}; add one with `oxy remote add`")))
281 281
    }
282 282
283 -
    /// Token for a remote: `remote.<name>.token`, else `$OXY_TOKEN`.
284 -
    fn remote_token(&self, name: &str) -> Option<String> {
285 -
        self.config.remote_tokens.get(name).cloned().or_else(|| std::env::var("OXY_TOKEN").ok())
283 +
    /// Credentials and trust for a remote: `remote.<name>.token` (else
284 +
    /// `$OXY_TOKEN`) and `remote.<name>.ca` (else `$OXY_CA`).
285 +
    fn remote_options(&self, name: &str) -> RemoteOptions {
286 +
        RemoteOptions {
287 +
            token: self.config.remote_tokens.get(name).cloned().or_else(|| std::env::var("OXY_TOKEN").ok()),
288 +
            ca: self.config.remote_cas.get(name).map(PathBuf::from).or_else(|| std::env::var("OXY_CA").ok().map(PathBuf::from)),
289 +
        }
286 290
    }
287 291
288 292
    fn open_named_remote(&self, name: &str) -> Result<Box<dyn crate::protocol::Remote>> {
289 -
        open_remote(&self.remote_url(name)?, self.remote_token(name))
293 +
        open_remote(&self.remote_url(name)?, &self.remote_options(name))
290 294
    }
291 295
292 296
    fn serial_path(&self, remote: &str, bookmark: &str) -> PathBuf {
…
185 unchanged lines
478 482
    /// Clone a remote into `dest`: same repository ID, `origin` remote,
479 483
    /// bookmark set to the remote's, working copy checked out.
480 484
    pub fn clone(url: &str, dest: &Path) -> Result<Repo> {
481 -
        let remote = open_remote(url, std::env::var("OXY_TOKEN").ok())?;
485 +
        let options = RemoteOptions {
486 +
            token: std::env::var("OXY_TOKEN").ok(),
487 +
            ca: std::env::var("OXY_CA").ok().map(PathBuf::from),
488 +
        };
489 +
        let remote = open_remote(url, &options)?;
482 490
        let id = remote.repo_id()?;
483 491
        let mut repo = Repo::init_with_id(dest, id)?;
484 492
        repo.config.set("remote.origin.url", url)?;
…
14 unchanged lines
modified crates/oxy-server/Cargo.toml +3 −0
…
8 unchanged lines
9 9
oxy-core = { path = "../oxy-core" }
10 10
axum = "0.8"
11 11
tokio = { version = "1", features = ["rt-multi-thread", "net", "macros", "sync"] }
12 +
axum-server = { version = "0.8", default-features = false, features = ["tls-rustls-no-provider"] }
13 +
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
12 14
13 15
[dev-dependencies]
16 +
rcgen = "0.14"
14 17
tempfile = "3"
modified crates/oxy-server/src/lib.rs +26 −6
1 1
//! Reference HTTP server for the Oxygen wire protocol (spec §15.1, §16).
2 2
//!
3 -
//! Serves every repository directory under a root at `/{repo}/oxy/v1/...`.
4 -
//! TLS is expected to be terminated in front of this server for now; the
5 -
//! protocol's integrity does not depend on the channel (spec §15).
3 +
//! Serves every repository directory under a root at `/{repo}/oxy/v1/...`,
4 +
//! over TLS when a certificate and key are given, else plain HTTP for use
5 +
//! behind a TLS-terminating proxy or on a loopback interface (spec §15.1).
6 6
//!
7 7
//! Authentication: when bearer tokens are configured, `push` requires one;
8 8
//! reads are open. With no tokens configured everything is open (development).
…
16 unchanged lines
25 25
    pub root: PathBuf,
26 26
    /// Bearer tokens allowed to push; empty means no authentication.
27 27
    pub tokens: Vec<String>,
28 +
    /// Serve HTTPS with this certificate chain and private key (PEM files).
29 +
    pub tls: Option<TlsFiles>,
28 30
}
29 31
32 +
#[derive(Clone, Debug)]
33 +
pub struct TlsFiles {
34 +
    pub cert: PathBuf,
35 +
    pub key: PathBuf,
36 +
}
37 +
30 38
struct App {
31 39
    cfg: ServerConfig,
32 40
    /// One lock per repository so concurrent pushes serialize.
…
119 unchanged lines
152 160
        .with_state(app)
153 161
}
154 162
155 -
/// Serve on an already-bound listener until the process ends.
163 +
/// Serve on an already-bound listener until the process ends; HTTPS when
164 +
/// `cfg.tls` is set.
156 165
pub async fn serve(listener: std::net::TcpListener, cfg: ServerConfig) -> std::io::Result<()> {
157 166
    listener.set_nonblocking(true)?;
158 -
    let listener = tokio::net::TcpListener::from_std(listener)?;
159 -
    axum::serve(listener, router(cfg)).await
167 +
    match cfg.tls.clone() {
168 +
        Some(tls) => {
169 +
            let _ = rustls::crypto::ring::default_provider().install_default();
170 +
            let rustls_cfg = axum_server::tls_rustls::RustlsConfig::from_pem_file(&tls.cert, &tls.key)
171 +
                .await
172 +
                .map_err(|e| std::io::Error::other(format!("loading TLS files: {e}")))?;
173 +
            axum_server::from_tcp_rustls(listener, rustls_cfg)?.serve(router(cfg).into_make_service()).await
174 +
        }
175 +
        None => {
176 +
            let listener = tokio::net::TcpListener::from_std(listener)?;
177 +
            axum::serve(listener, router(cfg)).await
178 +
        }
179 +
    }
160 180
}
161 181
162 182
/// Build a runtime and serve; for the CLI and for tests on a background thread.
…
4 unchanged lines
modified crates/oxy-server/tests/http.rs +52 −2
…
23 unchanged lines
24 24
}
25 25
26 26
fn start(root: &Path, tokens: Vec<String>) -> String {
27 +
    start_with(root, tokens, None)
28 +
}
29 +
30 +
fn start_with(root: &Path, tokens: Vec<String>, tls: Option<oxy_server::TlsFiles>) -> String {
27 31
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
28 32
    let port = listener.local_addr().unwrap().port();
29 -
    let cfg = oxy_server::ServerConfig { root: root.to_path_buf(), tokens };
33 +
    let scheme = if tls.is_some() { "https" } else { "http" };
34 +
    let cfg = oxy_server::ServerConfig { root: root.to_path_buf(), tokens, tls };
30 35
    std::thread::spawn(move || oxy_server::run_blocking(listener, cfg).unwrap());
31 -
    format!("http://127.0.0.1:{port}")
36 +
    // Give the acceptor a moment; the first request otherwise races the bind.
37 +
    std::thread::sleep(std::time::Duration::from_millis(50));
38 +
    format!("{scheme}://127.0.0.1:{port}")
32 39
}
33 40
41 +
/// A self-signed certificate for 127.0.0.1 written to `dir`: (cert, key).
42 +
fn self_signed(dir: &Path) -> (std::path::PathBuf, std::path::PathBuf) {
43 +
    fs::create_dir_all(dir).unwrap();
44 +
    let ck = rcgen::generate_simple_self_signed(vec!["127.0.0.1".into(), "localhost".into()]).unwrap();
45 +
    let cert = dir.join("cert.pem");
46 +
    let key = dir.join("key.pem");
47 +
    fs::write(&cert, ck.cert.pem()).unwrap();
48 +
    fs::write(&key, ck.signing_key.serialize_pem()).unwrap();
49 +
    (cert, key)
50 +
}
51 +
34 52
#[test]
35 53
fn clone_push_pull_over_http() {
36 54
    hermetic();
…
64 unchanged lines
101 119
    let origin = Repo::open(&served.join("origin")).unwrap();
102 120
    assert_eq!(origin.bookmark_heads().unwrap().len(), 1);
103 121
}
122 +
123 +
#[test]
124 +
fn https_with_a_self_signed_certificate() {
125 +
    hermetic();
126 +
    let dir = tempfile::tempdir().unwrap();
127 +
    let root = dir.path();
128 +
    let served = root.join("served");
129 +
    Repo::init(&served.join("origin")).unwrap();
130 +
    let (cert, key) = self_signed(root);
131 +
    let base = start_with(&served, vec![], Some(oxy_server::TlsFiles { cert: cert.clone(), key }));
132 +
    let url = format!("{base}/origin");
133 +
134 +
    // Without trusting the certificate the clone fails; with `OXY_CA` it works.
135 +
    let err = Repo::clone(&url, &root.join("untrusted")).err().unwrap().to_string();
136 +
    assert!(err.contains("certificate") || err.contains("tls") || err.contains("TLS") || err.contains("handshake"), "{err}");
137 +
    std::env::set_var("OXY_CA", &cert);
138 +
    let mut a = Repo::clone(&url, &root.join("a")).unwrap();
139 +
    std::env::remove_var("OXY_CA");
140 +
    // Persist the trust for this remote so later commands need no env var.
141 +
    a.config.set("remote.origin.ca", cert.to_str().unwrap()).unwrap();
142 +
    a.save_config().unwrap();
143 +
    write(&root.join("a"), "f.txt", b"tls\n");
144 +
    let h = a.commit("over tls", &[]).unwrap();
145 +
    assert!(a.push("origin", false).unwrap()[0].ok);
146 +
    let origin = Repo::open(&served.join("origin")).unwrap();
147 +
    assert_eq!(origin.bookmark_heads().unwrap(), vec![h]);
148 +
    // A wrong CA is refused.
149 +
    let (other_cert, _) = self_signed(&root.join("other"));
150 +
    a.config.set("remote.origin.ca", other_cert.to_str().unwrap()).unwrap();
151 +
    a.save_config().unwrap();
152 +
    assert!(a.fetch("origin").is_err());
153 +
}
modified src/cli/mod.rs +9 −3
…
71 unchanged lines
72 72
        #[arg(long)]
73 73
        force: bool,
74 74
    },
75 -
    /// Serve repositories under ROOT over HTTP (spec §16); TLS is terminated in front
75 +
    /// Serve repositories under ROOT over HTTPS (with --tls-cert/--tls-key) or plain HTTP
76 76
    Serve {
77 77
        /// Directory with one repository per subdirectory
78 78
        #[arg(long, default_value = ".")]
…
3 unchanged lines
82 82
        /// Bearer token allowed to push (repeatable); none means open
83 83
        #[arg(long = "token")]
84 84
        tokens: Vec<String>,
85 +
        /// PEM certificate chain; enables HTTPS together with --tls-key
86 +
        #[arg(long, requires = "tls_key")]
87 +
        tls_cert: Option<PathBuf>,
88 +
        /// PEM private key
89 +
        #[arg(long, requires = "tls_cert")]
90 +
        tls_key: Option<PathBuf>,
85 91
    },
86 92
    /// Manage the signing identity
87 93
    Identity {
…
2 unchanged lines
90 96
    },
91 97
    /// Verify the signatures on a change (default: every change in the log)
92 98
    Verify { change: Option<String> },
93 -
    /// Set a configuration key (author.*, core.sign, workspace.bookmark, remote.<name>.url|token)
99 +
    /// Set a configuration key (author.*, core.sign, workspace.bookmark, remote.<name>.url|token|ca)
94 100
    Config {
95 101
        key: String,
96 102
        value: String,
…
59 unchanged lines
156 162
        Cmd::Fetch { remote } => sync::fetch(remote.as_deref()),
157 163
        Cmd::Pull { remote } => sync::pull(remote.as_deref()),
158 164
        Cmd::Push { remote, force } => sync::push(remote.as_deref(), force),
159 -
        Cmd::Serve { root, bind, tokens } => sync::serve(root, &bind, tokens),
165 +
        Cmd::Serve { root, bind, tokens, tls_cert, tls_key } => sync::serve(root, &bind, tokens, tls_cert.zip(tls_key)),
160 166
        Cmd::Identity { cmd } => match cmd {
161 167
            IdentityCmd::Create { name, email, local } => identity::create(name, email, local),
162 168
            IdentityCmd::Show => identity::show(),
…
5 unchanged lines
modified src/cli/sync.rs +8 −3
…
96 unchanged lines
97 97
    Ok(code)
98 98
}
99 99
100 -
pub fn serve(root: PathBuf, bind: &str, tokens: Vec<String>) -> Result<i32> {
100 +
pub fn serve(root: PathBuf, bind: &str, tokens: Vec<String>, tls: Option<(PathBuf, PathBuf)>) -> Result<i32> {
101 101
    let listener = std::net::TcpListener::bind(bind).with_context(|| format!("binding {bind}"))?;
102 102
    let root = root.canonicalize()?;
103 +
    let tls = tls.map(|(cert, key)| oxy_server::TlsFiles { cert, key });
103 104
    println!(
104 -
        "Serving repositories under {} on http://{bind}/<repo>/oxy/v1/ ({})",
105 +
        "Serving repositories under {} on {}://{bind}/<repo>/oxy/v1/ ({})",
105 106
        root.display(),
107 +
        if tls.is_some() { "https" } else { "http" },
106 108
        if tokens.is_empty() { "open: no push tokens configured" } else { "push requires a token" }
107 109
    );
108 -
    oxy_server::run_blocking(listener, oxy_server::ServerConfig { root, tokens })?;
110 +
    if tls.is_none() && !bind.starts_with("127.") && !bind.starts_with("localhost") && !bind.starts_with("[::1]") {
111 +
        eprintln!("warning: serving plain HTTP on a non-loopback address; put a TLS terminator in front or pass --tls-cert/--tls-key");
112 +
    }
113 +
    oxy_server::run_blocking(listener, oxy_server::ServerConfig { root, tokens, tls })?;
109 114
    Ok(0)
110 115
}