├── .github
├── readme-screenshot.png
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.lock
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── app
├── CHANGELOG.md
├── Cargo.toml
└── src
│ ├── args.rs
│ ├── main.rs
│ └── server.rs
└── lib
├── CHANGELOG.md
├── Cargo.toml
├── build.rs
├── examples
└── main.rs
├── package.json
├── src
├── assets
│ ├── dir-listing.html
│ ├── not-found.html
│ └── proxy-error.html
├── browser.ts
├── config.rs
├── generated
│ └── browser.js
├── inject.rs
├── lib.rs
├── serve
│ ├── fs.rs
│ ├── mod.rs
│ └── proxy.rs
├── tests.rs
├── util.rs
└── ws.rs
└── tsconfig.json
/.github/readme-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LukasKalbertodt/penguin/aa8a4b9b5eb2c58b186d0fee6d1cc0f37173b80b/.github/readme-screenshot.png
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [ main ]
7 |
8 | env:
9 | CARGO_TERM_COLOR: always
10 | RUSTFLAGS: --deny warnings
11 |
12 | jobs:
13 | check:
14 | name: 'Build & test'
15 | runs-on: ubuntu-20.04
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Restore cargo cache
19 | uses: Swatinem/rust-cache@v1.3.0
20 | - name: Install tsc
21 | run: npm install
22 | working-directory: lib
23 | - name: Compile TS file
24 | run: npx tsc --outFile src/generated/check.js
25 | working-directory: lib
26 | - name: Make sure generated JS file is up to date
27 | run: diff -u --color lib/src/generated/browser.js lib/src/generated/check.js
28 | - name: Build
29 | run: cargo build
30 | - name: Run tests
31 | run: cargo test
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /lib/node_modules
3 | /lib/package-lock.json
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | - [Changelog for the library](./lib/CHANGELOG.md)
4 | - [Changelog for the CLI app](./app/CHANGELOG.md)
5 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "adler"
7 | version = "1.0.2"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
10 |
11 | [[package]]
12 | name = "aho-corasick"
13 | version = "0.7.18"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
16 | dependencies = [
17 | "memchr",
18 | ]
19 |
20 | [[package]]
21 | name = "alloc-no-stdlib"
22 | version = "2.0.3"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3"
25 |
26 | [[package]]
27 | name = "alloc-stdlib"
28 | version = "0.2.1"
29 | source = "registry+https://github.com/rust-lang/crates.io-index"
30 | checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2"
31 | dependencies = [
32 | "alloc-no-stdlib",
33 | ]
34 |
35 | [[package]]
36 | name = "ansi_term"
37 | version = "0.12.1"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
40 | dependencies = [
41 | "winapi 0.3.9",
42 | ]
43 |
44 | [[package]]
45 | name = "anyhow"
46 | version = "1.0.57"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
49 |
50 | [[package]]
51 | name = "atty"
52 | version = "0.2.14"
53 | source = "registry+https://github.com/rust-lang/crates.io-index"
54 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
55 | dependencies = [
56 | "hermit-abi",
57 | "libc",
58 | "winapi 0.3.9",
59 | ]
60 |
61 | [[package]]
62 | name = "autocfg"
63 | version = "1.1.0"
64 | source = "registry+https://github.com/rust-lang/crates.io-index"
65 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
66 |
67 | [[package]]
68 | name = "base64"
69 | version = "0.13.0"
70 | source = "registry+https://github.com/rust-lang/crates.io-index"
71 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
72 |
73 | [[package]]
74 | name = "bitflags"
75 | version = "1.3.2"
76 | source = "registry+https://github.com/rust-lang/crates.io-index"
77 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
78 |
79 | [[package]]
80 | name = "block-buffer"
81 | version = "0.10.2"
82 | source = "registry+https://github.com/rust-lang/crates.io-index"
83 | checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
84 | dependencies = [
85 | "generic-array",
86 | ]
87 |
88 | [[package]]
89 | name = "brotli"
90 | version = "3.3.4"
91 | source = "registry+https://github.com/rust-lang/crates.io-index"
92 | checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
93 | dependencies = [
94 | "alloc-no-stdlib",
95 | "alloc-stdlib",
96 | "brotli-decompressor",
97 | ]
98 |
99 | [[package]]
100 | name = "brotli-decompressor"
101 | version = "2.3.2"
102 | source = "registry+https://github.com/rust-lang/crates.io-index"
103 | checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80"
104 | dependencies = [
105 | "alloc-no-stdlib",
106 | "alloc-stdlib",
107 | ]
108 |
109 | [[package]]
110 | name = "bunt"
111 | version = "0.2.6"
112 | source = "registry+https://github.com/rust-lang/crates.io-index"
113 | checksum = "192bac6c13e04373feb683e4438cbc156e6bbe432f614a9d6247445108fc5551"
114 | dependencies = [
115 | "bunt-macros",
116 | "termcolor",
117 | ]
118 |
119 | [[package]]
120 | name = "bunt-macros"
121 | version = "0.2.5"
122 | source = "registry+https://github.com/rust-lang/crates.io-index"
123 | checksum = "181ae31bbb8b46f840a70dc1323ad938f2cd7a504e7adc98d6fe58094900a680"
124 | dependencies = [
125 | "litrs",
126 | "proc-macro2",
127 | "quote",
128 | "unicode-xid",
129 | ]
130 |
131 | [[package]]
132 | name = "byteorder"
133 | version = "1.4.3"
134 | source = "registry+https://github.com/rust-lang/crates.io-index"
135 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
136 |
137 | [[package]]
138 | name = "bytes"
139 | version = "1.1.0"
140 | source = "registry+https://github.com/rust-lang/crates.io-index"
141 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
142 |
143 | [[package]]
144 | name = "cc"
145 | version = "1.0.73"
146 | source = "registry+https://github.com/rust-lang/crates.io-index"
147 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
148 |
149 | [[package]]
150 | name = "cfb"
151 | version = "0.7.3"
152 | source = "registry+https://github.com/rust-lang/crates.io-index"
153 | checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
154 | dependencies = [
155 | "byteorder",
156 | "fnv",
157 | "uuid",
158 | ]
159 |
160 | [[package]]
161 | name = "cfg-if"
162 | version = "0.1.10"
163 | source = "registry+https://github.com/rust-lang/crates.io-index"
164 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
165 |
166 | [[package]]
167 | name = "cfg-if"
168 | version = "1.0.0"
169 | source = "registry+https://github.com/rust-lang/crates.io-index"
170 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
171 |
172 | [[package]]
173 | name = "clap"
174 | version = "2.34.0"
175 | source = "registry+https://github.com/rust-lang/crates.io-index"
176 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
177 | dependencies = [
178 | "ansi_term",
179 | "atty",
180 | "bitflags",
181 | "strsim",
182 | "textwrap",
183 | "unicode-width",
184 | "vec_map",
185 | ]
186 |
187 | [[package]]
188 | name = "core-foundation"
189 | version = "0.9.3"
190 | source = "registry+https://github.com/rust-lang/crates.io-index"
191 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
192 | dependencies = [
193 | "core-foundation-sys",
194 | "libc",
195 | ]
196 |
197 | [[package]]
198 | name = "core-foundation-sys"
199 | version = "0.8.3"
200 | source = "registry+https://github.com/rust-lang/crates.io-index"
201 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
202 |
203 | [[package]]
204 | name = "cpufeatures"
205 | version = "0.2.2"
206 | source = "registry+https://github.com/rust-lang/crates.io-index"
207 | checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
208 | dependencies = [
209 | "libc",
210 | ]
211 |
212 | [[package]]
213 | name = "crc32fast"
214 | version = "1.3.2"
215 | source = "registry+https://github.com/rust-lang/crates.io-index"
216 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
217 | dependencies = [
218 | "cfg-if 1.0.0",
219 | ]
220 |
221 | [[package]]
222 | name = "crypto-common"
223 | version = "0.1.3"
224 | source = "registry+https://github.com/rust-lang/crates.io-index"
225 | checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
226 | dependencies = [
227 | "generic-array",
228 | "typenum",
229 | ]
230 |
231 | [[package]]
232 | name = "digest"
233 | version = "0.10.3"
234 | source = "registry+https://github.com/rust-lang/crates.io-index"
235 | checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
236 | dependencies = [
237 | "block-buffer",
238 | "crypto-common",
239 | ]
240 |
241 | [[package]]
242 | name = "env_logger"
243 | version = "0.7.1"
244 | source = "registry+https://github.com/rust-lang/crates.io-index"
245 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
246 | dependencies = [
247 | "atty",
248 | "humantime",
249 | "log",
250 | "regex",
251 | "termcolor",
252 | ]
253 |
254 | [[package]]
255 | name = "fastrand"
256 | version = "1.7.0"
257 | source = "registry+https://github.com/rust-lang/crates.io-index"
258 | checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
259 | dependencies = [
260 | "instant",
261 | ]
262 |
263 | [[package]]
264 | name = "filetime"
265 | version = "0.2.16"
266 | source = "registry+https://github.com/rust-lang/crates.io-index"
267 | checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c"
268 | dependencies = [
269 | "cfg-if 1.0.0",
270 | "libc",
271 | "redox_syscall",
272 | "winapi 0.3.9",
273 | ]
274 |
275 | [[package]]
276 | name = "flate2"
277 | version = "1.0.24"
278 | source = "registry+https://github.com/rust-lang/crates.io-index"
279 | checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
280 | dependencies = [
281 | "crc32fast",
282 | "miniz_oxide",
283 | ]
284 |
285 | [[package]]
286 | name = "fnv"
287 | version = "1.0.7"
288 | source = "registry+https://github.com/rust-lang/crates.io-index"
289 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
290 |
291 | [[package]]
292 | name = "foreign-types"
293 | version = "0.3.2"
294 | source = "registry+https://github.com/rust-lang/crates.io-index"
295 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
296 | dependencies = [
297 | "foreign-types-shared",
298 | ]
299 |
300 | [[package]]
301 | name = "foreign-types-shared"
302 | version = "0.1.1"
303 | source = "registry+https://github.com/rust-lang/crates.io-index"
304 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
305 |
306 | [[package]]
307 | name = "form_urlencoded"
308 | version = "1.0.1"
309 | source = "registry+https://github.com/rust-lang/crates.io-index"
310 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
311 | dependencies = [
312 | "matches",
313 | "percent-encoding",
314 | ]
315 |
316 | [[package]]
317 | name = "fsevent"
318 | version = "0.4.0"
319 | source = "registry+https://github.com/rust-lang/crates.io-index"
320 | checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
321 | dependencies = [
322 | "bitflags",
323 | "fsevent-sys",
324 | ]
325 |
326 | [[package]]
327 | name = "fsevent-sys"
328 | version = "2.0.1"
329 | source = "registry+https://github.com/rust-lang/crates.io-index"
330 | checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
331 | dependencies = [
332 | "libc",
333 | ]
334 |
335 | [[package]]
336 | name = "fuchsia-zircon"
337 | version = "0.3.3"
338 | source = "registry+https://github.com/rust-lang/crates.io-index"
339 | checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
340 | dependencies = [
341 | "bitflags",
342 | "fuchsia-zircon-sys",
343 | ]
344 |
345 | [[package]]
346 | name = "fuchsia-zircon-sys"
347 | version = "0.3.3"
348 | source = "registry+https://github.com/rust-lang/crates.io-index"
349 | checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
350 |
351 | [[package]]
352 | name = "futures"
353 | version = "0.3.21"
354 | source = "registry+https://github.com/rust-lang/crates.io-index"
355 | checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
356 | dependencies = [
357 | "futures-channel",
358 | "futures-core",
359 | "futures-executor",
360 | "futures-io",
361 | "futures-sink",
362 | "futures-task",
363 | "futures-util",
364 | ]
365 |
366 | [[package]]
367 | name = "futures-channel"
368 | version = "0.3.21"
369 | source = "registry+https://github.com/rust-lang/crates.io-index"
370 | checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
371 | dependencies = [
372 | "futures-core",
373 | "futures-sink",
374 | ]
375 |
376 | [[package]]
377 | name = "futures-core"
378 | version = "0.3.21"
379 | source = "registry+https://github.com/rust-lang/crates.io-index"
380 | checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
381 |
382 | [[package]]
383 | name = "futures-executor"
384 | version = "0.3.21"
385 | source = "registry+https://github.com/rust-lang/crates.io-index"
386 | checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
387 | dependencies = [
388 | "futures-core",
389 | "futures-task",
390 | "futures-util",
391 | ]
392 |
393 | [[package]]
394 | name = "futures-io"
395 | version = "0.3.21"
396 | source = "registry+https://github.com/rust-lang/crates.io-index"
397 | checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
398 |
399 | [[package]]
400 | name = "futures-macro"
401 | version = "0.3.21"
402 | source = "registry+https://github.com/rust-lang/crates.io-index"
403 | checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
404 | dependencies = [
405 | "proc-macro2",
406 | "quote",
407 | "syn",
408 | ]
409 |
410 | [[package]]
411 | name = "futures-sink"
412 | version = "0.3.21"
413 | source = "registry+https://github.com/rust-lang/crates.io-index"
414 | checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
415 |
416 | [[package]]
417 | name = "futures-task"
418 | version = "0.3.21"
419 | source = "registry+https://github.com/rust-lang/crates.io-index"
420 | checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
421 |
422 | [[package]]
423 | name = "futures-util"
424 | version = "0.3.21"
425 | source = "registry+https://github.com/rust-lang/crates.io-index"
426 | checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
427 | dependencies = [
428 | "futures-channel",
429 | "futures-core",
430 | "futures-io",
431 | "futures-macro",
432 | "futures-sink",
433 | "futures-task",
434 | "memchr",
435 | "pin-project-lite",
436 | "pin-utils",
437 | "slab",
438 | ]
439 |
440 | [[package]]
441 | name = "generic-array"
442 | version = "0.14.5"
443 | source = "registry+https://github.com/rust-lang/crates.io-index"
444 | checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
445 | dependencies = [
446 | "typenum",
447 | "version_check",
448 | ]
449 |
450 | [[package]]
451 | name = "getrandom"
452 | version = "0.2.6"
453 | source = "registry+https://github.com/rust-lang/crates.io-index"
454 | checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
455 | dependencies = [
456 | "cfg-if 1.0.0",
457 | "libc",
458 | "wasi 0.10.2+wasi-snapshot-preview1",
459 | ]
460 |
461 | [[package]]
462 | name = "h2"
463 | version = "0.3.13"
464 | source = "registry+https://github.com/rust-lang/crates.io-index"
465 | checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57"
466 | dependencies = [
467 | "bytes",
468 | "fnv",
469 | "futures-core",
470 | "futures-sink",
471 | "futures-util",
472 | "http",
473 | "indexmap",
474 | "slab",
475 | "tokio",
476 | "tokio-util",
477 | "tracing",
478 | ]
479 |
480 | [[package]]
481 | name = "hashbrown"
482 | version = "0.11.2"
483 | source = "registry+https://github.com/rust-lang/crates.io-index"
484 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
485 |
486 | [[package]]
487 | name = "heck"
488 | version = "0.3.3"
489 | source = "registry+https://github.com/rust-lang/crates.io-index"
490 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
491 | dependencies = [
492 | "unicode-segmentation",
493 | ]
494 |
495 | [[package]]
496 | name = "hermit-abi"
497 | version = "0.1.19"
498 | source = "registry+https://github.com/rust-lang/crates.io-index"
499 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
500 | dependencies = [
501 | "libc",
502 | ]
503 |
504 | [[package]]
505 | name = "http"
506 | version = "0.2.8"
507 | source = "registry+https://github.com/rust-lang/crates.io-index"
508 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
509 | dependencies = [
510 | "bytes",
511 | "fnv",
512 | "itoa",
513 | ]
514 |
515 | [[package]]
516 | name = "http-body"
517 | version = "0.4.5"
518 | source = "registry+https://github.com/rust-lang/crates.io-index"
519 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
520 | dependencies = [
521 | "bytes",
522 | "http",
523 | "pin-project-lite",
524 | ]
525 |
526 | [[package]]
527 | name = "http-range"
528 | version = "0.1.5"
529 | source = "registry+https://github.com/rust-lang/crates.io-index"
530 | checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
531 |
532 | [[package]]
533 | name = "httparse"
534 | version = "1.7.1"
535 | source = "registry+https://github.com/rust-lang/crates.io-index"
536 | checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
537 |
538 | [[package]]
539 | name = "httpdate"
540 | version = "1.0.2"
541 | source = "registry+https://github.com/rust-lang/crates.io-index"
542 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
543 |
544 | [[package]]
545 | name = "humantime"
546 | version = "1.3.0"
547 | source = "registry+https://github.com/rust-lang/crates.io-index"
548 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
549 | dependencies = [
550 | "quick-error",
551 | ]
552 |
553 | [[package]]
554 | name = "hyper"
555 | version = "0.14.19"
556 | source = "registry+https://github.com/rust-lang/crates.io-index"
557 | checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f"
558 | dependencies = [
559 | "bytes",
560 | "futures-channel",
561 | "futures-core",
562 | "futures-util",
563 | "h2",
564 | "http",
565 | "http-body",
566 | "httparse",
567 | "httpdate",
568 | "itoa",
569 | "pin-project-lite",
570 | "socket2",
571 | "tokio",
572 | "tower-service",
573 | "tracing",
574 | "want",
575 | ]
576 |
577 | [[package]]
578 | name = "hyper-tls"
579 | version = "0.5.0"
580 | source = "registry+https://github.com/rust-lang/crates.io-index"
581 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
582 | dependencies = [
583 | "bytes",
584 | "hyper",
585 | "native-tls",
586 | "tokio",
587 | "tokio-native-tls",
588 | ]
589 |
590 | [[package]]
591 | name = "hyper-tungstenite"
592 | version = "0.8.0"
593 | source = "registry+https://github.com/rust-lang/crates.io-index"
594 | checksum = "b0ea2c1b59596d6b1302fe616266257a58b079f68fee329d6d111f79241cb7fd"
595 | dependencies = [
596 | "hyper",
597 | "pin-project",
598 | "tokio",
599 | "tokio-tungstenite",
600 | "tungstenite",
601 | ]
602 |
603 | [[package]]
604 | name = "idna"
605 | version = "0.2.3"
606 | source = "registry+https://github.com/rust-lang/crates.io-index"
607 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
608 | dependencies = [
609 | "matches",
610 | "unicode-bidi",
611 | "unicode-normalization",
612 | ]
613 |
614 | [[package]]
615 | name = "indexmap"
616 | version = "1.8.2"
617 | source = "registry+https://github.com/rust-lang/crates.io-index"
618 | checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a"
619 | dependencies = [
620 | "autocfg",
621 | "hashbrown",
622 | ]
623 |
624 | [[package]]
625 | name = "infer"
626 | version = "0.15.0"
627 | source = "registry+https://github.com/rust-lang/crates.io-index"
628 | checksum = "cb33622da908807a06f9513c19b3c1ad50fab3e4137d82a78107d502075aa199"
629 | dependencies = [
630 | "cfb",
631 | ]
632 |
633 | [[package]]
634 | name = "inotify"
635 | version = "0.7.1"
636 | source = "registry+https://github.com/rust-lang/crates.io-index"
637 | checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
638 | dependencies = [
639 | "bitflags",
640 | "inotify-sys",
641 | "libc",
642 | ]
643 |
644 | [[package]]
645 | name = "inotify-sys"
646 | version = "0.1.5"
647 | source = "registry+https://github.com/rust-lang/crates.io-index"
648 | checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
649 | dependencies = [
650 | "libc",
651 | ]
652 |
653 | [[package]]
654 | name = "instant"
655 | version = "0.1.12"
656 | source = "registry+https://github.com/rust-lang/crates.io-index"
657 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
658 | dependencies = [
659 | "cfg-if 1.0.0",
660 | ]
661 |
662 | [[package]]
663 | name = "iovec"
664 | version = "0.1.4"
665 | source = "registry+https://github.com/rust-lang/crates.io-index"
666 | checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
667 | dependencies = [
668 | "libc",
669 | ]
670 |
671 | [[package]]
672 | name = "itoa"
673 | version = "1.0.2"
674 | source = "registry+https://github.com/rust-lang/crates.io-index"
675 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
676 |
677 | [[package]]
678 | name = "kernel32-sys"
679 | version = "0.2.2"
680 | source = "registry+https://github.com/rust-lang/crates.io-index"
681 | checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
682 | dependencies = [
683 | "winapi 0.2.8",
684 | "winapi-build",
685 | ]
686 |
687 | [[package]]
688 | name = "lazy_static"
689 | version = "1.4.0"
690 | source = "registry+https://github.com/rust-lang/crates.io-index"
691 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
692 |
693 | [[package]]
694 | name = "lazycell"
695 | version = "1.3.0"
696 | source = "registry+https://github.com/rust-lang/crates.io-index"
697 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
698 |
699 | [[package]]
700 | name = "libc"
701 | version = "0.2.126"
702 | source = "registry+https://github.com/rust-lang/crates.io-index"
703 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
704 |
705 | [[package]]
706 | name = "litrs"
707 | version = "0.2.3"
708 | source = "registry+https://github.com/rust-lang/crates.io-index"
709 | checksum = "f9275e0933cf8bb20f008924c0cb07a0692fe54d8064996520bf998de9eb79aa"
710 | dependencies = [
711 | "proc-macro2",
712 | ]
713 |
714 | [[package]]
715 | name = "log"
716 | version = "0.4.17"
717 | source = "registry+https://github.com/rust-lang/crates.io-index"
718 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
719 | dependencies = [
720 | "cfg-if 1.0.0",
721 | ]
722 |
723 | [[package]]
724 | name = "matches"
725 | version = "0.1.9"
726 | source = "registry+https://github.com/rust-lang/crates.io-index"
727 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
728 |
729 | [[package]]
730 | name = "memchr"
731 | version = "2.5.0"
732 | source = "registry+https://github.com/rust-lang/crates.io-index"
733 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
734 |
735 | [[package]]
736 | name = "mime"
737 | version = "0.3.16"
738 | source = "registry+https://github.com/rust-lang/crates.io-index"
739 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
740 |
741 | [[package]]
742 | name = "mime_guess"
743 | version = "2.0.4"
744 | source = "registry+https://github.com/rust-lang/crates.io-index"
745 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
746 | dependencies = [
747 | "mime",
748 | "unicase",
749 | ]
750 |
751 | [[package]]
752 | name = "miniz_oxide"
753 | version = "0.5.3"
754 | source = "registry+https://github.com/rust-lang/crates.io-index"
755 | checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
756 | dependencies = [
757 | "adler",
758 | ]
759 |
760 | [[package]]
761 | name = "mio"
762 | version = "0.6.23"
763 | source = "registry+https://github.com/rust-lang/crates.io-index"
764 | checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
765 | dependencies = [
766 | "cfg-if 0.1.10",
767 | "fuchsia-zircon",
768 | "fuchsia-zircon-sys",
769 | "iovec",
770 | "kernel32-sys",
771 | "libc",
772 | "log",
773 | "miow",
774 | "net2",
775 | "slab",
776 | "winapi 0.2.8",
777 | ]
778 |
779 | [[package]]
780 | name = "mio"
781 | version = "0.8.3"
782 | source = "registry+https://github.com/rust-lang/crates.io-index"
783 | checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
784 | dependencies = [
785 | "libc",
786 | "log",
787 | "wasi 0.11.0+wasi-snapshot-preview1",
788 | "windows-sys",
789 | ]
790 |
791 | [[package]]
792 | name = "mio-extras"
793 | version = "2.0.6"
794 | source = "registry+https://github.com/rust-lang/crates.io-index"
795 | checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
796 | dependencies = [
797 | "lazycell",
798 | "log",
799 | "mio 0.6.23",
800 | "slab",
801 | ]
802 |
803 | [[package]]
804 | name = "miow"
805 | version = "0.2.2"
806 | source = "registry+https://github.com/rust-lang/crates.io-index"
807 | checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
808 | dependencies = [
809 | "kernel32-sys",
810 | "net2",
811 | "winapi 0.2.8",
812 | "ws2_32-sys",
813 | ]
814 |
815 | [[package]]
816 | name = "native-tls"
817 | version = "0.2.10"
818 | source = "registry+https://github.com/rust-lang/crates.io-index"
819 | checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
820 | dependencies = [
821 | "lazy_static",
822 | "libc",
823 | "log",
824 | "openssl",
825 | "openssl-probe",
826 | "openssl-sys",
827 | "schannel",
828 | "security-framework",
829 | "security-framework-sys",
830 | "tempfile",
831 | ]
832 |
833 | [[package]]
834 | name = "net2"
835 | version = "0.2.37"
836 | source = "registry+https://github.com/rust-lang/crates.io-index"
837 | checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
838 | dependencies = [
839 | "cfg-if 0.1.10",
840 | "libc",
841 | "winapi 0.3.9",
842 | ]
843 |
844 | [[package]]
845 | name = "notify"
846 | version = "4.0.17"
847 | source = "registry+https://github.com/rust-lang/crates.io-index"
848 | checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257"
849 | dependencies = [
850 | "bitflags",
851 | "filetime",
852 | "fsevent",
853 | "fsevent-sys",
854 | "inotify",
855 | "libc",
856 | "mio 0.6.23",
857 | "mio-extras",
858 | "walkdir",
859 | "winapi 0.3.9",
860 | ]
861 |
862 | [[package]]
863 | name = "num_cpus"
864 | version = "1.13.1"
865 | source = "registry+https://github.com/rust-lang/crates.io-index"
866 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
867 | dependencies = [
868 | "hermit-abi",
869 | "libc",
870 | ]
871 |
872 | [[package]]
873 | name = "once_cell"
874 | version = "1.12.0"
875 | source = "registry+https://github.com/rust-lang/crates.io-index"
876 | checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
877 |
878 | [[package]]
879 | name = "open"
880 | version = "2.1.3"
881 | source = "registry+https://github.com/rust-lang/crates.io-index"
882 | checksum = "f2423ffbf445b82e58c3b1543655968923dd06f85432f10be2bb4f1b7122f98c"
883 | dependencies = [
884 | "pathdiff",
885 | "windows-sys",
886 | ]
887 |
888 | [[package]]
889 | name = "openssl"
890 | version = "0.10.40"
891 | source = "registry+https://github.com/rust-lang/crates.io-index"
892 | checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e"
893 | dependencies = [
894 | "bitflags",
895 | "cfg-if 1.0.0",
896 | "foreign-types",
897 | "libc",
898 | "once_cell",
899 | "openssl-macros",
900 | "openssl-sys",
901 | ]
902 |
903 | [[package]]
904 | name = "openssl-macros"
905 | version = "0.1.0"
906 | source = "registry+https://github.com/rust-lang/crates.io-index"
907 | checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
908 | dependencies = [
909 | "proc-macro2",
910 | "quote",
911 | "syn",
912 | ]
913 |
914 | [[package]]
915 | name = "openssl-probe"
916 | version = "0.1.5"
917 | source = "registry+https://github.com/rust-lang/crates.io-index"
918 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
919 |
920 | [[package]]
921 | name = "openssl-src"
922 | version = "111.20.0+1.1.1o"
923 | source = "registry+https://github.com/rust-lang/crates.io-index"
924 | checksum = "92892c4f87d56e376e469ace79f1128fdaded07646ddf73aa0be4706ff712dec"
925 | dependencies = [
926 | "cc",
927 | ]
928 |
929 | [[package]]
930 | name = "openssl-sys"
931 | version = "0.9.74"
932 | source = "registry+https://github.com/rust-lang/crates.io-index"
933 | checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1"
934 | dependencies = [
935 | "autocfg",
936 | "cc",
937 | "libc",
938 | "openssl-src",
939 | "pkg-config",
940 | "vcpkg",
941 | ]
942 |
943 | [[package]]
944 | name = "pathdiff"
945 | version = "0.2.1"
946 | source = "registry+https://github.com/rust-lang/crates.io-index"
947 | checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
948 |
949 | [[package]]
950 | name = "penguin"
951 | version = "0.1.8"
952 | dependencies = [
953 | "brotli",
954 | "flate2",
955 | "futures",
956 | "http-range",
957 | "hyper",
958 | "hyper-tls",
959 | "hyper-tungstenite",
960 | "infer",
961 | "log",
962 | "mime_guess",
963 | "thiserror",
964 | "tokio",
965 | "tokio-util",
966 | ]
967 |
968 | [[package]]
969 | name = "penguin-app"
970 | version = "0.2.6"
971 | dependencies = [
972 | "anyhow",
973 | "bunt",
974 | "log",
975 | "notify",
976 | "open",
977 | "penguin",
978 | "pretty_env_logger",
979 | "structopt",
980 | "tokio",
981 | ]
982 |
983 | [[package]]
984 | name = "percent-encoding"
985 | version = "2.1.0"
986 | source = "registry+https://github.com/rust-lang/crates.io-index"
987 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
988 |
989 | [[package]]
990 | name = "pin-project"
991 | version = "1.0.10"
992 | source = "registry+https://github.com/rust-lang/crates.io-index"
993 | checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e"
994 | dependencies = [
995 | "pin-project-internal",
996 | ]
997 |
998 | [[package]]
999 | name = "pin-project-internal"
1000 | version = "1.0.10"
1001 | source = "registry+https://github.com/rust-lang/crates.io-index"
1002 | checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb"
1003 | dependencies = [
1004 | "proc-macro2",
1005 | "quote",
1006 | "syn",
1007 | ]
1008 |
1009 | [[package]]
1010 | name = "pin-project-lite"
1011 | version = "0.2.9"
1012 | source = "registry+https://github.com/rust-lang/crates.io-index"
1013 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
1014 |
1015 | [[package]]
1016 | name = "pin-utils"
1017 | version = "0.1.0"
1018 | source = "registry+https://github.com/rust-lang/crates.io-index"
1019 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
1020 |
1021 | [[package]]
1022 | name = "pkg-config"
1023 | version = "0.3.25"
1024 | source = "registry+https://github.com/rust-lang/crates.io-index"
1025 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
1026 |
1027 | [[package]]
1028 | name = "ppv-lite86"
1029 | version = "0.2.16"
1030 | source = "registry+https://github.com/rust-lang/crates.io-index"
1031 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
1032 |
1033 | [[package]]
1034 | name = "pretty_env_logger"
1035 | version = "0.4.0"
1036 | source = "registry+https://github.com/rust-lang/crates.io-index"
1037 | checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d"
1038 | dependencies = [
1039 | "env_logger",
1040 | "log",
1041 | ]
1042 |
1043 | [[package]]
1044 | name = "proc-macro-error"
1045 | version = "1.0.4"
1046 | source = "registry+https://github.com/rust-lang/crates.io-index"
1047 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
1048 | dependencies = [
1049 | "proc-macro-error-attr",
1050 | "proc-macro2",
1051 | "quote",
1052 | "syn",
1053 | "version_check",
1054 | ]
1055 |
1056 | [[package]]
1057 | name = "proc-macro-error-attr"
1058 | version = "1.0.4"
1059 | source = "registry+https://github.com/rust-lang/crates.io-index"
1060 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
1061 | dependencies = [
1062 | "proc-macro2",
1063 | "quote",
1064 | "version_check",
1065 | ]
1066 |
1067 | [[package]]
1068 | name = "proc-macro2"
1069 | version = "1.0.39"
1070 | source = "registry+https://github.com/rust-lang/crates.io-index"
1071 | checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
1072 | dependencies = [
1073 | "unicode-ident",
1074 | ]
1075 |
1076 | [[package]]
1077 | name = "quick-error"
1078 | version = "1.2.3"
1079 | source = "registry+https://github.com/rust-lang/crates.io-index"
1080 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
1081 |
1082 | [[package]]
1083 | name = "quote"
1084 | version = "1.0.18"
1085 | source = "registry+https://github.com/rust-lang/crates.io-index"
1086 | checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
1087 | dependencies = [
1088 | "proc-macro2",
1089 | ]
1090 |
1091 | [[package]]
1092 | name = "rand"
1093 | version = "0.8.5"
1094 | source = "registry+https://github.com/rust-lang/crates.io-index"
1095 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
1096 | dependencies = [
1097 | "libc",
1098 | "rand_chacha",
1099 | "rand_core",
1100 | ]
1101 |
1102 | [[package]]
1103 | name = "rand_chacha"
1104 | version = "0.3.1"
1105 | source = "registry+https://github.com/rust-lang/crates.io-index"
1106 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
1107 | dependencies = [
1108 | "ppv-lite86",
1109 | "rand_core",
1110 | ]
1111 |
1112 | [[package]]
1113 | name = "rand_core"
1114 | version = "0.6.3"
1115 | source = "registry+https://github.com/rust-lang/crates.io-index"
1116 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
1117 | dependencies = [
1118 | "getrandom",
1119 | ]
1120 |
1121 | [[package]]
1122 | name = "redox_syscall"
1123 | version = "0.2.13"
1124 | source = "registry+https://github.com/rust-lang/crates.io-index"
1125 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
1126 | dependencies = [
1127 | "bitflags",
1128 | ]
1129 |
1130 | [[package]]
1131 | name = "regex"
1132 | version = "1.5.6"
1133 | source = "registry+https://github.com/rust-lang/crates.io-index"
1134 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
1135 | dependencies = [
1136 | "aho-corasick",
1137 | "memchr",
1138 | "regex-syntax",
1139 | ]
1140 |
1141 | [[package]]
1142 | name = "regex-syntax"
1143 | version = "0.6.26"
1144 | source = "registry+https://github.com/rust-lang/crates.io-index"
1145 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
1146 |
1147 | [[package]]
1148 | name = "remove_dir_all"
1149 | version = "0.5.3"
1150 | source = "registry+https://github.com/rust-lang/crates.io-index"
1151 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
1152 | dependencies = [
1153 | "winapi 0.3.9",
1154 | ]
1155 |
1156 | [[package]]
1157 | name = "same-file"
1158 | version = "1.0.6"
1159 | source = "registry+https://github.com/rust-lang/crates.io-index"
1160 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
1161 | dependencies = [
1162 | "winapi-util",
1163 | ]
1164 |
1165 | [[package]]
1166 | name = "schannel"
1167 | version = "0.1.20"
1168 | source = "registry+https://github.com/rust-lang/crates.io-index"
1169 | checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
1170 | dependencies = [
1171 | "lazy_static",
1172 | "windows-sys",
1173 | ]
1174 |
1175 | [[package]]
1176 | name = "security-framework"
1177 | version = "2.6.1"
1178 | source = "registry+https://github.com/rust-lang/crates.io-index"
1179 | checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc"
1180 | dependencies = [
1181 | "bitflags",
1182 | "core-foundation",
1183 | "core-foundation-sys",
1184 | "libc",
1185 | "security-framework-sys",
1186 | ]
1187 |
1188 | [[package]]
1189 | name = "security-framework-sys"
1190 | version = "2.6.1"
1191 | source = "registry+https://github.com/rust-lang/crates.io-index"
1192 | checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
1193 | dependencies = [
1194 | "core-foundation-sys",
1195 | "libc",
1196 | ]
1197 |
1198 | [[package]]
1199 | name = "sha-1"
1200 | version = "0.10.0"
1201 | source = "registry+https://github.com/rust-lang/crates.io-index"
1202 | checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
1203 | dependencies = [
1204 | "cfg-if 1.0.0",
1205 | "cpufeatures",
1206 | "digest",
1207 | ]
1208 |
1209 | [[package]]
1210 | name = "slab"
1211 | version = "0.4.6"
1212 | source = "registry+https://github.com/rust-lang/crates.io-index"
1213 | checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
1214 |
1215 | [[package]]
1216 | name = "socket2"
1217 | version = "0.4.4"
1218 | source = "registry+https://github.com/rust-lang/crates.io-index"
1219 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
1220 | dependencies = [
1221 | "libc",
1222 | "winapi 0.3.9",
1223 | ]
1224 |
1225 | [[package]]
1226 | name = "strsim"
1227 | version = "0.8.0"
1228 | source = "registry+https://github.com/rust-lang/crates.io-index"
1229 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
1230 |
1231 | [[package]]
1232 | name = "structopt"
1233 | version = "0.3.26"
1234 | source = "registry+https://github.com/rust-lang/crates.io-index"
1235 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
1236 | dependencies = [
1237 | "clap",
1238 | "lazy_static",
1239 | "structopt-derive",
1240 | ]
1241 |
1242 | [[package]]
1243 | name = "structopt-derive"
1244 | version = "0.4.18"
1245 | source = "registry+https://github.com/rust-lang/crates.io-index"
1246 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
1247 | dependencies = [
1248 | "heck",
1249 | "proc-macro-error",
1250 | "proc-macro2",
1251 | "quote",
1252 | "syn",
1253 | ]
1254 |
1255 | [[package]]
1256 | name = "syn"
1257 | version = "1.0.96"
1258 | source = "registry+https://github.com/rust-lang/crates.io-index"
1259 | checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf"
1260 | dependencies = [
1261 | "proc-macro2",
1262 | "quote",
1263 | "unicode-ident",
1264 | ]
1265 |
1266 | [[package]]
1267 | name = "tempfile"
1268 | version = "3.3.0"
1269 | source = "registry+https://github.com/rust-lang/crates.io-index"
1270 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
1271 | dependencies = [
1272 | "cfg-if 1.0.0",
1273 | "fastrand",
1274 | "libc",
1275 | "redox_syscall",
1276 | "remove_dir_all",
1277 | "winapi 0.3.9",
1278 | ]
1279 |
1280 | [[package]]
1281 | name = "termcolor"
1282 | version = "1.1.3"
1283 | source = "registry+https://github.com/rust-lang/crates.io-index"
1284 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
1285 | dependencies = [
1286 | "winapi-util",
1287 | ]
1288 |
1289 | [[package]]
1290 | name = "textwrap"
1291 | version = "0.11.0"
1292 | source = "registry+https://github.com/rust-lang/crates.io-index"
1293 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
1294 | dependencies = [
1295 | "unicode-width",
1296 | ]
1297 |
1298 | [[package]]
1299 | name = "thiserror"
1300 | version = "1.0.31"
1301 | source = "registry+https://github.com/rust-lang/crates.io-index"
1302 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
1303 | dependencies = [
1304 | "thiserror-impl",
1305 | ]
1306 |
1307 | [[package]]
1308 | name = "thiserror-impl"
1309 | version = "1.0.31"
1310 | source = "registry+https://github.com/rust-lang/crates.io-index"
1311 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
1312 | dependencies = [
1313 | "proc-macro2",
1314 | "quote",
1315 | "syn",
1316 | ]
1317 |
1318 | [[package]]
1319 | name = "tinyvec"
1320 | version = "1.6.0"
1321 | source = "registry+https://github.com/rust-lang/crates.io-index"
1322 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
1323 | dependencies = [
1324 | "tinyvec_macros",
1325 | ]
1326 |
1327 | [[package]]
1328 | name = "tinyvec_macros"
1329 | version = "0.1.0"
1330 | source = "registry+https://github.com/rust-lang/crates.io-index"
1331 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
1332 |
1333 | [[package]]
1334 | name = "tokio"
1335 | version = "1.19.2"
1336 | source = "registry+https://github.com/rust-lang/crates.io-index"
1337 | checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439"
1338 | dependencies = [
1339 | "bytes",
1340 | "libc",
1341 | "memchr",
1342 | "mio 0.8.3",
1343 | "num_cpus",
1344 | "once_cell",
1345 | "pin-project-lite",
1346 | "socket2",
1347 | "tokio-macros",
1348 | "winapi 0.3.9",
1349 | ]
1350 |
1351 | [[package]]
1352 | name = "tokio-macros"
1353 | version = "1.8.0"
1354 | source = "registry+https://github.com/rust-lang/crates.io-index"
1355 | checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
1356 | dependencies = [
1357 | "proc-macro2",
1358 | "quote",
1359 | "syn",
1360 | ]
1361 |
1362 | [[package]]
1363 | name = "tokio-native-tls"
1364 | version = "0.3.0"
1365 | source = "registry+https://github.com/rust-lang/crates.io-index"
1366 | checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
1367 | dependencies = [
1368 | "native-tls",
1369 | "tokio",
1370 | ]
1371 |
1372 | [[package]]
1373 | name = "tokio-tungstenite"
1374 | version = "0.17.1"
1375 | source = "registry+https://github.com/rust-lang/crates.io-index"
1376 | checksum = "06cda1232a49558c46f8a504d5b93101d42c0bf7f911f12a105ba48168f821ae"
1377 | dependencies = [
1378 | "futures-util",
1379 | "log",
1380 | "tokio",
1381 | "tungstenite",
1382 | ]
1383 |
1384 | [[package]]
1385 | name = "tokio-util"
1386 | version = "0.7.3"
1387 | source = "registry+https://github.com/rust-lang/crates.io-index"
1388 | checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45"
1389 | dependencies = [
1390 | "bytes",
1391 | "futures-core",
1392 | "futures-sink",
1393 | "pin-project-lite",
1394 | "tokio",
1395 | "tracing",
1396 | ]
1397 |
1398 | [[package]]
1399 | name = "tower-service"
1400 | version = "0.3.1"
1401 | source = "registry+https://github.com/rust-lang/crates.io-index"
1402 | checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
1403 |
1404 | [[package]]
1405 | name = "tracing"
1406 | version = "0.1.34"
1407 | source = "registry+https://github.com/rust-lang/crates.io-index"
1408 | checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
1409 | dependencies = [
1410 | "cfg-if 1.0.0",
1411 | "pin-project-lite",
1412 | "tracing-core",
1413 | ]
1414 |
1415 | [[package]]
1416 | name = "tracing-core"
1417 | version = "0.1.27"
1418 | source = "registry+https://github.com/rust-lang/crates.io-index"
1419 | checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921"
1420 | dependencies = [
1421 | "once_cell",
1422 | ]
1423 |
1424 | [[package]]
1425 | name = "try-lock"
1426 | version = "0.2.3"
1427 | source = "registry+https://github.com/rust-lang/crates.io-index"
1428 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
1429 |
1430 | [[package]]
1431 | name = "tungstenite"
1432 | version = "0.17.2"
1433 | source = "registry+https://github.com/rust-lang/crates.io-index"
1434 | checksum = "d96a2dea40e7570482f28eb57afbe42d97551905da6a9400acc5c328d24004f5"
1435 | dependencies = [
1436 | "base64",
1437 | "byteorder",
1438 | "bytes",
1439 | "http",
1440 | "httparse",
1441 | "log",
1442 | "rand",
1443 | "sha-1",
1444 | "thiserror",
1445 | "url",
1446 | "utf-8",
1447 | ]
1448 |
1449 | [[package]]
1450 | name = "typenum"
1451 | version = "1.15.0"
1452 | source = "registry+https://github.com/rust-lang/crates.io-index"
1453 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
1454 |
1455 | [[package]]
1456 | name = "unicase"
1457 | version = "2.6.0"
1458 | source = "registry+https://github.com/rust-lang/crates.io-index"
1459 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
1460 | dependencies = [
1461 | "version_check",
1462 | ]
1463 |
1464 | [[package]]
1465 | name = "unicode-bidi"
1466 | version = "0.3.8"
1467 | source = "registry+https://github.com/rust-lang/crates.io-index"
1468 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
1469 |
1470 | [[package]]
1471 | name = "unicode-ident"
1472 | version = "1.0.0"
1473 | source = "registry+https://github.com/rust-lang/crates.io-index"
1474 | checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
1475 |
1476 | [[package]]
1477 | name = "unicode-normalization"
1478 | version = "0.1.19"
1479 | source = "registry+https://github.com/rust-lang/crates.io-index"
1480 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
1481 | dependencies = [
1482 | "tinyvec",
1483 | ]
1484 |
1485 | [[package]]
1486 | name = "unicode-segmentation"
1487 | version = "1.9.0"
1488 | source = "registry+https://github.com/rust-lang/crates.io-index"
1489 | checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
1490 |
1491 | [[package]]
1492 | name = "unicode-width"
1493 | version = "0.1.9"
1494 | source = "registry+https://github.com/rust-lang/crates.io-index"
1495 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
1496 |
1497 | [[package]]
1498 | name = "unicode-xid"
1499 | version = "0.2.3"
1500 | source = "registry+https://github.com/rust-lang/crates.io-index"
1501 | checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
1502 |
1503 | [[package]]
1504 | name = "url"
1505 | version = "2.2.2"
1506 | source = "registry+https://github.com/rust-lang/crates.io-index"
1507 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
1508 | dependencies = [
1509 | "form_urlencoded",
1510 | "idna",
1511 | "matches",
1512 | "percent-encoding",
1513 | ]
1514 |
1515 | [[package]]
1516 | name = "utf-8"
1517 | version = "0.7.6"
1518 | source = "registry+https://github.com/rust-lang/crates.io-index"
1519 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
1520 |
1521 | [[package]]
1522 | name = "uuid"
1523 | version = "1.6.1"
1524 | source = "registry+https://github.com/rust-lang/crates.io-index"
1525 | checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
1526 |
1527 | [[package]]
1528 | name = "vcpkg"
1529 | version = "0.2.15"
1530 | source = "registry+https://github.com/rust-lang/crates.io-index"
1531 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
1532 |
1533 | [[package]]
1534 | name = "vec_map"
1535 | version = "0.8.2"
1536 | source = "registry+https://github.com/rust-lang/crates.io-index"
1537 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
1538 |
1539 | [[package]]
1540 | name = "version_check"
1541 | version = "0.9.4"
1542 | source = "registry+https://github.com/rust-lang/crates.io-index"
1543 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
1544 |
1545 | [[package]]
1546 | name = "walkdir"
1547 | version = "2.3.2"
1548 | source = "registry+https://github.com/rust-lang/crates.io-index"
1549 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
1550 | dependencies = [
1551 | "same-file",
1552 | "winapi 0.3.9",
1553 | "winapi-util",
1554 | ]
1555 |
1556 | [[package]]
1557 | name = "want"
1558 | version = "0.3.0"
1559 | source = "registry+https://github.com/rust-lang/crates.io-index"
1560 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
1561 | dependencies = [
1562 | "log",
1563 | "try-lock",
1564 | ]
1565 |
1566 | [[package]]
1567 | name = "wasi"
1568 | version = "0.10.2+wasi-snapshot-preview1"
1569 | source = "registry+https://github.com/rust-lang/crates.io-index"
1570 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
1571 |
1572 | [[package]]
1573 | name = "wasi"
1574 | version = "0.11.0+wasi-snapshot-preview1"
1575 | source = "registry+https://github.com/rust-lang/crates.io-index"
1576 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
1577 |
1578 | [[package]]
1579 | name = "winapi"
1580 | version = "0.2.8"
1581 | source = "registry+https://github.com/rust-lang/crates.io-index"
1582 | checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
1583 |
1584 | [[package]]
1585 | name = "winapi"
1586 | version = "0.3.9"
1587 | source = "registry+https://github.com/rust-lang/crates.io-index"
1588 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1589 | dependencies = [
1590 | "winapi-i686-pc-windows-gnu",
1591 | "winapi-x86_64-pc-windows-gnu",
1592 | ]
1593 |
1594 | [[package]]
1595 | name = "winapi-build"
1596 | version = "0.1.1"
1597 | source = "registry+https://github.com/rust-lang/crates.io-index"
1598 | checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
1599 |
1600 | [[package]]
1601 | name = "winapi-i686-pc-windows-gnu"
1602 | version = "0.4.0"
1603 | source = "registry+https://github.com/rust-lang/crates.io-index"
1604 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1605 |
1606 | [[package]]
1607 | name = "winapi-util"
1608 | version = "0.1.5"
1609 | source = "registry+https://github.com/rust-lang/crates.io-index"
1610 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
1611 | dependencies = [
1612 | "winapi 0.3.9",
1613 | ]
1614 |
1615 | [[package]]
1616 | name = "winapi-x86_64-pc-windows-gnu"
1617 | version = "0.4.0"
1618 | source = "registry+https://github.com/rust-lang/crates.io-index"
1619 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1620 |
1621 | [[package]]
1622 | name = "windows-sys"
1623 | version = "0.36.1"
1624 | source = "registry+https://github.com/rust-lang/crates.io-index"
1625 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
1626 | dependencies = [
1627 | "windows_aarch64_msvc",
1628 | "windows_i686_gnu",
1629 | "windows_i686_msvc",
1630 | "windows_x86_64_gnu",
1631 | "windows_x86_64_msvc",
1632 | ]
1633 |
1634 | [[package]]
1635 | name = "windows_aarch64_msvc"
1636 | version = "0.36.1"
1637 | source = "registry+https://github.com/rust-lang/crates.io-index"
1638 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
1639 |
1640 | [[package]]
1641 | name = "windows_i686_gnu"
1642 | version = "0.36.1"
1643 | source = "registry+https://github.com/rust-lang/crates.io-index"
1644 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
1645 |
1646 | [[package]]
1647 | name = "windows_i686_msvc"
1648 | version = "0.36.1"
1649 | source = "registry+https://github.com/rust-lang/crates.io-index"
1650 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
1651 |
1652 | [[package]]
1653 | name = "windows_x86_64_gnu"
1654 | version = "0.36.1"
1655 | source = "registry+https://github.com/rust-lang/crates.io-index"
1656 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
1657 |
1658 | [[package]]
1659 | name = "windows_x86_64_msvc"
1660 | version = "0.36.1"
1661 | source = "registry+https://github.com/rust-lang/crates.io-index"
1662 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
1663 |
1664 | [[package]]
1665 | name = "ws2_32-sys"
1666 | version = "0.2.1"
1667 | source = "registry+https://github.com/rust-lang/crates.io-index"
1668 | checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
1669 | dependencies = [
1670 | "winapi 0.2.8",
1671 | "winapi-build",
1672 | ]
1673 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = ["app", "lib"]
3 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Project Developers
2 |
3 | Permission is hereby granted, free of charge, to any
4 | person obtaining a copy of this software and associated
5 | documentation files (the "Software"), to deal in the
6 | Software without restriction, including without
7 | limitation the rights to use, copy, modify, merge,
8 | publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software
10 | is furnished to do so, subject to the following
11 | conditions:
12 |
13 | The above copyright notice and this permission notice
14 | shall be included in all copies or substantial portions
15 | of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 | DEALINGS IN THE SOFTWARE.
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Penguin: language and framework agnostic dev server
2 |
3 |
4 | [
](https://github.com/LukasKalbertodt/penguin/actions/workflows/ci.yml)
5 | [
](https://crates.io/crates/penguin-app)
6 | [
](https://crates.io/crates/penguin)
7 | [
](https://docs.rs/penguin)
8 |
9 | Penguin is a dev server featuring live-reloading, a file server, proxy support, and more.
10 | It is language and framework agnostic, so it works for basically any web project.
11 | Browser sessions can reload themselves (e.g. when a file changes) or show an overlay with a custom message (e.g. the compiler error).
12 |
13 | Penguin is available both as a command line application (`penguin-app` on crates.io) and as a library. For for more information on the library, see [its documentation](https://docs.rs/penguin). The rest of this document will mainly talk about the CLI app.
14 |
15 |
16 | ## Example
17 |
18 |
19 |
20 |
21 |
22 | - `penguin serve .` serves the current directory as file server
23 | - `penguin proxy localhost:3000` forwards all requests to `http://localhost:3000`.
24 | - `-m uri_path:fs_path` allows you to mount additional directories in the router.
25 | - `penguin reload` reloads all active browser sessions.
26 |
27 |
28 | ## Installation
29 |
30 | For now, you have to compile the app yourself. It's easiest to install it from
31 | crates.io:
32 |
33 | ```
34 | cargo install penguin-app
35 | ```
36 |
37 | Don't worry about the `-app` suffix: the installed binary is called `penguin`.
38 |
39 |
40 | ## CLI Usage
41 |
42 | There are two main "entry points": `penguin proxy ` and `penguin serve `.
43 | The `proxy` subcommand is useful if you have some (backend) webserver on your own, e.g. to provide an API.
44 | The `serve` subcommand is useful if you only have static files that need to be served, e.g. for static site generators or backend-less single page applications.
45 |
46 | In either case, you can *mount* additional directories at an URL path with `-m/--mount`.
47 | The syntax is `-m :`, for example `-m fonts:frontend/static`.
48 | An HTTP request for `/fonts/foo.woff2` would be answered with the file `frontend/static/foo.woff2` or with 404 if said file does not exist.
49 |
50 | All paths that are served by Penguin are automatically watched by default.
51 | This means that any file change in any of those directories will lead to all browser sessions reloading automatically.
52 | You can watch additional paths (that are not mounted/served) with `-w/--watch`.
53 |
54 | Reloading all active browser sessions can also be done manually via `penguin reload`.
55 | This is intended to be used at the end of your build scripts.
56 | Note that Penguin is not a build system or task executor!
57 | So you are mostly expected to combine it with other tools, like [`watchexec`](https://github.com/watchexec/watchexec), [`cargo watch`](https://github.com/passcod/cargo-watch) or others.
58 | I am also working on [`floof`](https://github.com/LukasKalbertodt/floof/), which is a WIP file-watcher and task-runner/build-system that uses Penguin under the hood to provide a dev server.
59 |
60 | Penguins output can be modified with `-v/-vv` and the log level (set via `-l` or `RUST_LOG`).
61 |
62 | For the full CLI documentation run `penguin --help` or `penguin --help`.
63 |
64 |
65 | ## Project status and "using in production"
66 |
67 | This project is fairly young and not well tested.
68 | However, it already serves as a useful development tool for me.
69 | I'm interested in making it useful for as many people as possible without increasing the project's scope too much.
70 |
71 | I am looking for **Community Feedback**: please speak your mind in [this issue](https://github.com/LukasKalbertodt/penguin/issues/6).
72 | Especially if you have a use case that is not yet well served by Penguin, I'd like to know about that!
73 |
74 | "Can I use Penguin in production?". **No, absolutely not!** This is a
75 | development tool only and you should not open up a Penguin server to the public.
76 | There are probably a gazillion attack vectors.
77 |
78 |
79 | ## Versioning and stability guarantees
80 |
81 | The app and library are versioned independently from one another. The project
82 | mostly follows the usual semantic versioning guidelines.
83 |
84 | - The required Rust version (MSRV) can be bumped at any time, even with minor
85 | releases. This will change once this project reaches 1.0.
86 | - All UI (HTML/CSS) this app/lib produces is subject to change even with minor
87 | version bumps. For example, you cannot rely on a specific "directory listing"
88 | of the file server.
89 | - HTTP headers in server replies might be added (or potentially even removed)
90 | even in minor version bumps.
91 |
92 |
93 |
94 |
95 | ---
96 |
97 | ## License
98 |
99 | Licensed under either of Apache License, Version
100 | 2.0 or MIT license at your option.
101 | Unless you explicitly state otherwise, any contribution intentionally submitted
102 | for inclusion in this project by you, as defined in the Apache-2.0 license,
103 | shall be dual licensed as above, without any additional terms or conditions.
104 |
--------------------------------------------------------------------------------
/app/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to the penguin **CLI app** will be documented here.
4 |
5 |
6 | ## [Unreleased]
7 |
8 |
9 | ## [0.2.6] - 2022-11-26
10 | - Improve `cargo doc` workflow by treating `remove` file system events as less important in watcher. Adds `--removal-debounce` flag. See [385b63](https://github.com/LukasKalbertodt/penguin/commit/385b6395142aff28fa5063162a8023e1392b0cf1).
11 | - Updated the library to v0.1.8 ⇒ [check its changelog](../lib/CHANGELOG.md#017---2022-06-22).
12 | - Add basic HTTP range request support for the file server. With this, video files served by Penguin can be played by Safari.
13 | - Add body sniffing to detect HTML content (and insert reload script) more often (see #11)
14 |
15 | ## [0.2.5] - 2022-06-22
16 |
17 | - Updated the library to v0.1.7 ⇒ [check its changelog](../lib/CHANGELOG.md#017---2022-06-22).
18 |
19 | ## [0.2.4] - 2022-06-08
20 |
21 | - Updated the library to v0.1.6 ⇒ [check its changelog](../lib/CHANGELOG.md#016---2022-06-08).
22 | - Updated other dependencies.
23 |
24 | ## [0.2.3] - 2021-10-02
25 |
26 | - Add feature `vendored-openssl` to compile `openssl` from source
27 | [PR #10](https://github.com/LukasKalbertodt/penguin/pull/10) (Thanks @philipahlberg)
28 | - Updated the library to v0.1.5 ⇒ [check its changelog](../lib/CHANGELOG.md#014---2021-09-02).
29 | - Updated other dependencies.
30 |
31 | ## [0.2.2] - 2021-10-02
32 |
33 | - Updated the library to v0.1.4 ⇒ [check its changelog](../lib/CHANGELOG.md#014---2021-09-02).
34 | - Updated other dependencies.
35 |
36 |
37 | ## [0.2.1] - 2021-07-18
38 |
39 | Updated the library to v0.1.3 ⇒ [check its changelog](../lib/CHANGELOG.md#013---2021-07-18).
40 |
41 |
42 | ## [0.2.0] - 2021-05-11
43 |
44 | Updated the library to v0.1.2 ⇒ [check its changelog](../lib/CHANGELOG.md#012---2021-05-10).
45 |
46 | ### Breaking
47 | - Mounted file system paths are now automatically watched for file changes. The
48 | browser sessions will reload automatically if anything changes.
49 |
50 | ### Added
51 | - Add `--open` flag to open the browser automatically.
52 | - Add `--no-auto-watch` flag to disable auto watch behavior.
53 | - Add `-w/--watch` option to specify additional watched paths.
54 | - Add `--debounce` flag to set the debounce duration for watched paths.
55 |
56 |
57 | ## 0.1.0 - 2021-03-03
58 | ### Added
59 | - Everything
60 |
61 |
62 | [Unreleased]: https://github.com/LukasKalbertodt/penguin/compare/app-v0.2.6...HEAD
63 | [0.2.6]: https://github.com/LukasKalbertodt/penguin/compare/app-v0.2.5...app-v0.2.6
64 | [0.2.5]: https://github.com/LukasKalbertodt/penguin/compare/app-v0.2.4...app-v0.2.5
65 | [0.2.4]: https://github.com/LukasKalbertodt/penguin/compare/app-v0.2.3...app-v0.2.4
66 | [0.2.3]: https://github.com/LukasKalbertodt/penguin/compare/app-v0.2.2...app-v0.2.3
67 | [0.2.2]: https://github.com/LukasKalbertodt/penguin/compare/app-v0.2.1...app-v0.2.2
68 | [0.2.1]: https://github.com/LukasKalbertodt/penguin/compare/app-v0.2.0...app-v0.2.1
69 | [0.2.0]: https://github.com/LukasKalbertodt/penguin/compare/app-v0.1.0...app-v0.2.0
70 |
--------------------------------------------------------------------------------
/app/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "penguin-app"
3 | version = "0.2.6"
4 | authors = ["Lukas Kalbertodt "]
5 | edition = "2018"
6 |
7 | description = """
8 | Dev server with auto-reload, static file server, proxy support, and more.
9 | Language and framework agnostic. This is the CLI app, but Penguin exists
10 | as a library, too.
11 | """
12 | repository = "https://github.com/LukasKalbertodt/penguin/"
13 | readme = "../README.md"
14 | license = "MIT/Apache-2.0"
15 |
16 | keywords = ["development", "autoreload", "devserver"]
17 | categories = ["development-tools", "command-line-utilities", "web-programming::http-server"]
18 |
19 |
20 | [[bin]]
21 | name = "penguin"
22 | path = "src/main.rs"
23 |
24 |
25 | [dependencies]
26 | anyhow = "1"
27 | bunt = "0.2.4"
28 | log = "0.4"
29 | notify = "4"
30 | open = "2"
31 | penguin = { version = "0.1.8", path = "../lib" }
32 | pretty_env_logger = "0.4"
33 | structopt = "0.3"
34 | tokio = { version = "1", features = ["rt", "macros"]}
35 |
36 | [features]
37 | vendored-openssl = ["penguin/vendored-openssl"]
38 |
--------------------------------------------------------------------------------
/app/src/args.rs:
--------------------------------------------------------------------------------
1 | use std::{net::IpAddr, path::{Path, PathBuf}, time::Duration};
2 |
3 | use log::LevelFilter;
4 |
5 | use structopt::StructOpt;
6 | use penguin::{Mount, ProxyTarget};
7 |
8 | pub(crate) const DEFAULT_PORT: u16 = 4090;
9 |
10 | #[derive(Debug, StructOpt)]
11 | #[structopt(
12 | name = "Penguin",
13 | about = "Language-agnostic dev server that can serve directories and forward \
14 | requests to a proxy.",
15 | setting(structopt::clap::AppSettings::VersionlessSubcommands),
16 | )]
17 | pub(crate) struct Args {
18 | /// Port of the Penguin server.
19 | #[structopt(short, long, default_value = "4090", global = true)]
20 | pub(crate) port: u16,
21 |
22 | /// Address to bind to.
23 | ///
24 | /// Mostly useful to set to "0.0.0.0" to let other devices in your network
25 | /// access the server.
26 | #[structopt(long, default_value = "127.0.0.1", global = true)]
27 | pub(crate) bind: IpAddr,
28 |
29 | /// Overrides the default control path '/~~penguin' with a custom path.
30 | ///
31 | /// Only useful you need to use '/~~penguin' in your own application.
32 | #[structopt(long, global = true)]
33 | pub(crate) control_path: Option,
34 |
35 | /// Quiet: `-q` for less output, `-qq` for no output.
36 | #[structopt(short, global = true, parse(from_occurrences))]
37 | pub(crate) quiet: u8,
38 |
39 | /// Sets the log level: trace, debug, info, warn, error or off.
40 | ///
41 | /// This value is only used if `RUST_LOG` is NOT set. If it is, the
42 | /// environment variable controls everything.
43 | #[structopt(short, long, global = true, default_value = "warn")]
44 | pub(crate) log_level: LevelFilter,
45 |
46 | /// Automatically opens the browser with the URL of this server.
47 | #[structopt(long, global = true)]
48 | pub(crate) open: bool,
49 |
50 | #[structopt(subcommand)]
51 | pub(crate) cmd: Command,
52 | }
53 |
54 | #[derive(Debug, StructOpt)]
55 | pub(crate) enum Command {
56 | /// Serve the specified directory as file server.
57 | ///
58 | /// You can mount more directories via '--mount'. If you don't specify a
59 | /// main directory for this subcommand, you have to mount at least one
60 | /// directory via '--mount'.
61 | ///
62 | /// Like with `--mount`, the directory specified here will be watched for
63 | /// file changes to automatically reload browser sessions. You can disable
64 | /// that with `--no-auto-watch`.
65 | Serve {
66 | #[structopt(parse(from_os_str))]
67 | path: Option,
68 |
69 | #[structopt(flatten)]
70 | options: ServeOptions,
71 | },
72 |
73 | /// Starts a server forwarding all request to the specified target address.
74 | Proxy {
75 | target: ProxyTarget,
76 |
77 | #[structopt(flatten)]
78 | options: ServeOptions,
79 | },
80 |
81 | /// Reloads all browser sessions.
82 | ///
83 | /// This sends a reload request to a locally running penguin server. The
84 | /// port and control path can be specified, if they are non-standard.
85 | Reload,
86 | }
87 |
88 | #[derive(Debug, Clone, StructOpt)]
89 | pub(crate) struct ServeOptions {
90 | /// Mount a directory on an URI path: '--mount :'.
91 | ///
92 | /// Example: '--mount assets:/home/peter/images'. Can be specified multiple
93 | /// times. If you only want to mount one directory in the root, rather use
94 | /// the `penguin serve` subcommand.
95 | ///
96 | /// By default, directories specified here will be watched for file changes
97 | /// to automatically reload browser sessions. You can disable that with
98 | /// `--no-auto-watch`.
99 | #[structopt(
100 | short,
101 | long = "--mount",
102 | number_of_values = 1,
103 | parse(try_from_str = parse_mount),
104 | )]
105 | pub(crate) mounts: Vec,
106 |
107 | /// When specified, penguin will not automatically watch the mounted paths.
108 | #[structopt(long)]
109 | pub(crate) no_auto_watch: bool,
110 |
111 | /// Watch a path for file system changes, triggering a reload.
112 | ///
113 | /// Note that mounted paths are already watched by default.
114 | #[structopt(short, long = "--watch", number_of_values = 1)]
115 | pub(crate) watched_paths: Vec,
116 |
117 | /// The debounce duration (in ms) for watching paths.
118 | ///
119 | /// Debouncing means that if a watch-event arrived, we are not immediately
120 | /// triggering a reload. Instead we wait for this duration and see if any
121 | /// other events arrive during this period. Whenever an event arrives, we
122 | /// reset the timer (so we could wait indefinitely).
123 | #[structopt(
124 | long = "--debounce",
125 | default_value = "200",
126 | parse(try_from_str = parse_duration)
127 | )]
128 | pub(crate) debounce_duration: Duration,
129 |
130 | /// The debounce duration (in ms) for when a file in a watched path was
131 | /// removed.
132 | ///
133 | /// Like `--debounce`, but for "remove" file system events. This is treated
134 | /// separately as usually reloading quickly on deletion is not useful: the
135 | /// reload would result in a 404 page. And in many situation (e.g. cargo
136 | /// doc) the watched directory is first wiped by a build process and then
137 | /// populated again after a while. So this default is much higher.
138 | #[structopt(
139 | long = "--removal-debounce",
140 | default_value = "3000",
141 | parse(try_from_str = parse_duration)
142 | )]
143 | pub(crate) removal_debounce_duration: Duration,
144 | }
145 |
146 | fn parse_mount(s: &str) -> Result {
147 | let colon_pos = s.find(':').ok_or("does not contain a colon")?;
148 | let fs_path = Path::new(&s[colon_pos + 1..]).to_owned();
149 |
150 | let mut uri_path = s[..colon_pos].to_owned();
151 | if !uri_path.starts_with('/') {
152 | uri_path.insert(0, '/');
153 | }
154 | if uri_path.ends_with('/') && uri_path.len() > 1 {
155 | uri_path.pop();
156 | }
157 |
158 | Ok(Mount { uri_path, fs_path})
159 | }
160 |
161 | fn parse_duration(s: &str) -> Result {
162 | let ms = s.parse::().map_err(|_| "failed to parse as positive integer")?;
163 | Ok(Duration::from_millis(ms))
164 | }
165 |
166 | impl Args {
167 | pub(crate) fn is_quiet(&self) -> bool {
168 | self.quiet > 0
169 | }
170 |
171 | pub(crate) fn is_muted(&self) -> bool {
172 | self.quiet == 2
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/app/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::{env, iter};
2 |
3 | use anyhow::{Context, Result};
4 | use log::LevelFilter;
5 | use penguin::{Mount, hyper::{Body, Client, Request}};
6 | use structopt::StructOpt;
7 |
8 | use crate::args::{Args, Command};
9 |
10 | mod args;
11 | mod server;
12 |
13 |
14 | // A single thread runtime is plenty enough for a webserver purpose.
15 | #[tokio::main(flavor = "current_thread")]
16 | async fn main() {
17 | if let Err(e) = run().await {
18 | let header = "An error occured :-(";
19 | let line = iter::repeat('━').take(header.len() + 4).collect::();
20 |
21 | eprintln!();
22 | bunt::eprintln!(" {$yellow+intense}┏{}┓{/$}", line);
23 | bunt::eprintln!(" {$yellow+intense}┃{/$} {[red+bold]} {$yellow+intense}┃{/$}", header);
24 | bunt::eprintln!(" {$yellow+intense}┗{}┛{/$}", line);
25 | eprintln!();
26 |
27 | bunt::eprintln!("{[red+intense]}", e);
28 | if e.chain().count() > 1 {
29 | eprintln!();
30 | eprintln!("Caused by:");
31 | for cause in e.chain().skip(1) {
32 | bunt::eprintln!(" ‣ {}", cause);
33 | }
34 | }
35 | }
36 | }
37 |
38 | async fn run() -> Result<()> {
39 | if env::args().count() == 1 {
40 | print_welcome_message();
41 | return Ok(());
42 | }
43 |
44 | // Parse CLI arguments.
45 | let args = Args::from_args();
46 |
47 | init_logger(args.log_level);
48 |
49 | match &args.cmd {
50 | Command::Proxy { target, options } => {
51 | server::run(Some(target), &options.mounts, options, &args)
52 | .await
53 | .context("failed to run server")?;
54 | }
55 | Command::Serve { path, options } => {
56 | let root_mount = path.clone().map(|p| Mount { uri_path: "/".into(), fs_path: p });
57 | let mounts = options.mounts.iter().chain(&root_mount);
58 | server::run(None, mounts, options, &args).await.context("failed to run server")?;
59 | }
60 | Command::Reload => reload(&args).await.context("failed to send reload request")?,
61 | }
62 |
63 | Ok(())
64 | }
65 |
66 | fn init_logger(level: LevelFilter) {
67 | if env::var("RUST_LOG") == Err(env::VarError::NotPresent) {
68 | env::set_var("RUST_LOG", format!("penguin={}", level));
69 | }
70 |
71 | pretty_env_logger::init();
72 | }
73 |
74 | async fn reload(args: &Args) -> Result<()> {
75 | let uri = format!(
76 | "http://{}:{}{}/reload",
77 | args.bind,
78 | args.port,
79 | args.control_path.as_deref().unwrap_or(penguin::DEFAULT_CONTROL_PATH),
80 | );
81 |
82 | let req = Request::builder()
83 | .method("POST")
84 | .uri(&uri)
85 | .body(Body::empty())
86 | .expect("bug: failed to build request");
87 |
88 | if !args.is_quiet() {
89 | bunt::println!("Sending POST request to {[green]}", uri);
90 | }
91 |
92 | let client = Client::new();
93 | client.request(req).await
94 | .with_context(|| format!("failed to send request to '{}'", uri))?;
95 |
96 | if !args.is_quiet() {
97 | bunt::println!("{$green+bold}✔ done{/$}");
98 | }
99 |
100 | Ok(())
101 | }
102 |
103 | fn print_welcome_message() {
104 | bunt::println!("{$blue+bold+intense}Penguin 🐧{/$}");
105 | println!();
106 | println!("You have to specify a subcommand. Example usages:");
107 | bunt::println!(" ‣ Serve a directory: {$yellow}penguin serve ./target{/$}");
108 | bunt::println!(" ‣ Reload all browser sessions: {$yellow}penguin reload{/$}");
109 | bunt::println!(" ‣ Forward requests to proxy and serve one directory on a subpath:");
110 | bunt::println!(" {$yellow}penguin proxy localhost:8000 -m /assets:frontend/dist{/$}");
111 |
112 | println!();
113 | bunt::println!("For more information, run {$yellow}penguin -h{/$} for a short CLI overview");
114 | bunt::println!("or {$yellow}penguin --help{/$} for a detailed description.");
115 | }
116 |
--------------------------------------------------------------------------------
/app/src/server.rs:
--------------------------------------------------------------------------------
1 | use std::{env, ops::Deref, path::Path, thread, time::Duration};
2 |
3 | use anyhow::{Context, Result};
4 | use log::{debug, info, trace, LevelFilter};
5 | use notify::Op;
6 | use penguin::{Config, Controller, Mount, ProxyTarget, Server};
7 |
8 | use crate::args::{Args, DEFAULT_PORT, ServeOptions};
9 |
10 |
11 |
12 | pub(crate) async fn run(
13 | proxy: Option<&ProxyTarget>,
14 | mounts: impl Clone + IntoIterator- ,
15 | options: &ServeOptions,
16 | args: &Args,
17 | ) -> Result<()> {
18 | let bind_addr = (args.bind, args.port).into();
19 | let mut builder = Server::bind(bind_addr);
20 |
21 | for mount in mounts.clone() {
22 | builder = builder.add_mount(&mount.uri_path, &mount.fs_path)
23 | .context("failed to add mount")?;
24 | }
25 | if let Some(control_path) = &args.control_path {
26 | builder = builder.set_control_path(control_path);
27 | }
28 | if let Some(target) = proxy {
29 | builder = builder.proxy(target.clone())
30 | }
31 |
32 |
33 | let config = builder.validate().context("invalid penguin config")?;
34 | let (server, controller) = Server::build(config.clone());
35 |
36 | let watched_paths = if !options.no_auto_watch {
37 | mounts.into_iter()
38 | .map(|m| &*m.fs_path)
39 | .chain(options.watched_paths.iter().map(Deref::deref))
40 | .collect()
41 | } else if !options.watched_paths.is_empty() {
42 | options.watched_paths.iter().map(Deref::deref).collect()
43 | } else {
44 | vec![]
45 | };
46 |
47 | if !watched_paths.is_empty() {
48 | watch(controller, options, &watched_paths)?;
49 | }
50 |
51 | // Nice output of what is being done
52 | if !args.is_muted() {
53 | bunt::println!(
54 | "{$bold}Penguin started!{/$} Listening on {$yellow+intense+bold}http://{}{/$}",
55 | bind_addr,
56 | );
57 |
58 | if !args.is_quiet() {
59 | pretty_print_config(&config, args, &watched_paths);
60 | }
61 | }
62 |
63 | if args.open {
64 | // This is a bit hacky but it works and doing it properly is
65 | // surprisingly hard. We want to only open the browser if we were able
66 | // to start the server without problems (where 99% of anticipated
67 | // problems are: port is already in use). `hyper` doesn't quite allow us
68 | // to do that as far as I know. So we simply start a thread and wait a
69 | // bit. If starting the server errors, then the program (including this
70 | // thread) will be stopped quickly and the `open::that` call is never
71 | // executed.
72 | thread::spawn(move || {
73 | thread::sleep(Duration::from_millis(50));
74 |
75 | let url = format!("http://{}", bind_addr);
76 | match open::that(url) {
77 | Ok(_) => {}
78 | Err(e) => bunt::println!(
79 | "{$yellow}Warning{/$}: couldn't open browser. Error: {}",
80 | e,
81 | ),
82 | }
83 | });
84 | }
85 |
86 | server.await?;
87 |
88 | Ok(())
89 | }
90 |
91 | fn watch<'a>(
92 | controller: Controller,
93 | options: &ServeOptions,
94 | paths: &[&Path],
95 | ) -> Result<()> {
96 | use std::sync::mpsc::{channel, RecvTimeoutError};
97 | use notify::{RawEvent, RecursiveMode, Watcher};
98 |
99 | /// Helper to format an optional path in a nice way.
100 | fn pretty_path(event: &RawEvent) -> String {
101 | match &event.path {
102 | Some(p) => p.display().to_string(),
103 | None => "???".into(),
104 | }
105 | }
106 |
107 | // Create an configure watcher.
108 | let (tx, rx) = channel();
109 | let mut watcher = notify::raw_watcher(tx).context("could not create FS watcher")?;
110 |
111 | for path in paths {
112 | watcher.watch(path, RecursiveMode::Recursive)
113 | .context(format!("failed to watch '{}'", path.display()))?;
114 | }
115 |
116 | // We create a new thread that will react to incoming events and trigger a
117 | // page reload.
118 | let options = options.clone();
119 | thread::spawn(move || {
120 | // Move it to the thread to avoid dropping it early.
121 | let _watcher = watcher;
122 | let debounce_duration_of = |event: &RawEvent| {
123 | if event.op.as_ref().is_ok_and(|&op| op == Op::REMOVE) {
124 | options.removal_debounce_duration
125 | } else {
126 | options.debounce_duration
127 | }
128 | };
129 |
130 | while let Ok(event) = rx.recv() {
131 | let mut debounce_duration = debounce_duration_of(&event);
132 |
133 | debug!(
134 | "Received watch-event '{:?}' for '{}'. Debouncing now for {:?}.",
135 | event.op,
136 | pretty_path(&event),
137 | debounce_duration,
138 | );
139 |
140 | // Debounce. We loop forever until no new event arrived for
141 | // `debounce_duration`.
142 | loop {
143 | match rx.recv_timeout(debounce_duration) {
144 | Ok(event) => {
145 | trace!(
146 | "Debounce interrupted by '{:?}' of '{}'",
147 | event.op,
148 | pretty_path(&event),
149 | );
150 |
151 | // We reset the waiting duration to the minimum of both
152 | // events' durations. So if any non-remove event is
153 | // involved, the shorter duration is used.
154 | debounce_duration = std::cmp::min(
155 | debounce_duration_of(&event),
156 | debounce_duration,
157 | );
158 | },
159 | Err(RecvTimeoutError::Timeout) => break,
160 | Err(RecvTimeoutError::Disconnected) => return,
161 | }
162 | }
163 |
164 | // Finally, send a reload command
165 | info!("Reloading browser sessions due to file changes in watched directories");
166 | controller.reload();
167 | }
168 | });
169 |
170 | Ok(())
171 | }
172 |
173 | fn pretty_print_config(config: &Config, args: &Args, watched_paths: &[&Path]) {
174 | // Routing description
175 | println!();
176 | bunt::println!(" {$cyan+bold}▸ Routing:{/$}");
177 | bunt::println!(
178 | " ├╴ Requests to {[blue+intense]} are handled internally by penguin",
179 | config.control_path(),
180 | );
181 |
182 | for mount in config.mounts() {
183 | let fs_path = env::current_dir()
184 | .as_deref()
185 | .unwrap_or(Path::new("."))
186 | .join(&mount.fs_path);
187 |
188 | bunt::println!(
189 | " ├╴ Requests to {[blue+intense]} are served from the directory {[green]}",
190 | mount.uri_path,
191 | fs_path.display(),
192 | );
193 | }
194 |
195 | if let Some(proxy) = config.proxy() {
196 | bunt::println!(" ╰╴ All remaining requests are forwarded to {[green+intense]}", proxy);
197 | } else {
198 | bunt::println!(" ╰╴ All remaining requests will be responded to with 404");
199 | }
200 |
201 | if !watched_paths.is_empty() {
202 | println!();
203 | bunt::println!(" {$cyan+bold}▸ Watching:{/$} {$dimmed}(reloading on file change){/$}");
204 | for p in watched_paths {
205 | let canonical = p.canonicalize();
206 | bunt::println!(" • {[green]}", canonical.as_deref().unwrap_or(p).display());
207 | }
208 | }
209 |
210 | // Random hints
211 | println!();
212 | bunt::println!(" {$cyan+bold}▸ Hints:{/$}");
213 | bunt::println!(
214 | " • To reload all browser sessions, run {$yellow}penguin reload{}{}{/$}",
215 | if args.port != DEFAULT_PORT { format!(" -p {}", args.port) } else { "".into() },
216 | args.control_path.as_ref()
217 | .map(|p| format!(" --control-path {}", p))
218 | .unwrap_or_default(),
219 | );
220 | if args.log_level == LevelFilter::Warn {
221 | bunt::println!(
222 | " • For more log output use {$yellow}-l trace{/$} \
223 | or set the env variable {$yellow}RUST_LOG{/$}",
224 | );
225 | }
226 |
227 | println!();
228 | }
229 |
--------------------------------------------------------------------------------
/lib/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to the penguin **library** will be documented here.
4 |
5 |
6 | ## [Unreleased]
7 |
8 | ## [0.1.8] - 2023-11-26
9 | - Add basic HTTP range request support for the file server. With this, video files served by Penguin can be played by Safari.
10 | - Add body sniffing to detect HTML content (and insert reload script) more often (see #11)
11 |
12 | ## [0.1.7] - 2022-06-22
13 | ### Fixed
14 | - Fix 404, "gateway error" and dir-listing pages that were broken in the previous release. (The JS code wasn't injected correctly, showing up as plain text. Woops.)
15 |
16 | ## [0.1.6] - 2022-06-08
17 | ### Fixed
18 | - `Content-Security-Policy` (CSP) header is now potentially modified in proxy mode if required for penguin's injected script (`'self'` is potentially added to `script-src` and `connect-src`).
19 |
20 | ### Improved
21 | - Update dependencies (this bumps the MSRV to 1.56!)
22 |
23 | ## [0.1.5] - 2022-04-19
24 | ### Added
25 | - Add feature `vendored-openssl` to compile `openssl` from source
26 | [PR #10](https://github.com/LukasKalbertodt/penguin/pull/10) (Thanks @philipahlberg)
27 |
28 | ### Improved
29 | - Updated dependencies
30 |
31 | ## [0.1.4] - 2021-10-02
32 | ### Improved
33 | - Include reload script in 404 response: now the page can still reload itself
34 | after a 404 reply.
35 | - After a getting a gateway error, automatically reload all browser sessions
36 | once the proxy is reachable again. This is done by regularly polling the
37 | proxy from the Penguin server.
38 |
39 | ### Fixed
40 | - When using the proxy, the `host` HTTP-header is adjusted to the proxy target
41 | host (instead of the original `localhost:4090` that the browser sends).
42 | - Correctly handle compression in proxy: gzip and brotli compression is
43 | supported and the HTTP body is decompressed before the reload script is
44 | injected. This was just totally broken before. The `accept-encoding` header
45 | of the request is also adjusted to not list anything but `gzip` and `br`.
46 | - Rewrite `location` header to make HTTP redirects work with proxy.
47 |
48 | ## [0.1.3] - 2021-07-18
49 | ### Fixed
50 | - Fix bug resulting in endless reloading if the proxy is slow
51 | - Ignore one specific WS error that occurs often, is not important and caused
52 | lots of useless warnings
53 | - Correctly handle ping messages (also getting rid of useless warnings)
54 |
55 | ## [0.1.2] - 2021-05-10
56 | ### Added
57 | - All responses (except the ones forwarded from the proxy server) now contain
58 | the "server" HTTP header.
59 |
60 | ### Fixed
61 | - Make Penguin work with non-`127.0.0.1` loopback addresses.
62 | - Fix warning about directory traversal attack incorrectly being emitted.
63 |
64 | ## [0.1.1] - 2021-03-07
65 | ### Added
66 | - `util::wait_for_proxy`
67 |
68 | ### Changed
69 | - If the server cannot bind to the port, an error is returned from the server
70 | future instead of panicking.
71 |
72 |
73 | ## 0.1.0 - 2021-03-03
74 | ### Added
75 | - Everything
76 |
77 |
78 | [Unreleased]: https://github.com/LukasKalbertodt/penguin/compare/lib-v0.1.8...HEAD
79 | [0.1.8]: https://github.com/LukasKalbertodt/penguin/compare/lib-v0.1.7...lib-v0.1.8
80 | [0.1.7]: https://github.com/LukasKalbertodt/penguin/compare/lib-v0.1.6...lib-v0.1.7
81 | [0.1.6]: https://github.com/LukasKalbertodt/penguin/compare/lib-v0.1.5...lib-v0.1.6
82 | [0.1.5]: https://github.com/LukasKalbertodt/penguin/compare/lib-v0.1.4...lib-v0.1.5
83 | [0.1.4]: https://github.com/LukasKalbertodt/penguin/compare/lib-v0.1.3...lib-v0.1.4
84 | [0.1.3]: https://github.com/LukasKalbertodt/penguin/compare/lib-v0.1.2...lib-v0.1.3
85 | [0.1.2]: https://github.com/LukasKalbertodt/penguin/compare/lib-v0.1.1...lib-v0.1.2
86 | [0.1.1]: https://github.com/LukasKalbertodt/penguin/compare/lib-v0.1.0...lib-v0.1.1
87 |
--------------------------------------------------------------------------------
/lib/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "penguin"
3 | version = "0.1.8"
4 | authors = ["Lukas Kalbertodt "]
5 | edition = "2018"
6 |
7 | description = """
8 | Dev server with auto-reload, static file server, proxy support, and more.
9 | Language and framework agnostic. This is the library crate, but Penguin exists
10 | as a CLI app, too.
11 | """
12 | documentation = "https://docs.rs/penguin/"
13 | repository = "https://github.com/LukasKalbertodt/penguin/"
14 | readme = "../README.md"
15 | license = "MIT/Apache-2.0"
16 |
17 | keywords = ["development", "autoreload", "devserver"]
18 | categories = ["development-tools", "web-programming::http-server"]
19 | exclude = ["Cargo.lock"]
20 |
21 |
22 | [dependencies]
23 | brotli = "3.2"
24 | flate2 = "1.0.22"
25 | futures = "0.3"
26 | http-range = "0.1.5"
27 | hyper = { version = "0.14", features = ["client", "http1", "http2", "server", "stream", "tcp"] }
28 | hyper-tls = "0.5"
29 | hyper-tungstenite = "0.8"
30 | infer = "0.15.0"
31 | log = "0.4"
32 | mime_guess = "2"
33 | thiserror = "1"
34 | tokio = { version = "1", features = ["fs", "macros"] }
35 | tokio-util = { version = "0.7.3", features = ["codec"] }
36 |
37 | [dev-dependencies]
38 | tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"]}
39 |
40 | [features]
41 | vendored-openssl = ["hyper-tls/vendored"]
42 |
--------------------------------------------------------------------------------
/lib/build.rs:
--------------------------------------------------------------------------------
1 | use std::{error::Error, path::{Path, PathBuf}, process::Command};
2 |
3 |
4 | // This build script compiles the Typescript code.
5 | fn main() -> Result<(), Box> {
6 | println!("cargo:rerun-if-changed=src/browser.ts");
7 |
8 | let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
9 | let infile = manifest_dir.join("src").join("browser.ts");
10 | let outfile = manifest_dir.join("src").join("generated").join("browser.js");
11 |
12 | // Cargo already just calls this script when the `.ts` file was changed.
13 | // However, we add this extra check to make sure we don't try to compile it
14 | // again if it's not necessary. This means that devs checking out the repo
15 | // or `cargo install`ing penguin don't need to have `tsc` installed (since
16 | // the generated file is checked into git).
17 | let need_compiling = !outfile.exists()
18 | || infile.metadata()?.modified()? > outfile.metadata()?.modified()?;
19 |
20 | if !need_compiling {
21 | return Ok(());
22 | }
23 |
24 | // Figure out which `tsc` to use. Prefer a locally installed one but if
25 | // that's not present, try a global `tsc`.
26 | let local_tsc = manifest_dir
27 | .join("node_modules")
28 | .join("typescript")
29 | .join("bin")
30 | .join("tsc");
31 |
32 | let tsc = if local_tsc.exists() {
33 | &local_tsc
34 | } else {
35 | Path::new("tsc")
36 | };
37 |
38 | // Run `tsc` and check the status.
39 | let status = Command::new(tsc)
40 | .current_dir(&manifest_dir)
41 | .arg("--pretty")
42 | .status();
43 | match status {
44 | Err(e) => {
45 | eprintln!("Error executing `tsc`.");
46 | if !local_tsc.exists() {
47 | eprintln!("You might need to run `npm install` in the `lib` folder");
48 | }
49 | Err(e)?;
50 | }
51 | Ok(status) if !status.success() => {
52 | Err("`tsc` reported errors.")?;
53 | }
54 | Ok(_) => {}
55 | }
56 |
57 | Ok(())
58 | }
59 |
--------------------------------------------------------------------------------
/lib/examples/main.rs:
--------------------------------------------------------------------------------
1 | use std::path::Path;
2 |
3 | use penguin::Server;
4 |
5 |
6 | #[tokio::main]
7 | async fn main() -> Result<(), Box> {
8 | let (server, _controller) = Server::bind(([127, 0, 0, 1], 3001).into())
9 | .add_mount("/", Path::new("."))?
10 | .build()?;
11 |
12 | // // Dummy code to regularly reload all sessions.
13 | // tokio::spawn(async move {
14 | // let mut interval = tokio::time::interval(std::time::Duration::from_secs(3));
15 | // loop {
16 | // interval.tick().await;
17 | // controller.reload();
18 | // }
19 | // });
20 |
21 | server.await?;
22 |
23 | Ok(())
24 | }
25 |
--------------------------------------------------------------------------------
/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "penguin",
3 | "version": "0.0.0",
4 | "private": true,
5 | "devDependencies": {
6 | "typescript": "^4.1.5"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/lib/src/assets/dir-listing.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Contents of directory {{ uri_path }}
4 |
21 |
22 |
23 |
Contents of directory {{ uri_path }}
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/lib/src/assets/not-found.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Not found
6 |
11 |
12 |
13 | 404 – Not found
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/lib/src/assets/proxy-error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Failed to connect to the proxy target.
4 |
5 |
6 | Failed to connect to the proxy target.
7 | {{ error }}
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/lib/src/browser.ts:
--------------------------------------------------------------------------------
1 | // This code was inserted by the 'penguin' library. It's here to enable features
2 | // like browser auto-reloading or showing messages. It does this by
3 | // communicating with the penguin server via a websocket.
4 | //
5 |
6 | // Configuration dependent values that are passed/interpolated by the penguin
7 | // server.
8 | const control_path = "{{ control_path }}";
9 |
10 |
11 | // The target URI of the websocket connection.
12 | const wsUri = (() => {
13 | const scheme = window.location.protocol === "https" ? "wss" : "ws";
14 | const host = window.location.host;
15 | return `${scheme}://${host}${control_path}`;
16 | })();
17 |
18 | // Open websocket connection and install handlers.
19 | const socket = new WebSocket(wsUri);
20 | socket.addEventListener("close", onConnectionError);
21 | socket.addEventListener("open", () => {
22 | socket.removeEventListener("close", onConnectionError)
23 |
24 | socket.addEventListener("close", () => {
25 | console.log("penguin server closed WS connection: trying to reconnect...");
26 | tryReconnect();
27 | });
28 | socket.addEventListener("message", onMessage);
29 | });
30 |
31 |
32 | function tryReconnect() {
33 | const DELAY_BETWEEN_RETRIES = 2000;
34 | const RETRY_COUNT_BEFORE_GIVING_UP = 30;
35 |
36 | function connect(unregister: () => void) {
37 | const socket = new WebSocket(wsUri);
38 | socket.addEventListener("open", () => {
39 | console.log("Reestablished connection: reloading...");
40 | unregister();
41 | location.reload();
42 | });
43 | }
44 |
45 | function retryRegularlyForAWhile() {
46 | let count = 0;
47 | const interval = setInterval(() => {
48 | connect(() => clearInterval(interval));
49 |
50 | count += 1;
51 | if (count > RETRY_COUNT_BEFORE_GIVING_UP) {
52 | clearInterval(interval);
53 | }
54 | }, DELAY_BETWEEN_RETRIES);
55 | }
56 |
57 | // We immediately start trying to reconnect in a loop, but stop after a
58 | // while to not waste system resources. But we also check for visibility
59 | // changes. Whenever the page visibility changes to "visible", we
60 | // immediately retry and also start the retry loop again.
61 | retryRegularlyForAWhile();
62 | const onVisibilityChange = () => {
63 | if (document.visibilityState === "visible") {
64 | connect(() => document.removeEventListener("visibilitychange", onVisibilityChange));
65 | retryRegularlyForAWhile();
66 | }
67 | };
68 | document.addEventListener("visibilitychange", onVisibilityChange);
69 | }
70 |
71 | function onConnectionError() {
72 | console.warn(`Could not connect to web socket backend ${wsUri}`);
73 | }
74 |
75 | function onMessage(event: MessageEvent) {
76 | if (typeof event.data !== 'string') {
77 | throw new Error("unexpected WS message from penguin");
78 | }
79 |
80 | const endLine = event.data.indexOf('\n');
81 | const command = event.data.slice(0, endLine === -1 ? undefined : endLine);
82 | const payload = endLine === - 1 ? "" : event.data.slice(endLine + 1);
83 |
84 | switch (command) {
85 | case "reload":
86 | console.log("Received reload request from penguin server: reloading page...");
87 | location.reload();
88 | break;
89 |
90 | case "message":
91 | showMessage(payload);
92 | break;
93 |
94 | default:
95 | throw new Error("unexpected WS command from penguin");
96 | }
97 | }
98 |
99 | function showMessage(message: string) {
100 | let overlay = document.createElement("div");
101 |
102 | // We encode '✖' as escape code to make this work with non-UTF8 HTML.
103 | let closeButton = document.createElement("button");
104 | closeButton.innerText = "Close \u2716";
105 | closeButton.style.fontSize = "20px";
106 | closeButton.style.fontFamily = "sans-serif";
107 | closeButton.style.display = "inline-block";
108 | closeButton.style.cursor = "pointer";
109 | closeButton.addEventListener("click", () => overlay.style.display = "none");
110 |
111 | let header = document.createElement("div");
112 | header.style.textAlign = "right";
113 | header.style.margin = "8px";
114 | header.appendChild(closeButton);
115 |
116 | let content = document.createElement("div");
117 | content.innerHTML = message;
118 | content.style.margin = "16px";
119 | content.style.height = "100%";
120 |
121 | overlay.appendChild(header);
122 | overlay.appendChild(content);
123 | overlay.style.position= "fixed";
124 | overlay.style.zIndex = "987654321"; // Arbitrary very large number
125 | overlay.style.height = "100vh";
126 | overlay.style.width = "100vw";
127 | overlay.style.top = "0";
128 | overlay.style.left = "0";
129 | overlay.style.backgroundColor = "#ebebeb";
130 |
131 | document.body.prepend(overlay);
132 | }
133 |
--------------------------------------------------------------------------------
/lib/src/config.rs:
--------------------------------------------------------------------------------
1 | use std::{fmt, net::{IpAddr, SocketAddr}, path::PathBuf, str::FromStr};
2 |
3 | use hyper::{Uri, http::uri};
4 |
5 | use crate::{Controller, Server};
6 |
7 |
8 | /// The URI path which is used for penguin internal control functions (e.g.
9 | /// opening WS connections).
10 | ///
11 | /// We need a path that:
12 | /// - is unlikely to clash with real paths of existing web applications,
13 | /// - is still somewhat easy to type and remember (e.g. to send requests via
14 | /// `curl`), and
15 | /// - doesn't use any invalid characters for URLs.
16 | pub const DEFAULT_CONTROL_PATH: &str = "/~~penguin";
17 |
18 | /// A valid penguin server configuration.
19 | ///
20 | /// To create a configuration, use [`Server::bind`] to obtain a [`Builder`]
21 | /// which can be turned into a `Config`.
22 | #[derive(Debug, Clone)]
23 | pub struct Config {
24 | /// The port/socket address the server should be listening on.
25 | pub(crate) bind_addr: SocketAddr,
26 |
27 | /// Proxy target that HTTP requests should be forwarded to.
28 | pub(crate) proxy: Option,
29 |
30 | /// A list of directories to serve as a file server. As expected from other
31 | /// file servers, this lists the contents of directories and serves files
32 | /// directly. HTML files are injected with the penguin JS code.
33 | pub(crate) mounts: Vec,
34 |
35 | /// HTTP requests to this path are interpreted by this library to perform
36 | /// its function and are not normally served via the reverse proxy or the
37 | /// static file server.
38 | ///
39 | /// Has to start with `/` and *not* include the trailing `/`.
40 | pub(crate) control_path: String,
41 | }
42 |
43 | impl Config {
44 | pub fn proxy(&self) -> Option<&ProxyTarget> {
45 | self.proxy.as_ref()
46 | }
47 |
48 | pub fn mounts(&self) -> &[Mount] {
49 | &self.mounts
50 | }
51 |
52 | pub fn control_path(&self) -> &str {
53 | &self.control_path
54 | }
55 | }
56 |
57 | /// Builder for the configuration of `Server`.
58 | #[derive(Debug, Clone)]
59 | pub struct Builder(Config);
60 |
61 | impl Builder {
62 | /// Creates a new configuration. The `bind_addr` is what the server will
63 | /// listen on.
64 | pub(crate) fn new(bind_addr: SocketAddr) -> Self {
65 | Self(Config {
66 | bind_addr,
67 | proxy: None,
68 | control_path: DEFAULT_CONTROL_PATH.into(),
69 | mounts: Vec::new(),
70 | })
71 | }
72 |
73 | /// Enables and sets a proxy: incoming requests (that do not match a mount)
74 | /// are forwarded to the given proxy target and its response is forwarded
75 | /// back to the initiator of the request.
76 | ///
77 | /// **Panics** if this method is called more than once on a single
78 | /// `Builder`.
79 | pub fn proxy(mut self, target: ProxyTarget) -> Self {
80 | if let Some(prev) = self.0.proxy {
81 | panic!(
82 | "`Builder::proxy` called a second time: is called with '{}' now \
83 | but was previously called with '{}'",
84 | target,
85 | prev,
86 | );
87 | }
88 |
89 | self.0.proxy = Some(target);
90 | self
91 | }
92 |
93 | /// Adds a mount: a directory to be served via file server under `uri_path`.
94 | /// The order in which the serve dirs are added does not matter. When
95 | /// serving a request, the most specific matching entry "wins".
96 | ///
97 | /// This method returns `ConfigError::DuplicateUriPath` if the same
98 | /// `uri_path` was added before.
99 | pub fn add_mount(
100 | mut self,
101 | uri_path: impl Into,
102 | fs_path: impl Into,
103 | ) -> Result {
104 | let mut uri_path = uri_path.into();
105 | normalize_path(&mut uri_path);
106 |
107 | if self.0.mounts.iter().any(|other| other.uri_path == uri_path) {
108 | return Err(ConfigError::DuplicateUriPath(uri_path));
109 | }
110 |
111 | self.0.mounts.push(Mount {
112 | uri_path,
113 | fs_path: fs_path.into(),
114 | });
115 |
116 | Ok(self)
117 | }
118 |
119 | /// Overrides the control path (`/~~penguin` by default) with a custom path.
120 | ///
121 | /// This is only useful if your web application wants to use the route
122 | /// `/~~penguin`.
123 | pub fn set_control_path(mut self, path: impl Into) -> Self {
124 | self.0.control_path = path.into();
125 | normalize_path(&mut self.0.control_path);
126 | self
127 | }
128 |
129 | /// Validates the configuration and builds the server and controller from
130 | /// it. This is a shortcut for [`Builder::validate`] plus [`Server::build`].
131 | pub fn build(self) -> Result<(Server, Controller), ConfigError> {
132 | self.validate().map(Server::build)
133 | }
134 |
135 | /// Validates the configuration and returns the finished [`Config`].
136 | pub fn validate(self) -> Result {
137 | if self.0.proxy.is_none() && self.0.mounts.is_empty() {
138 | return Err(ConfigError::NoProxyOrMount)
139 | }
140 |
141 | if self.0.proxy.is_some() && self.0.mounts.iter().any(|other| other.uri_path == "/") {
142 | return Err(ConfigError::ProxyAndRootMount);
143 | }
144 |
145 | Ok(self.0)
146 | }
147 | }
148 |
149 | fn normalize_path(path: &mut String) {
150 | if path.len() > 1 && path.ends_with('/') {
151 | path.pop();
152 | }
153 | if !path.starts_with('/') {
154 | path.insert(0, '/');
155 | }
156 | }
157 |
158 | /// Configuration validation error.
159 | #[derive(Debug, thiserror::Error)]
160 | #[non_exhaustive]
161 | pub enum ConfigError {
162 | #[error("URI path '{0}' was added as mount twice")]
163 | DuplicateUriPath(String),
164 |
165 | #[error("a proxy was configured but a mount on '/' was added as well (in \
166 | that case, the proxy is would be ignored)")]
167 | ProxyAndRootMount,
168 |
169 | #[error("neither a proxy nor a mount was specified: server would always \
170 | respond 404 in this case")]
171 | NoProxyOrMount,
172 | }
173 |
174 | /// Defintion of a proxy target consisting of a scheme and authority (≈host).
175 | ///
176 | /// To create this type you can:
177 | /// - use the `FromStr` impl: `"http://localhost:8000".parse()`, or
178 | /// - use the `From<(Scheme, Authority)>` impl.
179 | ///
180 | /// The `FromStr` allows omitting the scheme ('http' or 'https') if the host is
181 | /// `"localhost"` or a loopback address and defaults to 'http' in that case. For
182 | /// all other hosts, the scheme has to be specified.
183 | #[derive(Debug, Clone, PartialEq, Eq)]
184 | pub struct ProxyTarget {
185 | pub(crate) scheme: uri::Scheme,
186 | pub(crate) authority: uri::Authority,
187 | }
188 |
189 | impl From<(uri::Scheme, uri::Authority)> for ProxyTarget {
190 | fn from((scheme, authority): (uri::Scheme, uri::Authority)) -> Self {
191 | Self { scheme, authority }
192 | }
193 | }
194 |
195 | impl fmt::Display for ProxyTarget {
196 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
197 | write!(f, "{}://{}", self.scheme, self.authority)
198 | }
199 | }
200 |
201 | impl FromStr for ProxyTarget {
202 | type Err = ProxyTargetParseError;
203 | fn from_str(src: &str) -> Result {
204 | let parts = src.parse::()?.into_parts();
205 | let has_real_path = parts.path_and_query.as_ref()
206 | .map_or(false, |pq| !pq.as_str().is_empty() && pq.as_str() != "/");
207 | if has_real_path {
208 | return Err(ProxyTargetParseError::HasPath);
209 | }
210 |
211 | let authority = parts.authority.ok_or(ProxyTargetParseError::MissingAuthority)?;
212 | let scheme = parts.scheme
213 | .or_else(|| {
214 | // If the authority is a loopback IP or "localhost", we default to HTTP as scheme.
215 | let ip = authority.host().parse::();
216 | if authority.host() == "localhost" || ip.map_or(false, |ip| ip.is_loopback()) {
217 | Some(uri::Scheme::HTTP)
218 | } else {
219 | None
220 | }
221 | })
222 | .ok_or(ProxyTargetParseError::MissingScheme)?;
223 |
224 | Ok(Self { scheme, authority })
225 | }
226 | }
227 |
228 | /// Error that can occur when parsing a `ProxyTarget` from a string.
229 | #[derive(Debug, thiserror::Error)]
230 | #[non_exhaustive]
231 | pub enum ProxyTargetParseError {
232 | /// The string could not be parsed as `http::Uri`.
233 | #[error("invalid URI: {0}")]
234 | InvalidUri(#[from] uri::InvalidUri),
235 |
236 | /// The parsed URL has a path, but a proxy target must not have a path.
237 | #[error("proxy target has path which is not allowed")]
238 | HasPath,
239 |
240 | /// The URI does not have a scheme ('http' or 'https') specified when it
241 | /// should have.
242 | #[error("proxy target has no scheme ('http' or 'https') specified, but a \
243 | scheme must be specified for non-local targets")]
244 | MissingScheme,
245 |
246 | /// The URI does not have an authority (≈ "host"), but it needs one.
247 | #[error("proxy target has no authority (\"host\") specified")]
248 | MissingAuthority,
249 | }
250 |
251 | /// A mapping from URI path to file system path.
252 | #[derive(Debug, Clone)]
253 | pub struct Mount {
254 | /// Path prefix of the URI that will map to the directory. Has to start with
255 | /// `/` and *not* include the trailing `/`.
256 | pub uri_path: String,
257 |
258 | /// Path to a directory on the file system that is served under the
259 | /// specified URI path.
260 | pub fs_path: PathBuf,
261 | }
262 |
--------------------------------------------------------------------------------
/lib/src/generated/browser.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | // This code was inserted by the 'penguin' library. It's here to enable features
3 | // like browser auto-reloading or showing messages. It does this by
4 | // communicating with the penguin server via a websocket.
5 | //
6 | // Configuration dependent values that are passed/interpolated by the penguin
7 | // server.
8 | const control_path = "{{ control_path }}";
9 | // The target URI of the websocket connection.
10 | const wsUri = (() => {
11 | const scheme = window.location.protocol === "https" ? "wss" : "ws";
12 | const host = window.location.host;
13 | return `${scheme}://${host}${control_path}`;
14 | })();
15 | // Open websocket connection and install handlers.
16 | const socket = new WebSocket(wsUri);
17 | socket.addEventListener("close", onConnectionError);
18 | socket.addEventListener("open", () => {
19 | socket.removeEventListener("close", onConnectionError);
20 | socket.addEventListener("close", () => {
21 | console.log("penguin server closed WS connection: trying to reconnect...");
22 | tryReconnect();
23 | });
24 | socket.addEventListener("message", onMessage);
25 | });
26 | function tryReconnect() {
27 | const DELAY_BETWEEN_RETRIES = 2000;
28 | const RETRY_COUNT_BEFORE_GIVING_UP = 30;
29 | function connect(unregister) {
30 | const socket = new WebSocket(wsUri);
31 | socket.addEventListener("open", () => {
32 | console.log("Reestablished connection: reloading...");
33 | unregister();
34 | location.reload();
35 | });
36 | }
37 | function retryRegularlyForAWhile() {
38 | let count = 0;
39 | const interval = setInterval(() => {
40 | connect(() => clearInterval(interval));
41 | count += 1;
42 | if (count > RETRY_COUNT_BEFORE_GIVING_UP) {
43 | clearInterval(interval);
44 | }
45 | }, DELAY_BETWEEN_RETRIES);
46 | }
47 | // We immediately start trying to reconnect in a loop, but stop after a
48 | // while to not waste system resources. But we also check for visibility
49 | // changes. Whenever the page visibility changes to "visible", we
50 | // immediately retry and also start the retry loop again.
51 | retryRegularlyForAWhile();
52 | const onVisibilityChange = () => {
53 | if (document.visibilityState === "visible") {
54 | connect(() => document.removeEventListener("visibilitychange", onVisibilityChange));
55 | retryRegularlyForAWhile();
56 | }
57 | };
58 | document.addEventListener("visibilitychange", onVisibilityChange);
59 | }
60 | function onConnectionError() {
61 | console.warn(`Could not connect to web socket backend ${wsUri}`);
62 | }
63 | function onMessage(event) {
64 | if (typeof event.data !== 'string') {
65 | throw new Error("unexpected WS message from penguin");
66 | }
67 | const endLine = event.data.indexOf('\n');
68 | const command = event.data.slice(0, endLine === -1 ? undefined : endLine);
69 | const payload = endLine === -1 ? "" : event.data.slice(endLine + 1);
70 | switch (command) {
71 | case "reload":
72 | console.log("Received reload request from penguin server: reloading page...");
73 | location.reload();
74 | break;
75 | case "message":
76 | showMessage(payload);
77 | break;
78 | default:
79 | throw new Error("unexpected WS command from penguin");
80 | }
81 | }
82 | function showMessage(message) {
83 | let overlay = document.createElement("div");
84 | // We encode '✖' as escape code to make this work with non-UTF8 HTML.
85 | let closeButton = document.createElement("button");
86 | closeButton.innerText = "Close \u2716";
87 | closeButton.style.fontSize = "20px";
88 | closeButton.style.fontFamily = "sans-serif";
89 | closeButton.style.display = "inline-block";
90 | closeButton.style.cursor = "pointer";
91 | closeButton.addEventListener("click", () => overlay.style.display = "none");
92 | let header = document.createElement("div");
93 | header.style.textAlign = "right";
94 | header.style.margin = "8px";
95 | header.appendChild(closeButton);
96 | let content = document.createElement("div");
97 | content.innerHTML = message;
98 | content.style.margin = "16px";
99 | content.style.height = "100%";
100 | overlay.appendChild(header);
101 | overlay.appendChild(content);
102 | overlay.style.position = "fixed";
103 | overlay.style.zIndex = "987654321"; // Arbitrary very large number
104 | overlay.style.height = "100vh";
105 | overlay.style.width = "100vw";
106 | overlay.style.top = "0";
107 | overlay.style.left = "0";
108 | overlay.style.backgroundColor = "#ebebeb";
109 | document.body.prepend(overlay);
110 | }
111 |
--------------------------------------------------------------------------------
/lib/src/inject.rs:
--------------------------------------------------------------------------------
1 | use crate::Config;
2 |
3 |
4 | /// Returns the JS code within `"#);
34 |
35 | let mut out = input[..insert_idx].to_vec();
36 | out.extend_from_slice(script_tag.as_bytes());
37 | out.extend_from_slice(&input[insert_idx..]);
38 | out
39 | }
40 |
--------------------------------------------------------------------------------
/lib/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Penguin is a dev server with features like auto-reloading, a static file
2 | //! server, and proxy-support. It is available both, as an app and as a library.
3 | //! You are currently reading the library docs. If you are interested in the CLI
4 | //! app, see [the README](https://github.com/LukasKalbertodt/penguin#readme).
5 | //!
6 | //! This library essentially allows you to configure and then start an HTTP
7 | //! server. After starting the server you get a [`Controller`] which allows you
8 | //! to send commands to active browser sessions, like reloading the page or
9 | //! showing a message.
10 | //!
11 | //!
12 | //! # Quick start
13 | //!
14 | //! This should get you started as it shows almost everything this library has
15 | //! to offer:
16 | //!
17 | //! ```no_run
18 | //! use std::{path::Path, time::Duration};
19 | //! use penguin::Server;
20 | //!
21 | //! #[tokio::main]
22 | //! async fn main() -> Result<(), Box> {
23 | //! // Configure the server.
24 | //! let (server, controller) = Server::bind(([127, 0, 0, 1], 4090).into())
25 | //! .proxy("localhost:8000".parse()?)
26 | //! .add_mount("/assets", Path::new("./frontend/build"))?
27 | //! .build()?;
28 | //!
29 | //! // In some other task, you can control the browser sessions. This dummy
30 | //! // code just waits 5 seconds and then reloads all sessions.
31 | //! tokio::spawn(async move {
32 | //! tokio::time::sleep(Duration::from_secs(5)).await;
33 | //! controller.reload();
34 | //! });
35 | //!
36 | //! server.await?;
37 | //!
38 | //! Ok(())
39 | //! }
40 | //! ```
41 | //!
42 | //! # Routing
43 | //!
44 | //! Incoming requests are routed like this (from highest to lowest priority):
45 | //!
46 | //! - Requests to the control path (`/~~penguin` by default) are internally
47 | //! handled. This is used for establishing WS connections and to receive
48 | //! commands.
49 | //! - Requests with a path matching one of the mounts is served from that
50 | //! directory.
51 | //! - The most specific mount (i.e. the one with the longest URI path) is
52 | //! used. Consider there are two mounts: `/cat` -> `./foo` and `/cat/paw`
53 | //! -> `./bar`. Then a request to `/cat/paw/info.json` is replied to with
54 | //! `./bar/info.json` while a request to `/cat/style.css` is replied to
55 | //! with `./foo/style.css`
56 | //! - If a proxy is configured, then all remaining requests are forwarded to it
57 | //! and its reply is forwarded back to the initiator of the request. Otherwise
58 | //! (no proxy configured), all remaining requests are answered with 404.
59 | //!
60 | //!
61 |
62 | #![deny(missing_debug_implementations)]
63 |
64 | use std::{fmt, future::Future, net::SocketAddr, pin::Pin, task};
65 |
66 | use tokio::sync::broadcast::{self, Sender};
67 |
68 | mod config;
69 | mod inject;
70 | mod serve;
71 | pub mod util;
72 | mod ws;
73 |
74 | #[cfg(test)]
75 | mod tests;
76 |
77 | /// Reexport of `hyper` dependency (which includes `http`).
78 | pub extern crate hyper;
79 |
80 | pub use config::{
81 | Builder, Config, ConfigError, DEFAULT_CONTROL_PATH, Mount, ProxyTarget, ProxyTargetParseError
82 | };
83 |
84 | /// Penguin server: the main type of this library.
85 | ///
86 | /// This type implements `Future`, and can thus be `await`ed. If you do not
87 | /// `await` (or otherwise poll) this, the server will not start serving.
88 | #[must_use = "futures do nothing unless you `.await` or poll them"]
89 | pub struct Server {
90 | // TODO: maybe avoid boxing this if possible?
91 | future: Pin>>>,
92 | }
93 |
94 | impl Server {
95 | /// Returns a builder to configure the server with the bind address of the
96 | /// server being set to `addr`.
97 | pub fn bind(addr: SocketAddr) -> Builder {
98 | Builder::new(addr)
99 | }
100 |
101 | /// Builds a server and a controller from a configuration. Most of the time
102 | /// you can use [`Builder::build`] instead of this method.
103 | pub fn build(config: Config) -> (Self, Controller) {
104 | let (sender, _) = broadcast::channel(ACTION_CHANNEL_SIZE);
105 | let controller = Controller(sender.clone());
106 | let future = Box::pin(serve::run(config, sender));
107 |
108 | (Self { future }, controller)
109 | }
110 | }
111 |
112 | impl Future for Server {
113 | type Output = Result<(), hyper::Error>;
114 |
115 | fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll {
116 | self.future.as_mut().poll(cx)
117 | }
118 | }
119 |
120 | impl fmt::Debug for Server {
121 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122 | f.pad("Server(_)")
123 | }
124 | }
125 |
126 | const ACTION_CHANNEL_SIZE: usize = 64;
127 |
128 | /// A handle to send commands to the server.
129 | #[derive(Debug, Clone)]
130 | pub struct Controller(Sender);
131 |
132 | impl Controller {
133 | /// Reloads all active browser sessions.
134 | pub fn reload(&self) {
135 | let _ = self.0.send(Action::Reload);
136 | }
137 |
138 | /// Shows a message as overlay in all active browser sessions. The given
139 | /// string will be copied into the `innerHTML` of a `` verbatim.
140 | ///
141 | /// This call will overwrite/hide all previous messages.
142 | pub fn show_message(&self, msg: impl Into
) {
143 | let _ = self.0.send(Action::Message(msg.into()));
144 | }
145 | }
146 |
147 | #[derive(Debug, Clone)]
148 | enum Action {
149 | Reload,
150 | Message(String),
151 | }
152 |
--------------------------------------------------------------------------------
/lib/src/serve/fs.rs:
--------------------------------------------------------------------------------
1 | use std::{io::{self, ErrorKind}, path::Path};
2 |
3 | use http_range::{HttpRange, HttpRangeParseError};
4 | use hyper::{Body, Request, Response, header, StatusCode};
5 | use tokio::{fs, io::{AsyncSeekExt, AsyncReadExt}};
6 | use tokio_util::codec::{FramedRead, BytesCodec};
7 |
8 | use crate::{inject, Config};
9 | use super::{bad_request, not_found, SERVER_HEADER};
10 |
11 |
12 | /// Checks if the request matches any `config.mounts` and returns an
13 | /// appropriate response in that case. Otherwise `Ok(None)` is returned.
14 | pub(crate) async fn try_serve(
15 | req: &Request,
16 | config: &Config,
17 | ) -> Option> {
18 | let (subpath, mount) = config.mounts.iter()
19 | .filter_map(|mount| {
20 | req.uri()
21 | .path()
22 | .strip_prefix(&mount.uri_path)
23 | .map(|subpath| {
24 | // Make sure that subpath never starts with `/`.
25 | (subpath.trim_start_matches('/').to_owned(), mount)
26 | })
27 | })
28 |
29 | // We want the "most specific" mount, so the longest URI path wins.
30 | .max_by_key(|(_, mount)| mount.uri_path.len())?;
31 |
32 | Some(serve(req, &subpath, &mount.fs_path, config).await)
33 | }
34 |
35 | async fn serve(
36 | req: &Request,
37 | subpath: &str,
38 | fs_root: &Path,
39 | config: &Config,
40 | ) -> Response {
41 | log::trace!("Serving request from file server...");
42 |
43 | let subpath = Path::new(subpath);
44 | let path = fs_root.join(subpath);
45 |
46 | // Protect against directory traversal attacks.
47 | macro_rules! canonicalize {
48 | ($path:expr) => {
49 | match fs::canonicalize($path).await {
50 | Ok(v) => v,
51 | Err(e) if e.kind() == ErrorKind::NotFound => return not_found(config),
52 | Err(e) => panic!(
53 | "unhandled error: could not canonicalize path '{}': {}",
54 | $path.display(),
55 | e,
56 | ),
57 | }
58 | };
59 | }
60 |
61 | let canonical_req = canonicalize!(&path);
62 | let canonical_root = canonicalize!(fs_root);
63 | if !canonical_req.starts_with(canonical_root) {
64 | log::warn!(
65 | "Directory traversal attack detected ({:?} {}) -> responding BAD REQUEST",
66 | req.method(),
67 | req.uri().path(),
68 | );
69 |
70 | return bad_request("Bad request: requested file outside of served directory\n");
71 | }
72 |
73 | // Dispatch depending on whether it's a file or directory.
74 | if !path.exists() {
75 | not_found(config)
76 | } else if path.is_file() {
77 | log::trace!("Serving requested file");
78 | serve_file(&path, req, config).await
79 | } else if path.join("index.html").is_file() {
80 | log::trace!("Serving 'index.html' file in requested directory");
81 | serve_file(&path.join("index.html"), req, config).await
82 | } else {
83 | log::trace!("Listing contents of directory...");
84 | serve_dir(req.uri().path(), &path, config)
85 | .await
86 | .expect("failed to read directory contents due to IO error")
87 | }
88 | }
89 |
90 | /// Lists the contents of a directory.
91 | async fn serve_dir(
92 | uri_path: &str,
93 | path: &Path,
94 | config: &Config,
95 | ) -> Result, io::Error> {
96 | const DIR_LISTING_HTML: &str = include_str!("../assets/dir-listing.html");
97 |
98 | // Collect all children of this folder.
99 | let mut folders = Vec::new();
100 | let mut files = Vec::new();
101 | let mut it = fs::read_dir(path).await?;
102 | while let Some(entry) = it.next_entry().await? {
103 | let name = entry.file_name().to_string_lossy().into_owned();
104 | if entry.file_type().await?.is_file() {
105 | files.push((name, false));
106 | } else {
107 | folders.push((name + "/", false));
108 | }
109 | }
110 |
111 | // Also collect all mounts that are mounted below this path.
112 | for sd in config.mounts.iter().filter(|sd| sd.fs_path.exists()) {
113 | if let Some(rest) = sd.uri_path.strip_prefix(uri_path) {
114 | if rest.is_empty() {
115 | continue;
116 | }
117 |
118 | let name = rest.find('/')
119 | .map(|pos| &rest[..pos])
120 | .unwrap_or(rest)
121 | .to_owned();
122 | if sd.fs_path.is_dir() {
123 | folders.push((name + "/", true));
124 | } else {
125 | files.push((name, true));
126 | }
127 | }
128 | }
129 |
130 | folders.sort();
131 | files.sort();
132 |
133 | // Build list of children.
134 | let mut entries = String::from("\n");
135 | for (name, is_mount) in folders.into_iter().chain(files) {
136 | entries.push_str(&format!(
137 | "{0}
\n",
138 | name,
139 | if is_mount { "mount" } else { "real" },
140 | ));
141 | }
142 |
143 | let html = DIR_LISTING_HTML
144 | .replace("{{ uri_path }}", uri_path)
145 | .replace("{{ entries }}", &entries)
146 | .replace("{{ control_path }}", config.control_path());
147 |
148 | Ok(
149 | Response::builder()
150 | .header("Content-Type", "text/html; charset=utf-8")
151 | .header("Server", SERVER_HEADER)
152 | .body(html.into())
153 | .expect("bug: invalid response")
154 | )
155 | }
156 |
157 | /// Serves a single file. If it's a HTML file, our JS code is injected.
158 | async fn serve_file(
159 | path: &Path,
160 | req: &Request,
161 | config: &Config,
162 | ) -> Response {
163 | // TODO: maybe we should return 403 if the file can't be read due to
164 | // permissions? Generally, the `unwrap`s in this function are... meh.
165 |
166 | let mime = mime_guess::from_path(&path).first();
167 | if mime.as_ref().map_or(false, |mime| mime.as_ref().starts_with("text/html")) {
168 | let raw = fs::read(path).await.expect("failed to read file");
169 | let html = inject::into(&raw, &config);
170 |
171 | Response::builder()
172 | .header("Content-Type", "text/html")
173 | .header("Content-Length", html.len().to_string())
174 | .header("Server", SERVER_HEADER)
175 | .body(html.into())
176 | .expect("bug: invalid response")
177 | } else {
178 | let mut file = fs::File::open(path).await.expect("failed to open file");
179 | let file_size = file.metadata().await.expect("failed to read file metadata").len();
180 |
181 | let mut response = Response::builder()
182 | .header("Server", SERVER_HEADER)
183 | .header(header::ACCEPT_RANGES, "bytes");
184 | if let Some(mime) = mime {
185 | response = response.header("Content-Type", mime.to_string());
186 | }
187 |
188 | if let Some(range_header) = req.headers().get(header::RANGE) {
189 | let range = match HttpRange::parse_bytes(range_header.as_bytes(), file_size) {
190 | Ok(ranges) if ranges.len() == 1 => ranges[0],
191 | Ok(_) => {
192 | return Response::builder()
193 | .status(StatusCode::BAD_REQUEST)
194 | .header("Server", SERVER_HEADER)
195 | .body("multiple ranges in 'Range' header not supported".into())
196 | .expect("bug: invalid response")
197 | }
198 | Err(HttpRangeParseError::InvalidRange) => todo!(),
199 | Err(HttpRangeParseError::NoOverlap) => {
200 | return Response::builder()
201 | .status(StatusCode::RANGE_NOT_SATISFIABLE)
202 | .header("Server", SERVER_HEADER)
203 | .body("".into())
204 | .expect("bug: invalid response");
205 | }
206 | };
207 |
208 | file.seek(io::SeekFrom::Start(range.start)).await.unwrap();
209 | let reader = FramedRead::new(file.take(range.length), BytesCodec::new());
210 | let body = Body::wrap_stream(reader);
211 | response
212 | .status(StatusCode::PARTIAL_CONTENT)
213 | .header(header::CONTENT_LENGTH, range.length)
214 | .header(header::CONTENT_RANGE, format!(
215 | "bytes {}-{}/{}",
216 | range.start,
217 | range.start + range.length,
218 | file_size,
219 | ))
220 | .body(body)
221 | .expect("bug: invalid response")
222 | } else {
223 | let body = Body::wrap_stream(FramedRead::new(file, BytesCodec::new()));
224 | response
225 | .header(header::CONTENT_LENGTH, file_size)
226 | .body(body)
227 | .expect("bug: invalid response")
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/lib/src/serve/mod.rs:
--------------------------------------------------------------------------------
1 | use std::{convert::Infallible, future::Future, panic::AssertUnwindSafe, sync::Arc};
2 |
3 | use futures::FutureExt;
4 | use hyper::{
5 | Body, Method, Request, Response, Server, StatusCode,
6 | http::uri::PathAndQuery,
7 | service::{make_service_fn, service_fn},
8 | };
9 | use tokio::sync::broadcast::Sender;
10 |
11 | use crate::serve::proxy::ProxyContext;
12 | use super::{Action, Config};
13 |
14 | mod fs;
15 | mod proxy;
16 |
17 |
18 | pub(crate) async fn run(config: Config, actions: Sender) -> Result<(), hyper::Error> {
19 | let addr = config.bind_addr;
20 |
21 | let ctx = Arc::new(Context {
22 | config,
23 | proxy: ProxyContext::new(),
24 | });
25 | let make_service = make_service_fn(move |_| {
26 | let ctx = Arc::clone(&ctx);
27 | let actions = actions.clone();
28 |
29 | async {
30 | Ok::<_, Infallible>(service_fn(move |req| {
31 | handle_internal_errors(
32 | handle(req, Arc::clone(&ctx), actions.clone())
33 | )
34 | }))
35 | }
36 | });
37 |
38 | log::info!("Creating hyper server");
39 | let server = Server::try_bind(&addr)?.serve(make_service);
40 |
41 | log::info!("Start listening with hyper server");
42 | server.await?;
43 |
44 | Ok(())
45 | }
46 |
47 | async fn handle_internal_errors(
48 | future: impl Future