mirror of
https://github.com/Devolutions/IronRDP.git
synced 2025-12-23 12:26:46 +00:00
Compare commits
1288 commits
ironrdp-v0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87f8d073c8 | ||
|
|
bd2aed7686 | ||
|
|
b50b648344 | ||
|
|
113284a053 | ||
|
|
d587b0c4c1 | ||
|
|
0903c9ae75 | ||
|
|
f31af061b9 | ||
|
|
7123150b63 | ||
|
|
632ad86f67 | ||
|
|
da5db5bf11 | ||
|
|
a2af587e60 | ||
|
|
924330159a | ||
|
|
cf978321d3 | ||
|
|
b303ae3a90 | ||
|
|
bca6d190a8 | ||
|
|
cca323adab | ||
|
|
742607240c | ||
|
|
866a6d8674 | ||
|
|
430f70b43f | ||
|
|
5bd319126d | ||
|
|
bfb0cae2f8 | ||
|
|
a70e01d9c5 | ||
|
|
f2326ef046 | ||
|
|
966ba8a53e | ||
|
|
79e71c4f90 | ||
|
|
9622619e8c | ||
|
|
d3e0cb17e1 | ||
|
|
2cedc05722 | ||
|
|
abc391c134 | ||
|
|
bbdecc2aa9 | ||
|
|
e87048c19b | ||
|
|
52225cca3e | ||
|
|
6214c95c6f | ||
|
|
a0a3e750c9 | ||
|
|
d24dbb1e2c | ||
|
|
a24a1fa9e8 | ||
|
|
cca53fd79b | ||
|
|
da38fa20a3 | ||
|
|
82dbb6460f | ||
|
|
af8ebdcfa2 | ||
|
|
ce298d1c19 | ||
|
|
a8289bf63f | ||
|
|
bbc38db750 | ||
|
|
49a0a9e6d2 | ||
|
|
c6b5487559 | ||
|
|
18c81ed5d8 | ||
|
|
b91a4eeb01 | ||
|
|
209108dc2c | ||
|
|
a660d7f960 | ||
|
|
3198abae2f | ||
|
|
6127e13c83 | ||
|
|
8dc41e2feb | ||
|
|
2994edf320 | ||
|
|
ac2964d406 | ||
|
|
0b39078c26 | ||
|
|
4f518f0a7b | ||
|
|
0141178ae2 | ||
|
|
c1fcf7a0c3 | ||
|
|
c417012b38 | ||
|
|
42fbd5f378 | ||
|
|
4cf86ffdba | ||
|
|
6c0014d5b3 | ||
|
|
3182a018e2 | ||
|
|
5f52a44b84 | ||
|
|
e6421b509c | ||
|
|
e5042a7d81 | ||
|
|
fe47ced857 | ||
|
|
630525deae | ||
|
|
a8b6fb1d74 | ||
|
|
898df8c0fa | ||
|
|
bd8f2743d6 | ||
|
|
8a8027481f | ||
|
|
5ddf68de79 | ||
|
|
2259bd7706 | ||
|
|
fd43129b13 | ||
|
|
e8d7570cd1 | ||
|
|
4beab02353 | ||
|
|
17833fe009 | ||
|
|
8cf9f3dda4 | ||
|
|
21fa028dff | ||
|
|
598cd3f76e | ||
|
|
50574c570f | ||
|
|
729ecf965e | ||
|
|
94ca3e25a8 | ||
|
|
cd2f25f97a | ||
|
|
6b626d4fca | ||
|
|
d31291362c | ||
|
|
7f57e12fab | ||
|
|
6de7f4bf60 | ||
|
|
23c0cc2c36 | ||
|
|
a3b2017e5f | ||
|
|
5d0c74df91 | ||
|
|
5b948e2161 | ||
|
|
303bee0456 | ||
|
|
ca11e338d7 | ||
|
|
ae99d14a69 | ||
|
|
cec3fa70fc | ||
|
|
2c7f976ecf | ||
|
|
998ef87f96 | ||
|
|
76e4ac230a | ||
|
|
a996d02a66 | ||
|
|
9e23597c50 | ||
|
|
fe31cf2c57 | ||
|
|
c8c70975dd | ||
|
|
f34a9f2500 | ||
|
|
b1f6004ab1 | ||
|
|
8fe288400d | ||
|
|
b0c145d0d9 | ||
|
|
6fddcaae38 | ||
|
|
ff2c968052 | ||
|
|
5fe6043208 | ||
|
|
dc6fd0433d | ||
|
|
514400f6fc | ||
|
|
6fab9f8228 | ||
|
|
e01f38f3a8 | ||
|
|
119c7077c9 | ||
|
|
a6e0364be7 | ||
|
|
1fdbfd29ed | ||
|
|
27f5504508 | ||
|
|
c84b46be91 | ||
|
|
35c19ce444 | ||
|
|
64535c5559 | ||
|
|
7d28ef83a6 | ||
|
|
cd184d30bd | ||
|
|
9ff3cffb59 | ||
|
|
a682d9cc48 | ||
|
|
ac291423de | ||
|
|
5fc9fefa02 | ||
|
|
85fc1c7bb3 | ||
|
|
867ed221da | ||
|
|
166b76010c | ||
|
|
4df5dd8762 | ||
|
|
5d8a487001 | ||
|
|
218fed03c7 | ||
|
|
0f9e8b1017 | ||
|
|
39566bff58 | ||
|
|
424e46d225 | ||
|
|
100765f98f | ||
|
|
32b0e40eca | ||
|
|
5d513dcf09 | ||
|
|
07b1e13d69 | ||
|
|
3ef5cf0c20 | ||
|
|
9a9ab63e1c | ||
|
|
00f2f1b067 | ||
|
|
8634ab05bc | ||
|
|
ae052ed835 | ||
|
|
f30bb99746 | ||
|
|
aafd45229b | ||
|
|
721c680f9b | ||
|
|
8c574f254e | ||
|
|
87df67fdc7 | ||
|
|
613fd51f26 | ||
|
|
d3aaa43c23 | ||
|
|
727c9b7710 | ||
|
|
fc604a567b | ||
|
|
03cac54ada | ||
|
|
de0877188c | ||
|
|
8d09e327a1 | ||
|
|
971ad922a5 | ||
|
|
12edc04c0c | ||
|
|
b4fb0aa0c7 | ||
|
|
cb99c82c7d | ||
|
|
a47a12ce94 | ||
|
|
4a40883f2b | ||
|
|
83ad04dd56 | ||
|
|
fc8f8ee279 | ||
|
|
eeac1fee1f | ||
|
|
555894ecc8 | ||
|
|
7a0fcce203 | ||
|
|
e7393f50d0 | ||
|
|
f96b1b2ce8 | ||
|
|
4fc295db4c | ||
|
|
f7cbb08e2e | ||
|
|
061ccf2f0a | ||
|
|
aa82dfb1fd | ||
|
|
5484b11fbd | ||
|
|
dc5da26fb9 | ||
|
|
c6d606e670 | ||
|
|
2307983178 | ||
|
|
cbe905ba07 | ||
|
|
0732e87517 | ||
|
|
67e0c3c401 | ||
|
|
8287cf0681 | ||
|
|
2b3f558c0a | ||
|
|
f6fb3a41b3 | ||
|
|
067d80314a | ||
|
|
a3255610d8 | ||
|
|
c710909a3c | ||
|
|
14e245d73e | ||
|
|
56f8ba4767 | ||
|
|
33530212c4 | ||
|
|
b09d46f8f2 | ||
|
|
7e23a8bb97 | ||
|
|
4dc5945019 | ||
|
|
a84a5c0571 | ||
|
|
b0e87a3776 | ||
|
|
9c99133569 | ||
|
|
cf21250dcc | ||
|
|
48e02441d2 | ||
|
|
eca256ae10 | ||
|
|
6910a3ca36 | ||
|
|
2a49588b3d | ||
|
|
4260537c90 | ||
|
|
76a2a0b47b | ||
|
|
ad64c83814 | ||
|
|
8bcf362102 | ||
|
|
f6285c5989 | ||
|
|
7c4a496ece | ||
|
|
dfb13ec499 | ||
|
|
d3d758891b | ||
|
|
51d6d1fcbe | ||
|
|
bbf7ab3394 | ||
|
|
d38c2013f0 | ||
|
|
153fe3c20d | ||
|
|
ae3066337f | ||
|
|
bfa71126bf | ||
|
|
4f9ef0c21a | ||
|
|
cd19cfc526 | ||
|
|
7829f4adb1 | ||
|
|
727c30870b | ||
|
|
4d9cf56e68 | ||
|
|
c31aa58fe6 | ||
|
|
5482365655 | ||
|
|
e5f92ae11c | ||
|
|
dfb99029a9 | ||
|
|
0f49ea4608 | ||
|
|
03f793940a | ||
|
|
112a1672d5 | ||
|
|
5c5f441bdd | ||
|
|
370a3f1104 | ||
|
|
c09f9719e0 | ||
|
|
9408789491 | ||
|
|
7f1a8be7f5 | ||
|
|
4340af89d8 | ||
|
|
cc0a17c269 | ||
|
|
4c8c5318e3 | ||
|
|
1236a9be99 | ||
|
|
bca455f158 | ||
|
|
f68cd06ac3 | ||
|
|
63a5cd6752 | ||
|
|
4a81c5d7cc | ||
|
|
9f6647c341 | ||
|
|
6024251985 | ||
|
|
5e6e4e1627 | ||
|
|
aa6777b56a | ||
|
|
5abd9ff8e0 | ||
|
|
9bc382348d | ||
|
|
e76f15e8bc | ||
|
|
87ed315bc2 | ||
|
|
3029c8f909 | ||
|
|
294dc39790 | ||
|
|
3d1762c777 | ||
|
|
783702962a | ||
|
|
0817d60910 | ||
|
|
ce7379be03 | ||
|
|
f03ee393a3 | ||
|
|
d995265724 | ||
|
|
038c85f36d | ||
|
|
08c2a9d3b7 | ||
|
|
dd787af5a0 | ||
|
|
fcb390140d | ||
|
|
e6ee67353b | ||
|
|
7b8b207e21 | ||
|
|
927eea1707 | ||
|
|
194ed07630 | ||
|
|
24e64d7589 | ||
|
|
aef4b924aa | ||
|
|
d8ab533463 | ||
|
|
fb3769c4a7 | ||
|
|
20581bb6f1 | ||
|
|
cc78b1e3dc | ||
|
|
16651e74a4 | ||
|
|
baafb20063 | ||
|
|
b5bd48c986 | ||
|
|
ff798c91a7 | ||
|
|
1ff4bfc62c | ||
|
|
8961b40012 | ||
|
|
e0eea449b8 | ||
|
|
f287e168a8 | ||
|
|
45884c5d38 | ||
|
|
712da42ded | ||
|
|
ec1832bba0 | ||
|
|
178670b4a8 | ||
|
|
806f1d7694 | ||
|
|
bdde2c76de | ||
|
|
a82e280b93 | ||
|
|
c09531ef0c | ||
|
|
ecd2450a7a | ||
|
|
fe676eeac5 | ||
|
|
cc3dbf124f | ||
|
|
5e6746c1b6 | ||
|
|
0ff1ed8de5 | ||
|
|
184cfd24ae | ||
|
|
2c7556ba1e | ||
|
|
fde18ad01a | ||
|
|
f21a6bf7d0 | ||
|
|
5ffeeea3ae | ||
|
|
135b8bc4f6 | ||
|
|
a8b9614323 | ||
|
|
4172571e8e | ||
|
|
3d7bc28b97 | ||
|
|
7bd92c0ce5 | ||
|
|
5e78f91713 | ||
|
|
f9b6992e74 | ||
|
|
b31b99eafb | ||
|
|
1df0737b0d | ||
|
|
e70e7e2c5f | ||
|
|
cd7a60ba45 | ||
|
|
45f66117ba | ||
|
|
7507a152f1 | ||
|
|
9f0edcc4c9 | ||
|
|
5dcc526f51 | ||
|
|
abcc42e01f | ||
|
|
032c38be92 | ||
|
|
817abb9805 | ||
|
|
ba488f956c | ||
|
|
aeb1193674 | ||
|
|
9d86c28865 | ||
|
|
137d91ae7a | ||
|
|
c2164716c3 | ||
|
|
a76e84d459 | ||
|
|
229070a435 | ||
|
|
1e87961d16 | ||
|
|
3c43fdda76 | ||
|
|
7f57817805 | ||
|
|
db6f4cdb7f | ||
|
|
4e581e0f47 | ||
|
|
a50cd643dc | ||
|
|
ff26400822 | ||
|
|
b957c085b3 | ||
|
|
d47c1e6415 | ||
|
|
401cedf010 | ||
|
|
9f4e6d410b | ||
|
|
f62104efe6 | ||
|
|
3159eeceec | ||
|
|
8ab98820bd | ||
|
|
1bb6895422 | ||
|
|
989a56f233 | ||
|
|
51c45df8c4 | ||
|
|
aa210204a7 | ||
|
|
248588371a | ||
|
|
b72e0857bf | ||
|
|
bceb6c1492 | ||
|
|
35da41b20d | ||
|
|
3700caba22 | ||
|
|
31ae40c206 | ||
|
|
43f05a712d | ||
|
|
97f4f25813 | ||
|
|
19d6b1ea83 | ||
|
|
780c5383e1 | ||
|
|
570cbe3c3f | ||
|
|
0705840aa5 | ||
|
|
b19008c029 | ||
|
|
b5e6f2bb4f | ||
|
|
f2c8482ba6 | ||
|
|
7cb1ac99d1 | ||
|
|
097cdb66f9 | ||
|
|
92dd927ec2 | ||
|
|
c21fa44fd6 | ||
|
|
5c890d40ad | ||
|
|
d01e667ebc | ||
|
|
47a77d2b36 | ||
|
|
3b9d558e9c | ||
|
|
6e8a8236dc | ||
|
|
5214929218 | ||
|
|
7f08a098e2 | ||
|
|
81984f9377 | ||
|
|
6b4af94071 | ||
|
|
c4595bda2f | ||
|
|
2a98f8be3e | ||
|
|
bf9adaa1a5 | ||
|
|
e76dc12485 | ||
|
|
58e1cb9034 | ||
|
|
5555c7b9dd | ||
|
|
bb41724147 | ||
|
|
d7ba22fbed | ||
|
|
5f1c44027a | ||
|
|
05c0c97262 | ||
|
|
62d809152a | ||
|
|
de86e2b14a | ||
|
|
0b5f691c1e | ||
|
|
ccf6348270 | ||
|
|
9152132776 | ||
|
|
11b92bfcbd | ||
|
|
9b2926ea12 | ||
|
|
2a5e783c43 | ||
|
|
c8597733fe | ||
|
|
cc0843838d | ||
|
|
e6d6e9d8a7 | ||
|
|
f14f3115d4 | ||
|
|
ff6c6e875b | ||
|
|
fd9a597fd0 | ||
|
|
ef33e14133 | ||
|
|
fa353765af | ||
|
|
a6c36511f6 | ||
|
|
0f9877ad39 | ||
|
|
e21c5568a4 | ||
|
|
a0fccf8d1a | ||
|
|
7c72a9f9bb | ||
|
|
98b77b5ee5 | ||
|
|
1a36fd3669 | ||
|
|
c26fab4a45 | ||
|
|
82c7c2f5b0 | ||
|
|
c4587b537c | ||
|
|
63963182b5 | ||
|
|
ab8a87d942 | ||
|
|
265b661b81 | ||
|
|
3dc8a56070 | ||
|
|
de9b9cbb6b | ||
|
|
059d775816 | ||
|
|
a16a131e43 | ||
|
|
dd221bf224 | ||
|
|
a2378efb7a | ||
|
|
8b2ba27f45 | ||
|
|
dd249909a8 | ||
|
|
25bbb2682c | ||
|
|
a0d32d7245 | ||
|
|
236a132120 | ||
|
|
96d222c442 | ||
|
|
9292988a88 | ||
|
|
9198284263 | ||
|
|
114098b673 | ||
|
|
9757167df5 | ||
|
|
cff5c1a59c | ||
|
|
c01017fd41 | ||
|
|
912c27cffe | ||
|
|
52832598ec | ||
|
|
d696c9b05c | ||
|
|
2139921c03 | ||
|
|
02c6fd5dfe | ||
|
|
97ef9f0acb | ||
|
|
bf26d6c108 | ||
|
|
686553659e | ||
|
|
a6b694b7b8 | ||
|
|
3650649914 | ||
|
|
6c1bc32a09 | ||
|
|
8236148bbd | ||
|
|
50b848529c | ||
|
|
66590487c2 | ||
|
|
a13149d5a1 | ||
|
|
054f812f2e | ||
|
|
d4ca10cdc2 | ||
|
|
0c10367ebc | ||
|
|
2d3bdffeb5 | ||
|
|
fc4951780c | ||
|
|
2f57fd2de3 | ||
|
|
d1d13c8297 | ||
|
|
4b1dbaa910 | ||
|
|
755738ff9c | ||
|
|
6e290ab366 | ||
|
|
fe0d9e9773 | ||
|
|
0ee5bfc561 | ||
|
|
20c899e464 | ||
|
|
5c077f9e47 | ||
|
|
eb499de352 | ||
|
|
511b777ef6 | ||
|
|
267ef5ba3b | ||
|
|
ea60c49b6b | ||
|
|
fc23992dea | ||
|
|
2c1bec6496 | ||
|
|
2fe519da1a | ||
|
|
49cba12c8b | ||
|
|
69eba11325 | ||
|
|
2e59014c97 | ||
|
|
bab049aa00 | ||
|
|
36da11c02e | ||
|
|
294af1cc5c | ||
|
|
807eb59b07 | ||
|
|
d26e64e4c2 | ||
|
|
7c7ca4dbd1 | ||
|
|
d9b69c68f9 | ||
|
|
9ec8f547f9 | ||
|
|
5a7561bc19 | ||
|
|
9cbac79596 | ||
|
|
1cd5c4b7f3 | ||
|
|
9470d2a7c2 | ||
|
|
2a4d357d27 | ||
|
|
d0bacffc27 | ||
|
|
4583de2c42 | ||
|
|
de67e58dd5 | ||
|
|
58afe285c2 | ||
|
|
f12e6aa827 | ||
|
|
6dfd659099 | ||
|
|
21d8ce38b1 | ||
|
|
6e567d401f | ||
|
|
ff81e7502c | ||
|
|
631964d615 | ||
|
|
58f31b88e0 | ||
|
|
5831547811 | ||
|
|
37cecc0a16 | ||
|
|
3e738a96ed | ||
|
|
5ea39d05af | ||
|
|
e8d362d5ea | ||
|
|
3c503cb2d1 | ||
|
|
d1b95676f0 | ||
|
|
7dd1787c52 | ||
|
|
a9356fc57b | ||
|
|
5381b24444 | ||
|
|
89c7549701 | ||
|
|
3a1aeedb55 | ||
|
|
a096b14488 | ||
|
|
c609fab780 | ||
|
|
4e962431d2 | ||
|
|
5338ea015c | ||
|
|
bf56a7fc80 | ||
|
|
7bf699cdcd | ||
|
|
8d15f0bca9 | ||
|
|
4ef36bf5fa | ||
|
|
f55e82f02e | ||
|
|
5e5e1aa217 | ||
|
|
876a47dd99 | ||
|
|
87014d4afb | ||
|
|
42cc02d6f6 | ||
|
|
c1a7c4de8c | ||
|
|
26a12a69c7 | ||
|
|
4a7f233725 | ||
|
|
59c2dc4675 | ||
|
|
2a2b555a11 | ||
|
|
f5dd282271 | ||
|
|
b9db9ea645 | ||
|
|
8fc30cb22e | ||
|
|
4c4d93bc6f | ||
|
|
ac24e15a3d | ||
|
|
86bc10bf95 | ||
|
|
aa8c34edf4 | ||
|
|
5e1cd31c4c | ||
|
|
bb1860d153 | ||
|
|
069e4345a1 | ||
|
|
70b83799f2 | ||
|
|
6e1b00ee3e | ||
|
|
7c268d8630 | ||
|
|
ef1be931c8 | ||
|
|
0faebe13fe | ||
|
|
f953889b72 | ||
|
|
a002057016 | ||
|
|
c5a4abd112 | ||
|
|
648f73c995 | ||
|
|
fa10362106 | ||
|
|
3c6b2ef2e2 | ||
|
|
b7164ecc68 | ||
|
|
9384723179 | ||
|
|
303315c168 | ||
|
|
649613877e | ||
|
|
b55924ee0a | ||
|
|
c04bc2d29c | ||
|
|
3d3d9f2c56 | ||
|
|
880d5012e6 | ||
|
|
ba49456236 | ||
|
|
158e78ef04 | ||
|
|
7916997b0b | ||
|
|
402ffd56c9 | ||
|
|
ab5760d47b | ||
|
|
fda9530ef6 | ||
|
|
23bc008d65 | ||
|
|
c49f190d29 | ||
|
|
1ef9dd3f37 | ||
|
|
b4c4b7ef58 | ||
|
|
f1c3f7aa60 | ||
|
|
80e8d1b257 | ||
|
|
b73a0a88c3 | ||
|
|
00d4750e4b | ||
|
|
b495704455 | ||
|
|
f6a45ca24b | ||
|
|
278a0506c2 | ||
|
|
76b0518afa | ||
|
|
7419467ad3 | ||
|
|
4154ceea05 | ||
|
|
40cd8405f2 | ||
|
|
fb8f12a62e | ||
|
|
703b245993 | ||
|
|
5357a462cc | ||
|
|
af7deae70a | ||
|
|
09ae0d043d | ||
|
|
f8c0c0ed47 | ||
|
|
e54fa5f4c8 | ||
|
|
6f779406e6 | ||
|
|
98e7dbab99 | ||
|
|
46b703e813 | ||
|
|
d8f2d10558 | ||
|
|
8843e11b55 | ||
|
|
d082d029a2 | ||
|
|
7646c3cddb | ||
|
|
676522d33f | ||
|
|
d8e21b0bd4 | ||
|
|
a6d6c2728b | ||
|
|
a73e0b7870 | ||
|
|
c73330b491 | ||
|
|
aac0740d52 | ||
|
|
36a08292d5 | ||
|
|
2a7e0d9d7e | ||
|
|
1df06f43c6 | ||
|
|
2c036c4217 | ||
|
|
ba97d5ceb5 | ||
|
|
194763fd6d | ||
|
|
e0657b07d2 | ||
|
|
757b50941c | ||
|
|
6e18a4c475 | ||
|
|
ee30a72d27 | ||
|
|
31e36a1364 | ||
|
|
b762c04274 | ||
|
|
2245440cdd | ||
|
|
cd4bf18f6d | ||
|
|
0f238097e0 | ||
|
|
c97b1f90bc | ||
|
|
7a6173ab40 | ||
|
|
64e4437491 | ||
|
|
4417e62fc6 | ||
|
|
c4bf76c33e | ||
|
|
1a878d950a | ||
|
|
cfd60a1412 | ||
|
|
8f14abdf1e | ||
|
|
cc7ab39d1f | ||
|
|
d5f069cfd1 | ||
|
|
e58aa7111d | ||
|
|
92efe2adf7 | ||
|
|
422f79eafe | ||
|
|
43157f64e9 | ||
|
|
902b5b392d | ||
|
|
9171cd7083 | ||
|
|
c53b88ebac | ||
|
|
da53d45b44 | ||
|
|
3aaef7e484 | ||
|
|
f4476ec69a | ||
|
|
867d7bfe9d | ||
|
|
ddfd0bde2a | ||
|
|
a0b82f1921 | ||
|
|
5406c286f4 | ||
|
|
20e60bc415 | ||
|
|
ea60fcacbb | ||
|
|
f2d573d987 | ||
|
|
00de2164be | ||
|
|
b6a839e248 | ||
|
|
9410f5356b | ||
|
|
4350058945 | ||
|
|
7307148cb8 | ||
|
|
05fc85bb13 | ||
|
|
db3fb61253 | ||
|
|
22779a7a21 | ||
|
|
ae78520681 | ||
|
|
69fd1750bf | ||
|
|
790b399718 | ||
|
|
da2870506f | ||
|
|
51ccf555e6 | ||
|
|
77b06d62bd | ||
|
|
adf2797ef7 | ||
|
|
934177d772 | ||
|
|
5945d24df2 | ||
|
|
27ead54bce | ||
|
|
3a805761e7 | ||
|
|
4436e64434 | ||
|
|
b1c899a3ea | ||
|
|
a8a0f469be | ||
|
|
4b12d20aa3 | ||
|
|
822197e463 | ||
|
|
53b2484efd | ||
|
|
d36391c164 | ||
|
|
8309457b7e | ||
|
|
fe293cd144 | ||
|
|
41ee9f6825 | ||
|
|
69f341b0ed | ||
|
|
abc0ee0617 | ||
|
|
2e1a9ac88e | ||
|
|
0c352c4711 | ||
|
|
cae242bbe4 | ||
|
|
9017c1068d | ||
|
|
06132cf8c2 | ||
|
|
5976d5c231 | ||
|
|
f89908c6eb | ||
|
|
43b6cdb93f | ||
|
|
34540d1fd1 | ||
|
|
783ada1666 | ||
|
|
dec0c36fe9 | ||
|
|
9d11111129 | ||
|
|
d8d2326ad3 | ||
|
|
dfad96c3bc | ||
|
|
1cd7b5e0e0 | ||
|
|
a4d1d74ed5 | ||
|
|
021d3c4fa0 | ||
|
|
8fb7300046 | ||
|
|
41d28d1e1a | ||
|
|
c45b0bd759 | ||
|
|
dfbe947e5b | ||
|
|
87375cb4d4 | ||
|
|
d5c00d0c03 | ||
|
|
e339346f5e | ||
|
|
f81b909475 | ||
|
|
f28a1d480a | ||
|
|
67dac77a58 | ||
|
|
d92fa6c66e | ||
|
|
7302d605c1 | ||
|
|
ac29c3d537 | ||
|
|
a659924fc5 | ||
|
|
9f1b04634d | ||
|
|
63743e9fcc | ||
|
|
ed8883edc4 | ||
|
|
5266147669 | ||
|
|
58755938c2 | ||
|
|
64e64d8cb4 | ||
|
|
1422467750 | ||
|
|
579823a1a9 | ||
|
|
e24fb56275 | ||
|
|
5291a9addc | ||
|
|
590221469a | ||
|
|
f64ce02cf6 | ||
|
|
0ec5be5dc4 | ||
|
|
7a1ff4a8e7 | ||
|
|
083b5044ad | ||
|
|
ca9527f8be | ||
|
|
35839459aa | ||
|
|
84a1b710d7 | ||
|
|
fd105e4b56 | ||
|
|
5c42ade597 | ||
|
|
a232b4ee0f | ||
|
|
fefcdc42fe | ||
|
|
90b3f63e29 | ||
|
|
1f067069ba | ||
|
|
c0031d34eb | ||
|
|
26421c7469 | ||
|
|
a18fb34b6d | ||
|
|
d70c9f0e98 | ||
|
|
bffe18d204 | ||
|
|
6a53a0a803 | ||
|
|
8523916acf | ||
|
|
b5a4ce19da | ||
|
|
ef2055235a | ||
|
|
9ce4986668 | ||
|
|
d8fd2ec3ae | ||
|
|
a64bd42d06 | ||
|
|
e85633233a | ||
|
|
4035543d13 | ||
|
|
5962d0d2cb | ||
|
|
649478baa4 | ||
|
|
3fe6577d15 | ||
|
|
7b5c4a73fb | ||
|
|
98ffaccae2 | ||
|
|
e68a875483 | ||
|
|
3bcf51306d | ||
|
|
ba3796f738 | ||
|
|
d46e7964bf | ||
|
|
704084952f | ||
|
|
2cef7e56fd | ||
|
|
f2ee34ffc2 | ||
|
|
a3ec56d875 | ||
|
|
c8f6fe5181 | ||
|
|
de25134007 | ||
|
|
d066f8e409 | ||
|
|
04d78b6581 | ||
|
|
8e84fe50b0 | ||
|
|
1e53669b11 | ||
|
|
a1d8bb246e | ||
|
|
e3aa8bcf0d | ||
|
|
31ed09b909 | ||
|
|
4da364367e | ||
|
|
c046e5396b | ||
|
|
282731bfe8 | ||
|
|
31feec5b79 | ||
|
|
6d24fc7183 | ||
|
|
5d0f6ce045 | ||
|
|
a2a4c89e4f | ||
|
|
2dc5742cd0 | ||
|
|
f1b8014ba6 | ||
|
|
9faf1ff08d | ||
|
|
2f955f16c3 | ||
|
|
e88c9af70d | ||
|
|
b0dd0677a4 | ||
|
|
ed963cefb6 | ||
|
|
6b3a53829d | ||
|
|
a45deebc98 | ||
|
|
825104bdf0 | ||
|
|
e12bb0fdb4 | ||
|
|
079ef0273d | ||
|
|
57cff8008e | ||
|
|
047b3094fd | ||
|
|
f7209006c5 | ||
|
|
d240081f4d | ||
|
|
2b585181ee | ||
|
|
c0ca9fda2e | ||
|
|
86db302d17 | ||
|
|
3200aa9765 | ||
|
|
bd14c6083b | ||
|
|
d9bbd4b1b3 | ||
|
|
bfa3bdac26 | ||
|
|
8c4c640905 | ||
|
|
ef2c3df761 | ||
|
|
7e11d3e198 | ||
|
|
c4193371bd | ||
|
|
e92d8c3e17 | ||
|
|
220df049b6 | ||
|
|
1ed913f891 | ||
|
|
cf4c9eae41 | ||
|
|
def44ec32c | ||
|
|
3ee44b5c9b | ||
|
|
ba38d2b7ea | ||
|
|
a3073b9a73 | ||
|
|
bce9436635 | ||
|
|
86b8e1429f | ||
|
|
5ca05f1f5f | ||
|
|
5fd5ce946e | ||
|
|
af7805a5fc | ||
|
|
1732e5e21e | ||
|
|
0d2b25a546 | ||
|
|
9e6f9310ea | ||
|
|
bc2cdc0a10 | ||
|
|
a7e163b813 | ||
|
|
39ca17b9c3 | ||
|
|
d53a5321b2 | ||
|
|
da009da927 | ||
|
|
3f36e9195e | ||
|
|
0cd612bd43 | ||
|
|
abe90e40f4 | ||
|
|
a1db20fef0 | ||
|
|
180975e416 | ||
|
|
c944674ecb | ||
|
|
60080191aa | ||
|
|
42329b326d | ||
|
|
3a52fed1fe | ||
|
|
896f4b32a1 | ||
|
|
df165129fa | ||
|
|
2ddb618e83 | ||
|
|
aecc29b780 | ||
|
|
f93c546252 | ||
|
|
3d30d6bfc3 | ||
|
|
da82c4f717 | ||
|
|
37aa6426db | ||
|
|
0f6a7068e1 | ||
|
|
a96cacdab0 | ||
|
|
f29f2c64c3 | ||
|
|
3c76c76705 | ||
|
|
203f39eb88 | ||
|
|
33e61a6c19 | ||
|
|
d0aae30a86 | ||
|
|
df329ac888 | ||
|
|
fe27f60e1c | ||
|
|
d81e144832 | ||
|
|
671b742bec | ||
|
|
869d0d31a2 | ||
|
|
14bb6d0c82 | ||
|
|
0b45bada37 | ||
|
|
7434f344f0 | ||
|
|
50a708c5a0 | ||
|
|
494506f7bb | ||
|
|
4b5e2a2392 | ||
|
|
151a7e9839 | ||
|
|
bdb421d987 | ||
|
|
294b1d7d6e | ||
|
|
65932ac23e | ||
|
|
c61b17ab70 | ||
|
|
03b0a2fde8 | ||
|
|
94fa7b7f54 | ||
|
|
9a3f51edb9 | ||
|
|
f8a982dbda | ||
|
|
119b0d33c2 | ||
|
|
7c432fc553 | ||
|
|
cf407e389b | ||
|
|
38d3c33aa9 | ||
|
|
7d57c068fc | ||
|
|
6aa513ca6d | ||
|
|
0d21e39272 | ||
|
|
d1ed78a4fa | ||
|
|
7d8a30304b | ||
|
|
d7f9cd6256 | ||
|
|
978096f8c8 | ||
|
|
f39141787a | ||
|
|
090440a45d | ||
|
|
f52c1ca5d5 | ||
|
|
db9a7cc705 | ||
|
|
e0521f3906 | ||
|
|
33c66edfc6 | ||
|
|
7519d8cc83 | ||
|
|
fded9f6a7e | ||
|
|
9aa0e15ed2 | ||
|
|
a563b15048 | ||
|
|
74b45c7144 | ||
|
|
056ec6a034 | ||
|
|
d24000a2ae | ||
|
|
49ea64d913 | ||
|
|
3725455682 | ||
|
|
e3669b8eb7 | ||
|
|
0954f42519 | ||
|
|
9801c4f560 | ||
|
|
bd8df6dd1c | ||
|
|
d47f181783 | ||
|
|
4a54d78cf6 | ||
|
|
4506e1b185 | ||
|
|
2384a70916 | ||
|
|
6588f7cdc7 | ||
|
|
9355ae5b3d | ||
|
|
b611cda9b2 | ||
|
|
53967a6bb0 | ||
|
|
06dbe8008b | ||
|
|
58a5d396a3 | ||
|
|
00ba7ad94d | ||
|
|
8776ec97c7 | ||
|
|
5742852d60 | ||
|
|
fa85584658 | ||
|
|
ad20697051 | ||
|
|
1c516e0026 | ||
|
|
e6b923fabe | ||
|
|
fadb0f14d3 | ||
|
|
084fcaf289 | ||
|
|
3e47129eef | ||
|
|
5ca81cca85 | ||
|
|
6bf638e200 | ||
|
|
950e88688c | ||
|
|
3467579ffe | ||
|
|
1a91c455a5 | ||
|
|
b0e0dd456e | ||
|
|
b6b509588c | ||
|
|
732e995509 | ||
|
|
d14f96cd61 | ||
|
|
dca40613f6 | ||
|
|
d81ca56f31 | ||
|
|
4d0246c9b1 | ||
|
|
1c220581de | ||
|
|
a513701173 | ||
|
|
74e40b88cb | ||
|
|
b87627b10e | ||
|
|
f5f5e9c087 | ||
|
|
d69ff6e6e7 | ||
|
|
4258f06292 | ||
|
|
6aa3f3e63b | ||
|
|
b554c6a6d1 | ||
|
|
ed31ead9d3 | ||
|
|
045956060e | ||
|
|
96979ef81d | ||
|
|
e6518af4fd | ||
|
|
a5747f3661 | ||
|
|
2b254c2869 | ||
|
|
716f5c5e05 | ||
|
|
078aa433c2 | ||
|
|
781cfcb18b | ||
|
|
bca685f43b | ||
|
|
25f49133d3 | ||
|
|
32910b3a11 | ||
|
|
6ad640d904 | ||
|
|
5b210f2fc7 | ||
|
|
723a91a432 | ||
|
|
6153f68b8d | ||
|
|
4feb51909a | ||
|
|
08d165d71f | ||
|
|
82d7977672 | ||
|
|
162695534f | ||
|
|
130932acee | ||
|
|
d2ddbcc4ee | ||
|
|
d2ddf50a40 | ||
|
|
ccb82f9583 | ||
|
|
c1ac51904f | ||
|
|
8fc213e699 | ||
|
|
5530550ef3 | ||
|
|
89f25b4335 | ||
|
|
c4fae49d45 | ||
|
|
2849d5830b | ||
|
|
1902b79558 | ||
|
|
ef57fba453 | ||
|
|
8c41b7649e | ||
|
|
da36bd66cc | ||
|
|
1a47255e7e | ||
|
|
179ca93dec | ||
|
|
4979ed4b6c | ||
|
|
73ea1ff05f | ||
|
|
76e3a35df1 | ||
|
|
a5aa8a2ef4 | ||
|
|
a5845f5c9f | ||
|
|
f1fab22fe0 | ||
|
|
52d1accf89 | ||
|
|
c025f494bc | ||
|
|
d6e1daccb6 | ||
|
|
e10bf4fc38 | ||
|
|
2b501496d9 | ||
|
|
c666dc5f03 | ||
|
|
c24e893a5d | ||
|
|
a9bc51a10c | ||
|
|
23777d6b8b | ||
|
|
1d0b5fa1ee | ||
|
|
d4ea558da0 | ||
|
|
dff3f307eb | ||
|
|
ab16cce949 | ||
|
|
7839acfa63 | ||
|
|
19b8c119a9 | ||
|
|
aa8f3754ad | ||
|
|
f0e00515cd | ||
|
|
a0e3cbccf6 | ||
|
|
3a412a331f | ||
|
|
7ea8d1d503 | ||
|
|
500beb21fa | ||
|
|
5a1999aae3 | ||
|
|
baf63b203b | ||
|
|
28e2029790 | ||
|
|
c2f57f5bcb | ||
|
|
cebfe8c6f3 | ||
|
|
fbe8921528 | ||
|
|
b8ff6fee25 | ||
|
|
d60782adae | ||
|
|
497f53df96 | ||
|
|
29bf964dd6 | ||
|
|
10ee653c04 | ||
|
|
fc9fef30a2 | ||
|
|
a3116686f5 | ||
|
|
291dbe6bc5 | ||
|
|
39ecfb1368 | ||
|
|
40740169e0 | ||
|
|
f834305563 | ||
|
|
5513546d89 | ||
|
|
39d5ded239 | ||
|
|
9859cb7518 | ||
|
|
8b359ff4a3 | ||
|
|
9e287b6cf7 | ||
|
|
7d648ef56e | ||
|
|
2075833ff2 | ||
|
|
a2cb31a3d7 | ||
|
|
a086e94d0b | ||
|
|
dec61fe690 | ||
|
|
47bc49b50e | ||
|
|
a29ff47f49 | ||
|
|
ce0d7c1442 | ||
|
|
ca46230453 | ||
|
|
0799bbc73c | ||
|
|
10612cda8d | ||
|
|
40e5581de3 | ||
|
|
a4fee87c49 | ||
|
|
78caad219a | ||
|
|
8b5cdc914d | ||
|
|
3c593970f2 | ||
|
|
94673ece73 | ||
|
|
b130b2e374 | ||
|
|
b70908d321 | ||
|
|
aa6f9e286e | ||
|
|
a5543e6419 | ||
|
|
1192f54bcc | ||
|
|
bf05ef8749 | ||
|
|
774a8634da | ||
|
|
c1802b625e | ||
|
|
b6407a688a | ||
|
|
622a0457bc | ||
|
|
7fa43a594e | ||
|
|
cbc7b6afdc | ||
|
|
3d98cd1d94 | ||
|
|
c670ab4331 | ||
|
|
25d88c7f81 | ||
|
|
d8c6efa3d1 | ||
|
|
6283e37937 | ||
|
|
6b660fa298 | ||
|
|
fe1567c887 | ||
|
|
86b86ba343 | ||
|
|
babbd68af5 | ||
|
|
9d33cad303 | ||
|
|
783167f23d | ||
|
|
37ac7052aa | ||
|
|
eb049f69db | ||
|
|
d0eed25157 | ||
|
|
d466861a89 | ||
|
|
aeec4c0a67 | ||
|
|
cd91756305 | ||
|
|
d51fb6dacb | ||
|
|
2938db416b | ||
|
|
ea6eea96fa | ||
|
|
74e95f692a | ||
|
|
1132126c2a | ||
|
|
55176ff99b | ||
|
|
4c38be29c7 | ||
|
|
0b408200a8 | ||
|
|
38b3b4887a | ||
|
|
e10e2f4d91 | ||
|
|
dbbd168b97 | ||
|
|
bcc4e35ecb | ||
|
|
1f401a1350 | ||
|
|
2cb0b476b5 | ||
|
|
25bf6d4ca3 | ||
|
|
20312a678e | ||
|
|
7e4c216249 | ||
|
|
c19f559e3c | ||
|
|
709f79558a | ||
|
|
687a5c3311 | ||
|
|
feee4f68d6 | ||
|
|
e7be7a54e1 | ||
|
|
f209c9a77b | ||
|
|
a4a26d315e | ||
|
|
b98443b407 | ||
|
|
56e10e1497 | ||
|
|
1655955c1f | ||
|
|
93d5ca739a | ||
|
|
4844e77b7f | ||
|
|
9aaf05ef73 | ||
|
|
5d88531259 | ||
|
|
493e8f13a4 | ||
|
|
124c3aa251 | ||
|
|
9b7bad65e5 | ||
|
|
310df4c319 | ||
|
|
c0f5fd4c50 | ||
|
|
f5a6ac7751 | ||
|
|
0125089bc6 | ||
|
|
0b20b56068 | ||
|
|
9c3ec78588 | ||
|
|
cf2287739d | ||
|
|
8857c3c25e | ||
|
|
845e394841 | ||
|
|
04924757cb | ||
|
|
60bc72f873 | ||
|
|
36a210aae9 | ||
|
|
20e6025c6c | ||
|
|
f3d17d3d6c | ||
|
|
368582ec5a | ||
|
|
fce63eaf57 | ||
|
|
c81f0f0c0d | ||
|
|
55d11a5000 | ||
|
|
a2c6a5c124 | ||
|
|
71d7c0c651 | ||
|
|
e99e6c7b55 | ||
|
|
137556013f | ||
|
|
f11131b18a | ||
|
|
95faf8bbc6 | ||
|
|
5e9990ce38 | ||
|
|
a8b9cfe99c | ||
|
|
dd7413958d | ||
|
|
ccdd3825b0 | ||
|
|
440eb655ef | ||
|
|
4516521412 | ||
|
|
c7ddcb2a2b | ||
|
|
01a10939bd | ||
|
|
f3c06b96dc | ||
|
|
710a51f24a | ||
|
|
4ad13df499 | ||
|
|
eba943d812 | ||
|
|
0bbb518c69 | ||
|
|
93bcea67ca | ||
|
|
843342b0a7 | ||
|
|
ef1c191fc8 | ||
|
|
46c8602563 | ||
|
|
39ecb06da0 | ||
|
|
2a842540b6 | ||
|
|
0953227700 | ||
|
|
db78d7e5d7 | ||
|
|
89b4f736fd | ||
|
|
33b7fe72d0 | ||
|
|
407cb24121 | ||
|
|
85b07450ae | ||
|
|
7d37967106 | ||
|
|
89e791c727 | ||
|
|
0f3849999f | ||
|
|
6d376a115f | ||
|
|
4471253e6a | ||
|
|
1f6ad789de | ||
|
|
c949f5b46c | ||
|
|
68ebc674a5 | ||
|
|
9819b9a976 | ||
|
|
631bf82fec | ||
|
|
5dd0382e43 | ||
|
|
5b36fc42b5 | ||
|
|
744445da68 | ||
|
|
a7e30ca617 | ||
|
|
137c978997 | ||
|
|
5d987265f1 | ||
|
|
e2ee180f7e | ||
|
|
1857368053 | ||
|
|
2c2979d618 | ||
|
|
236f42ec5c | ||
|
|
d9e2de84e6 | ||
|
|
053bec42ef | ||
|
|
145c905fc2 | ||
|
|
42ce3f88b4 | ||
|
|
c270b1a239 | ||
|
|
328879d5bd | ||
|
|
51016a872b | ||
|
|
d58a48f86c | ||
|
|
5908f57bcf | ||
|
|
9b5afa5382 | ||
|
|
2f78175c67 | ||
|
|
9d57b63a97 | ||
|
|
80f7b6d032 | ||
|
|
898fb8d40a | ||
|
|
e01d3263d9 | ||
|
|
7a744a217e | ||
|
|
3b1f969afa | ||
|
|
ea0e6a4fff | ||
|
|
d164087d1a | ||
|
|
98c6a1e97e | ||
|
|
aa85fe4869 | ||
|
|
28eb35f69b | ||
|
|
440f5de449 | ||
|
|
d34870902e | ||
|
|
217b763b18 | ||
|
|
19b513fee3 | ||
|
|
1dd5fab929 | ||
|
|
9ce61b4d47 | ||
|
|
bc869fc3dc | ||
|
|
5110d9bfae | ||
|
|
f991685ffb | ||
|
|
3bd8ca28ec | ||
|
|
ad9ae95480 | ||
|
|
5dcde3745c | ||
|
|
659b3425bd | ||
|
|
6fda0a0d52 | ||
|
|
91e353d32b | ||
|
|
67c6d28ffb | ||
|
|
de839b4dfb | ||
|
|
b333b022c0 | ||
|
|
2f089c9a8f | ||
|
|
0ae846b6c3 | ||
|
|
fd97d7eaf3 | ||
|
|
e19b0af527 | ||
|
|
2837d104bf | ||
|
|
261630f03c | ||
|
|
d8f3c5dfb2 | ||
|
|
816a665e93 | ||
|
|
142cdc07ae | ||
|
|
ad0112b47e | ||
|
|
7264c01712 | ||
|
|
32311d2fc9 | ||
|
|
c71e553537 | ||
|
|
2e8165d7ef | ||
|
|
ad4099ae12 | ||
|
|
8b69d46f98 | ||
|
|
8e9d8dc777 | ||
|
|
e44d5cd14e | ||
|
|
d81760753e | ||
|
|
a8b68aab8a | ||
|
|
2b97e9c0c8 | ||
|
|
04711ff0bc | ||
|
|
ca53209c84 | ||
|
|
ecfdf9dd11 | ||
|
|
23edaf8b64 | ||
|
|
a5ae271a4e | ||
|
|
6aaf18c971 | ||
|
|
f47e7fb75d | ||
|
|
67ca9a8818 | ||
|
|
d8ac8b3849 | ||
|
|
c1e26ea457 | ||
|
|
4e3a52b023 | ||
|
|
7f2b55d304 | ||
|
|
25947f21a5 | ||
|
|
72ed496113 | ||
|
|
c41bc76b8f | ||
|
|
e33e499cd0 | ||
|
|
509a81b4fe | ||
|
|
e2bc1eedc0 | ||
|
|
a5ba53328e | ||
|
|
783c3bf1af | ||
|
|
5a984856fb | ||
|
|
f9b5985dea | ||
|
|
34fe59191b | ||
|
|
5247a3a198 | ||
|
|
c070793fc4 | ||
|
|
3e584e07e1 | ||
|
|
3372d1a17f | ||
|
|
a465bf1262 | ||
|
|
ba6826c2d7 | ||
|
|
d4689087d0 | ||
|
|
7a0c7880d2 | ||
|
|
8a90aaa7fc | ||
|
|
d1d6c6b04a | ||
|
|
fb96b060b9 | ||
|
|
f4532612af | ||
|
|
750c4059df | ||
|
|
478ab33b4b | ||
|
|
4fe236f698 | ||
|
|
7da7482c98 | ||
|
|
8c2984cc4c | ||
|
|
689364d3c2 | ||
|
|
8aee045218 | ||
|
|
3401593bc7 | ||
|
|
57cd268d8d | ||
|
|
d0ca82fb73 | ||
|
|
06f35ea957 | ||
|
|
26896cd481 | ||
|
|
1a3d907ac9 | ||
|
|
48e67c8e04 | ||
|
|
75a4e33e5e | ||
|
|
7f9775480d | ||
|
|
b3864405cf | ||
|
|
62700600db | ||
|
|
4665307f4c | ||
|
|
1a3265ec0f | ||
|
|
b85838f187 | ||
|
|
e5c6bf36a6 | ||
|
|
58b5bb5297 | ||
|
|
b7773ab233 | ||
|
|
d0a9d0aa4c | ||
|
|
04e7a12f74 | ||
|
|
cd6ed50c0c | ||
|
|
9669f97c9b | ||
|
|
de2fffdc7a | ||
|
|
ff4afe998e | ||
|
|
a9b88b30c8 | ||
|
|
5e7ef6c78b | ||
|
|
a866bbdc11 | ||
|
|
c73f9b548d | ||
|
|
4aad704d30 | ||
|
|
7faedd2687 |
1188 changed files with 147291 additions and 19786 deletions
5
.cargo/config.toml
Normal file
5
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
|
||||
8
.gitattribute
Normal file
8
.gitattribute
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
*.rs text eol=lf
|
||||
*.toml text eol=lf
|
||||
*.cs text eol=lf
|
||||
*.js text eol=lf
|
||||
*.ps1 text eol=lf
|
||||
*.sln text eol=crlf
|
||||
|
||||
ffi/dotnet/Devolutions.IronRdp/Generated/** linguist-generated merge=binary
|
||||
3
.github/CODEOWNERS
vendored
Normal file
3
.github/CODEOWNERS
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# File auto-generated and managed by Devops
|
||||
/.github/ @devolutions/devops @devolutions/architecture-maintainers
|
||||
/.github/dependabot.yml @devolutions/security-managers
|
||||
28
.github/dependabot.yml
vendored
Normal file
28
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directories:
|
||||
- "/"
|
||||
- "/fuzz/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
assignees:
|
||||
- "CBenoit"
|
||||
open-pull-requests-limit: 3
|
||||
groups:
|
||||
crypto:
|
||||
patterns:
|
||||
- "md-5"
|
||||
- "md5"
|
||||
- "sha1"
|
||||
- "pkcs1"
|
||||
- "x509-cert"
|
||||
- "der"
|
||||
- "*tls*"
|
||||
- "*rand*"
|
||||
patch:
|
||||
dependency-type: "production"
|
||||
update-types:
|
||||
- "patch"
|
||||
dev:
|
||||
dependency-type: "development"
|
||||
215
.github/workflows/ci.yml
vendored
Normal file
215
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# Disable incremental compilation. CI builds are often closer to from-scratch builds, as changes
|
||||
# are typically bigger than from a local edit-compile cycle.
|
||||
# Incremental compilation also significantly increases the amount of IO and the size of ./target
|
||||
# folder, which makes caching less effective.
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
# Cache should never takes more than a few seconds to get downloaded.
|
||||
# If it does, let’s just rebuild from scratch instead of hanging "forever".
|
||||
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
|
||||
# Disabling debug info so compilation is faster and ./target folder is smaller.
|
||||
CARGO_PROFILE_DEV_DEBUG: 0
|
||||
|
||||
jobs:
|
||||
formatting:
|
||||
name: Check formatting
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo xtask check fmt -v
|
||||
|
||||
typos:
|
||||
name: Check typos
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Binary cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ./.cargo/local_root/bin
|
||||
key: ${{ runner.os }}-bin-${{ github.job }}-${{ hashFiles('xtask/src/bin_version.rs') }}
|
||||
|
||||
- name: typos (prepare)
|
||||
run: cargo xtask check install -v
|
||||
|
||||
- name: typos (check)
|
||||
run: cargo xtask check typos -v
|
||||
|
||||
checks:
|
||||
name: Checks [${{ matrix.os }}]
|
||||
needs: [formatting]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows, linux, macos]
|
||||
include:
|
||||
- os: windows
|
||||
runner: windows-latest
|
||||
- os: linux
|
||||
runner: ubuntu-latest
|
||||
- os: macos
|
||||
runner: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install devel packages
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: |
|
||||
sudo apt-get -y install libasound2-dev
|
||||
|
||||
- name: Install NASM
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
run: |
|
||||
choco install nasm
|
||||
$Env:PATH += ";$Env:ProgramFiles\NASM"
|
||||
echo "PATH=$Env:PATH" >> $Env:GITHUB_ENV
|
||||
shell: pwsh
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2.7.3
|
||||
|
||||
- name: Binary cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ./.cargo/local_root/bin
|
||||
key: ${{ runner.os }}-bin-${{ github.job }}-${{ hashFiles('xtask/src/bin_version.rs') }}
|
||||
|
||||
# Compilation is separated from execution so we know exactly the time for each step.
|
||||
|
||||
- name: Tests (compile)
|
||||
run: cargo xtask check tests --no-run -v
|
||||
|
||||
- name: Tests (run)
|
||||
run: cargo xtask check tests -v
|
||||
|
||||
- name: Lints
|
||||
run: cargo xtask check lints -v
|
||||
|
||||
- name: WASM (prepare)
|
||||
run: cargo xtask wasm install -v
|
||||
|
||||
- name: WASM (check)
|
||||
run: cargo xtask wasm check -v
|
||||
|
||||
- name: Lock files
|
||||
run: cargo xtask check locks -v
|
||||
|
||||
fuzz:
|
||||
name: Fuzzing
|
||||
needs: [formatting]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2.7.3
|
||||
with:
|
||||
workspaces: fuzz -> target
|
||||
|
||||
- name: Binary cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ./.cargo/local_root/bin
|
||||
key: ${{ runner.os }}-bin-${{ github.job }}-${{ hashFiles('xtask/src/bin_version.rs') }}
|
||||
|
||||
- name: Prepare
|
||||
run: cargo xtask fuzz install -v
|
||||
|
||||
# Simply run all fuzz targets for a few seconds, just checking there is nothing obviously wrong at a quick glance
|
||||
- name: Fuzz
|
||||
run: cargo xtask fuzz run -v
|
||||
|
||||
- name: Lock files
|
||||
run: cargo xtask check locks -v
|
||||
|
||||
web:
|
||||
name: Web Client
|
||||
needs: [formatting]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2.7.3
|
||||
|
||||
- name: Binary cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ./.cargo/local_root/bin
|
||||
key: ${{ runner.os }}-bin-${{ github.job }}-${{ hashFiles('xtask/src/bin_version.rs') }}
|
||||
|
||||
- name: Prepare
|
||||
run: cargo xtask web install -v
|
||||
|
||||
- name: Check
|
||||
run: cargo xtask web check -v
|
||||
|
||||
- name: Lock files
|
||||
run: cargo xtask check locks -v
|
||||
|
||||
ffi:
|
||||
name: FFI
|
||||
needs: [formatting]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2.7.3
|
||||
|
||||
- name: Binary cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ./.cargo/local_root/bin
|
||||
key: ${{ runner.os }}-bin-${{ github.job }}-${{ hashFiles('xtask/src/bin_version.rs') }}
|
||||
|
||||
- name: Prepare runner
|
||||
run: cargo xtask ffi install -v
|
||||
|
||||
- name: Build native library
|
||||
run: cargo xtask ffi build -v
|
||||
|
||||
- name: Generate bindings
|
||||
run: cargo xtask ffi bindings -v
|
||||
|
||||
- name: Build .NET projects
|
||||
run: cd ./ffi/dotnet && dotnet build
|
||||
|
||||
success:
|
||||
name: Success
|
||||
if: ${{ always() }}
|
||||
needs: [formatting, typos, checks, fuzz, web, ffi]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check success
|
||||
run: |
|
||||
$results = '${{ toJSON(needs.*.result) }}' | ConvertFrom-Json
|
||||
$succeeded = $($results | Where { $_ -Ne "success" }).Count -Eq 0
|
||||
exit $(if ($succeeded) { 0 } else { 1 })
|
||||
shell: pwsh
|
||||
50
.github/workflows/coverage.yml
vendored
Normal file
50
.github/workflows/coverage.yml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: Coverage Report
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Running the coverage job is only supported on the official repo itself, not on forks
|
||||
# (because $GITHUB_TOKEN only have read permissions when run on a fork)
|
||||
# We would need something like Codecov integration to handle forks properly
|
||||
# https://github.com/taiki-e/cargo-llvm-cov#continuous-integration
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2.7.3
|
||||
|
||||
- name: Prepare runner
|
||||
run: cargo xtask cov install -v
|
||||
|
||||
- name: Generate PR report
|
||||
if: ${{ github.event.number != '' }}
|
||||
run: cargo xtask cov report-gh --repo "${{ github.repository }}" --pr "${{ github.event.number }}" -v
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Configure Git Identity
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
run: |
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Update coverage data
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
run: cargo xtask cov update -v
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.DEVOLUTIONSBOT_TOKEN }}
|
||||
182
.github/workflows/fuzz.yml
vendored
Normal file
182
.github/workflows/fuzz.yml
vendored
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
name: Fuzz
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '12 3 * * 0' # At 03:12 AM UTC on Sunday.
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
|
||||
|
||||
jobs:
|
||||
corpus-download:
|
||||
name: Download corpus
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
AZURE_STORAGE_KEY: ${{ secrets.CORPUS_AZURE_STORAGE_KEY }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download fuzzing corpus
|
||||
run: cargo xtask fuzz corpus-fetch -v
|
||||
|
||||
- name: Save corpus
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
./fuzz/corpus
|
||||
./fuzz/artifacts
|
||||
key: fuzz-corpus-${{ github.run_id }}
|
||||
|
||||
fuzz:
|
||||
name: Fuzzing ${{ matrix.target }}
|
||||
needs: [corpus-download]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: [pdu_decoding, rle_decompression, bitmap_stream, cliprdr_format, channel_processing]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download corpus
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
./fuzz/corpus
|
||||
./fuzz/artifacts
|
||||
key: fuzz-corpus-${{ github.run_id }}
|
||||
|
||||
- name: Print corpus
|
||||
run: |
|
||||
tree ./fuzz/corpus
|
||||
tree ./fuzz/artifacts
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2.7.3
|
||||
with:
|
||||
workspaces: fuzz -> target
|
||||
|
||||
- name: Binary cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ./.cargo/local_root/bin
|
||||
key: ${{ runner.os }}-bin-${{ github.job }}-${{ hashFiles('xtask/src/bin_version.rs') }}
|
||||
|
||||
- name: Prepare runner
|
||||
run: cargo xtask fuzz install -v
|
||||
|
||||
- name: Fuzz
|
||||
run: cargo xtask fuzz run --duration 1000 --target ${{ matrix.target }} -v
|
||||
|
||||
- name: Minify fuzzing corpus
|
||||
if: ${{ always() && !cancelled() }}
|
||||
run: cargo xtask fuzz corpus-min --target ${{ matrix.target }} -v
|
||||
|
||||
# Use GitHub artifacts instead of cache for the updated corpus
|
||||
# because same cache can’t be used by multiple jobs at the same time.
|
||||
# Also, we can’t dynamically create a unique cache keys for all
|
||||
# the targets, because then we can’t easily retrieve this cache
|
||||
# without hardcoding a step for each one. It’s not good for maintenance.
|
||||
|
||||
- name: Prepare minified corpus upload
|
||||
# We want to upload artifacts even if fuzzing "fails" (so we can retrieve the artifact causing the crash)
|
||||
if: ${{ always() && !cancelled() }}
|
||||
run: |
|
||||
mkdir ${{ runner.temp }}/corpus/
|
||||
cp -r ./fuzz/corpus/${{ matrix.target }} ${{ runner.temp }}/corpus
|
||||
mkdir ${{ runner.temp }}/artifacts/
|
||||
cp -r ./fuzz/artifacts/${{ matrix.target }} ${{ runner.temp }}/artifacts
|
||||
|
||||
- name: Upload minified corpus
|
||||
if: ${{ always() && !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 7
|
||||
name: minified-corpus-${{ matrix.target }}
|
||||
path: |
|
||||
${{ runner.temp }}/corpus
|
||||
${{ runner.temp }}/artifacts
|
||||
|
||||
corpus-merge:
|
||||
name: Corpus merge artifacts
|
||||
if: ${{ always() && !cancelled() }}
|
||||
needs: [fuzz]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Merge Artifacts
|
||||
uses: actions/upload-artifact/merge@v4
|
||||
with:
|
||||
name: minified-corpus
|
||||
pattern: minified-corpus-*
|
||||
delete-merged: true
|
||||
|
||||
corpus-upload:
|
||||
name: Upload corpus
|
||||
if: ${{ always() && !cancelled() }}
|
||||
needs: [corpus-merge]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
AZURE_STORAGE_KEY: ${{ secrets.CORPUS_AZURE_STORAGE_KEY }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download updated corpus
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: minified-corpus
|
||||
path: ./fuzz/
|
||||
|
||||
- name: Print corpus
|
||||
run: |
|
||||
tree ./fuzz/corpus
|
||||
tree ./fuzz/artifacts
|
||||
|
||||
- name: Upload fuzzing corpus
|
||||
run: cargo xtask fuzz corpus-push -v
|
||||
|
||||
- name: Clean corpus cache
|
||||
run: |
|
||||
curl -L \
|
||||
-X DELETE \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ github.token }}"\
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/caches?key=fuzz-corpus-${{ github.run_id }}"
|
||||
|
||||
notify:
|
||||
name: Notify failure
|
||||
if: ${{ always() && contains(needs.*.result, 'failure') && github.event_name == 'schedule' }}
|
||||
needs: [fuzz]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_ARCHITECTURE }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
steps:
|
||||
- name: Send slack notification
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v1.26.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*${{ github.repository }}* :warning: \n Fuzz workflow for *${{ github.repository }}* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|found a bug>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
234
.github/workflows/npm-publish.yml
vendored
Normal file
234
.github/workflows/npm-publish.yml
vendored
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
name: Publish npm package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry-run:
|
||||
description: 'Dry run'
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
schedule:
|
||||
- cron: '48 3 * * 1' # 3:48 AM UTC every Monday
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
name: Preflight
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
dry-run: ${{ steps.get-dry-run.outputs.dry-run }}
|
||||
|
||||
steps:
|
||||
- name: Get dry run
|
||||
id: get-dry-run
|
||||
run: |
|
||||
$IsDryRun = '${{ github.event.inputs.dry-run }}' -Eq 'true' -Or '${{ github.event_name }}' -Eq 'schedule'
|
||||
|
||||
if ($IsDryRun) {
|
||||
echo "dry-run=true" >> $Env:GITHUB_OUTPUT
|
||||
} else {
|
||||
echo "dry-run=false" >> $Env:GITHUB_OUTPUT
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
build:
|
||||
name: Build package [${{matrix.library}}]
|
||||
needs: [preflight]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
library:
|
||||
- iron-remote-desktop
|
||||
- iron-remote-desktop-rdp
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup wasm-pack
|
||||
run: |
|
||||
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
Set-Location -Path "./web-client/${{matrix.library}}/"
|
||||
npm install
|
||||
shell: pwsh
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
Set-PSDebug -Trace 1
|
||||
|
||||
Set-Location -Path "./web-client/${{matrix.library}}/"
|
||||
npm run build
|
||||
Set-Location -Path ./dist
|
||||
npm pack
|
||||
shell: pwsh
|
||||
|
||||
- name: Harvest package
|
||||
run: |
|
||||
Set-PSDebug -Trace 1
|
||||
|
||||
New-Item -ItemType "directory" -Path . -Name "npm-packages"
|
||||
Get-ChildItem -Path ./web-client/ -Recurse *.tgz | ForEach { Copy-Item $_ "./npm-packages" }
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload package artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-${{matrix.library}}
|
||||
path: npm-packages/*.tgz
|
||||
|
||||
npm-merge:
|
||||
name: Merge artifacts
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Merge Artifacts
|
||||
uses: actions/upload-artifact/merge@v4
|
||||
with:
|
||||
name: npm
|
||||
pattern: npm-*
|
||||
delete-merged: true
|
||||
|
||||
publish:
|
||||
name: Publish package
|
||||
environment: publish
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
needs: [preflight, npm-merge]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download NPM packages artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: npm
|
||||
path: npm-packages
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
Set-PSDebug -Trace 1
|
||||
|
||||
$isDryRun = '${{ needs.preflight.outputs.dry-run }}' -Eq 'true'
|
||||
|
||||
$files = Get-ChildItem -Recurse npm-packages/*.tgz
|
||||
|
||||
foreach ($file in $files) {
|
||||
Write-Host "Processing $($file.Name)..."
|
||||
|
||||
$match = [regex]::Match($file.Name, '^(?<name>.+)-(?<version>\d+\.\d+\.\d+)\.tgz$')
|
||||
|
||||
if (-not $match.Success) {
|
||||
Write-Host "Unable to parse package name/version from $($file.Name), skipping."
|
||||
continue
|
||||
}
|
||||
|
||||
$pkgName = $match.Groups['name'].Value
|
||||
|
||||
# Normalize scope for npm lookups: "devolutions-foo" => "@devolutions/foo"
|
||||
if ($pkgName -like 'devolutions-*') {
|
||||
$scopedName = "@devolutions/$($pkgName.Substring(12))"
|
||||
} else {
|
||||
$scopedName = $pkgName
|
||||
}
|
||||
|
||||
$pkgVersion = $match.Groups['version'].Value
|
||||
|
||||
# Check if this version exists on npm; exit code 0 means it does.
|
||||
npm view "$scopedName@$pkgVersion" | Out-Null
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "$scopedName@$pkgVersion already exists on npm; skipping publish."
|
||||
continue
|
||||
}
|
||||
|
||||
$publishCmd = @('npm','publish',"$file",'--access=public')
|
||||
|
||||
if ($isDryRun) {
|
||||
$publishCmd += '--dry-run'
|
||||
}
|
||||
|
||||
$publishCmd = $publishCmd -Join ' '
|
||||
Invoke-Expression $publishCmd
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Create version tags
|
||||
if: ${{ needs.preflight.outputs.dry-run == 'false' }}
|
||||
run: |
|
||||
set -e
|
||||
|
||||
git fetch --tags
|
||||
|
||||
for file in npm-packages/*.tgz; do
|
||||
base=$(basename "$file" .tgz)
|
||||
|
||||
# Split base at the last hyphen to separate name and version
|
||||
pkg=${base%-*}
|
||||
# Strip the unscoped prefix introduced by `npm pack` for @devolutions/<pkg>.
|
||||
pkg=${pkg#devolutions-}
|
||||
|
||||
version=${base##*-}
|
||||
|
||||
tag="npm-${pkg}-v${version}"
|
||||
|
||||
if git rev-parse "$tag" >/dev/null 2>&1; then
|
||||
echo "Tag $tag already exists; skipping."
|
||||
continue
|
||||
fi
|
||||
|
||||
git tag "$tag" "$GITHUB_SHA"
|
||||
git push origin "$tag"
|
||||
done
|
||||
shell: bash
|
||||
env:
|
||||
GIT_AUTHOR_NAME: github-actions
|
||||
GIT_AUTHOR_EMAIL: github-actions@github.com
|
||||
GIT_COMMITTER_NAME: github-actions
|
||||
GIT_COMMITTER_EMAIL: github-actions@github.com
|
||||
|
||||
- name: Update Artifactory Cache
|
||||
if: ${{ needs.preflight.outputs.dry-run == 'false' }}
|
||||
run: |
|
||||
gh workflow run update-artifactory-cache.yml --repo Devolutions/scheduled-tasks --field package_name="iron-remote-desktop"
|
||||
gh workflow run update-artifactory-cache.yml --repo Devolutions/scheduled-tasks --field package_name="iron-remote-desktop-rdp"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.DEVOLUTIONSBOT_WRITE_TOKEN }}
|
||||
|
||||
notify:
|
||||
name: Notify failure
|
||||
if: ${{ always() && contains(needs.*.result, 'failure') && github.event_name == 'schedule' }}
|
||||
needs: [preflight, build]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_ARCHITECTURE }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
steps:
|
||||
- name: Send slack notification
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v1.26.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*${{ github.repository }}* :fire::fire::fire::fire::fire: \n The scheduled build for *${{ github.repository }}* is <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|broken>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
420
.github/workflows/nuget-publish.yml
vendored
Normal file
420
.github/workflows/nuget-publish.yml
vendored
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
name: Publish NuGet package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry-run:
|
||||
description: 'Dry run'
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
schedule:
|
||||
- cron: '21 3 * * 1' # 3:21 AM UTC every Monday
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
name: Preflight
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
dry-run: ${{ steps.get-dry-run.outputs.dry-run }}
|
||||
project-version: ${{ steps.get-version.outputs.project-version }}
|
||||
package-version: ${{ steps.get-version.outputs.package-version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout ${{ github.repository }}
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get dry run
|
||||
id: get-dry-run
|
||||
run: |
|
||||
$IsDryRun = '${{ github.event.inputs.dry-run }}' -Eq 'true' -Or '${{ github.event_name }}' -Eq 'schedule'
|
||||
|
||||
if ($IsDryRun) {
|
||||
echo "dry-run=true" >> $Env:GITHUB_OUTPUT
|
||||
} else {
|
||||
echo "dry-run=false" >> $Env:GITHUB_OUTPUT
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
- name: Get version
|
||||
id: get-version
|
||||
run: |
|
||||
$CsprojXml = [Xml] (Get-Content .\ffi\dotnet\Devolutions.IronRdp\Devolutions.IronRdp.csproj)
|
||||
$ProjectVersion = $CsprojXml.Project.PropertyGroup.Version | Select-Object -First 1
|
||||
$PackageVersion = $ProjectVersion -Replace "^(\d+)\.(\d+)\.(\d+).(\d+)$", "`$1.`$2.`$3"
|
||||
echo "project-version=$ProjectVersion" >> $Env:GITHUB_OUTPUT
|
||||
echo "package-version=$PackageVersion" >> $Env:GITHUB_OUTPUT
|
||||
shell: pwsh
|
||||
|
||||
build-native:
|
||||
name: Native build
|
||||
needs: [preflight]
|
||||
runs-on: ${{matrix.runner}}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [win, osx, linux, ios, android]
|
||||
arch: [x86, x64, arm, arm64]
|
||||
include:
|
||||
- os: win
|
||||
runner: windows-2022
|
||||
- os: osx
|
||||
runner: macos-14
|
||||
- os: linux
|
||||
runner: ubuntu-22.04
|
||||
- os: ios
|
||||
runner: macos-14
|
||||
- os: android
|
||||
runner: ubuntu-22.04
|
||||
exclude:
|
||||
- arch: arm
|
||||
os: win
|
||||
- arch: arm
|
||||
os: osx
|
||||
- arch: arm
|
||||
os: linux
|
||||
- arch: arm
|
||||
os: ios
|
||||
- arch: x86
|
||||
os: win
|
||||
- arch: x86
|
||||
os: osx
|
||||
- arch: x86
|
||||
os: linux
|
||||
- arch: x86
|
||||
os: ios
|
||||
|
||||
steps:
|
||||
- name: Checkout ${{ github.repository }}
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure Android NDK
|
||||
if: ${{ matrix.os == 'android' }}
|
||||
uses: Devolutions/actions-public/cargo-android-ndk@v1
|
||||
with:
|
||||
android_api_level: "21"
|
||||
|
||||
- name: Configure macOS deployement target
|
||||
if: ${{ matrix.os == 'osx' }}
|
||||
run: Write-Output "MACOSX_DEPLOYMENT_TARGET=10.10" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
|
||||
shell: pwsh
|
||||
|
||||
- name: Configure iOS deployement target
|
||||
if: ${{ matrix.os == 'ios' }}
|
||||
run: Write-Output "IPHONEOS_DEPLOYMENT_TARGET=12.1" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
|
||||
shell: pwsh
|
||||
|
||||
- name: Update runner
|
||||
if: ${{ matrix.os == 'linux' }}
|
||||
run: sudo apt update
|
||||
|
||||
- name: Install dependencies for rustls
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
run: |
|
||||
choco install ninja nasm
|
||||
|
||||
# We need to add the NASM binary folder to the PATH manually.
|
||||
# We don't need to do that for ninja.
|
||||
Write-Output "PATH=$Env:PATH;$Env:ProgramFiles\NASM" >> $Env:GITHUB_ENV
|
||||
|
||||
# libclang / LLVM is a requirement for AWS LC.
|
||||
# https://aws.github.io/aws-lc-rs/requirements/windows.html#libclang--llvm
|
||||
$VSINSTALLDIR = $(vswhere.exe -latest -requires Microsoft.VisualStudio.Component.VC.Llvm.Clang -property installationPath)
|
||||
Write-Output "LIBCLANG_PATH=$VSINSTALLDIR\VC\Tools\Llvm\x64\bin" >> $Env:GITHUB_ENV
|
||||
|
||||
# Install Visual Studio Developer PowerShell Module for cmdlets such as Enter-VsDevShell
|
||||
Install-Module VsDevShell -Force
|
||||
shell: pwsh
|
||||
|
||||
# No pre-generated bindings for Android and iOS.
|
||||
# https://aws.github.io/aws-lc-rs/platform_support.html#pre-generated-bindings
|
||||
- name: Install bindgen-cli for aws-lc-sys
|
||||
if: ${{ matrix.os == 'android' || matrix.os == 'ios' }}
|
||||
run: cargo install --force --locked bindgen-cli
|
||||
|
||||
# For aws-lc-sys. Error returned otherwise:
|
||||
# > Unable to generate bindings: ClangDiagnostic("/usr/include/stdint.h:26:10: fatal error: 'bits/libc-header-start.h' file not found\n")
|
||||
- name: Install gcc-multilib
|
||||
if: ${{ matrix.os == 'android' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gcc-multilib
|
||||
|
||||
- name: Setup LLVM
|
||||
if: ${{ matrix.os == 'linux' }}
|
||||
uses: Devolutions/actions-public/setup-llvm@v1
|
||||
with:
|
||||
version: "18.1.8"
|
||||
|
||||
- name: Setup CBake
|
||||
if: ${{ matrix.os == 'linux' }}
|
||||
uses: Devolutions/actions-public/setup-cbake@v1
|
||||
with:
|
||||
cargo_env_scripts: true
|
||||
|
||||
- name: Build native lib (${{matrix.os}}-${{matrix.arch}})
|
||||
run: |
|
||||
$DotNetOs = '${{matrix.os}}'
|
||||
$DotNetArch = '${{matrix.arch}}'
|
||||
$DotNetRid = '${{matrix.os}}-${{matrix.arch}}'
|
||||
$RustArch = @{'x64'='x86_64';'arm64'='aarch64';
|
||||
'x86'='i686';'arm'='armv7'}[$DotNetArch]
|
||||
$RustPlatform = @{'win'='pc-windows-msvc';
|
||||
'osx'='apple-darwin';'ios'='apple-ios';
|
||||
'linux'='unknown-linux-gnu';'android'='linux-android'}[$DotNetOs]
|
||||
$LibPrefix = @{'win'='';'osx'='lib';'ios'='lib';
|
||||
'linux'='lib';'android'='lib'}[$DotNetOs]
|
||||
$LibSuffix = @{'win'='.dll';'osx'='.dylib';'ios'='.dylib';
|
||||
'linux'='.so';'android'='.so'}[$DotNetOs]
|
||||
$RustTarget = "$RustArch-$RustPlatform"
|
||||
|
||||
if (($DotNetOs -eq 'android') -and ($DotNetArch -eq 'arm')) {
|
||||
$RustTarget = "armv7-linux-androideabi"
|
||||
}
|
||||
|
||||
rustup target add $RustTarget
|
||||
|
||||
if ($DotNetOs -eq 'win') {
|
||||
$Env:RUSTFLAGS="-C target-feature=+crt-static"
|
||||
}
|
||||
|
||||
$ProjectVersion = '${{ needs.preflight.outputs.project-version }}'
|
||||
$PackageVersion = '${{ needs.preflight.outputs.package-version }}'
|
||||
|
||||
$CargoToml = Get-Content .\ffi\Cargo.toml
|
||||
$CargoToml = $CargoToml | ForEach-Object {
|
||||
if ($_.StartsWith("version =")) { "version = `"$PackageVersion`"" } else { $_ }
|
||||
}
|
||||
Set-Content -Path .\ffi\Cargo.toml -Value $CargoToml
|
||||
|
||||
if ($DotNetOs -eq 'linux') {
|
||||
$LinuxArch = @{'x64'='amd64';'arm64'='arm64'}[$DotNetArch]
|
||||
$Env:SYSROOT_NAME = "ubuntu-20.04-$LinuxArch"
|
||||
. "$HOME/.cargo/cbake/${RustTarget}-enter.ps1"
|
||||
$Env:AWS_LC_SYS_CMAKE_BUILDER="true"
|
||||
}
|
||||
|
||||
$CargoParams = @(
|
||||
"build",
|
||||
"-p", "ffi",
|
||||
"--profile", "production-ffi",
|
||||
"--target", "$RustTarget"
|
||||
)
|
||||
|
||||
& cargo $CargoParams
|
||||
|
||||
$OutputLibraryName = "${LibPrefix}ironrdp$LibSuffix"
|
||||
$RenamedLibraryName = "${LibPrefix}DevolutionsIronRdp$LibSuffix"
|
||||
$OutputLibrary = Join-Path "target" $RustTarget 'production-ffi' $OutputLibraryName
|
||||
$OutputPath = Join-Path "dependencies" "runtimes" $DotNetRid "native"
|
||||
New-Item -ItemType Directory -Path $OutputPath | Out-Null
|
||||
Copy-Item $OutputLibrary $(Join-Path $OutputPath $RenamedLibraryName)
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload native components
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ironrdp-${{matrix.os}}-${{matrix.arch}}
|
||||
path: dependencies/runtimes/${{matrix.os}}-${{matrix.arch}}
|
||||
|
||||
build-universal:
|
||||
name: Universal build
|
||||
needs: [preflight, build-native]
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ osx, ios ]
|
||||
|
||||
steps:
|
||||
- name: Checkout ${{ github.repository }}
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup CCTools
|
||||
uses: Devolutions/actions-public/setup-cctools@v1
|
||||
|
||||
- name: Download native components
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dependencies/runtimes
|
||||
|
||||
- name: Lipo native components
|
||||
run: |
|
||||
Set-Location "dependencies/runtimes"
|
||||
# No RID for universal binaries, see: https://github.com/dotnet/runtime/issues/53156
|
||||
$OutputPath = Join-Path "${{ matrix.os }}-universal" "native"
|
||||
New-Item -ItemType Directory -Path $OutputPath | Out-Null
|
||||
$Libraries = Get-ChildItem -Recurse -Path "ironrdp-${{ matrix.os }}-*" -Filter "*.dylib" | Foreach-Object { $_.FullName } | Select -Unique
|
||||
$LipoCmd = $(@('lipo', '-create', '-output', (Join-Path -Path $OutputPath -ChildPath "libDevolutionsIronRdp.dylib")) + $Libraries) -Join ' '
|
||||
Write-Host $LipoCmd
|
||||
Invoke-Expression $LipoCmd
|
||||
shell: pwsh
|
||||
|
||||
- name: Framework
|
||||
if: ${{ matrix.os == 'ios' }}
|
||||
run: |
|
||||
$Version = '${{ needs.preflight.outputs.project-version }}'
|
||||
$ShortVersion = '${{ needs.preflight.outputs.package-version }}'
|
||||
$BundleName = "libDevolutionsIronRdp"
|
||||
$RuntimesDir = Join-Path "dependencies" "runtimes" "ios-universal" "native"
|
||||
$FrameworkDir = Join-Path "$RuntimesDir" "$BundleName.framework"
|
||||
New-Item -Path $FrameworkDir -ItemType "directory" -Force
|
||||
$FrameworkExecutable = Join-Path $FrameworkDir $BundleName
|
||||
Copy-Item -Path (Join-Path "$RuntimesDir" "$BundleName.dylib") -Destination $FrameworkExecutable -Force
|
||||
|
||||
$RPathCmd = $(@('install_name_tool', '-id', "@rpath/$BundleName.framework/$BundleName", "$FrameworkExecutable")) -Join ' '
|
||||
Write-Host $RPathCmd
|
||||
Invoke-Expression $RPathCmd
|
||||
|
||||
[xml] $InfoPlistXml = Get-Content (Join-Path "ffi" "dotnet" "Devolutions.IronRdp" "Info.plist")
|
||||
Select-Xml -xml $InfoPlistXml -XPath "/plist/dict/key[. = 'CFBundleIdentifier']/following-sibling::string[1]" |
|
||||
%{
|
||||
$_.Node.InnerXml = "com.devolutions.ironrdp"
|
||||
}
|
||||
Select-Xml -xml $InfoPlistXml -XPath "/plist/dict/key[. = 'CFBundleExecutable']/following-sibling::string[1]" |
|
||||
%{
|
||||
$_.Node.InnerXml = $BundleName
|
||||
}
|
||||
Select-Xml -xml $InfoPlistXml -XPath "/plist/dict/key[. = 'CFBundleVersion']/following-sibling::string[1]" |
|
||||
%{
|
||||
$_.Node.InnerXml = $Version
|
||||
}
|
||||
Select-Xml -xml $InfoPlistXml -XPath "/plist/dict/key[. = 'CFBundleShortVersionString']/following-sibling::string[1]" |
|
||||
%{
|
||||
$_.Node.InnerXml = $ShortVersion
|
||||
}
|
||||
|
||||
# Write the plist *without* a BOM
|
||||
$Encoding = New-Object System.Text.UTF8Encoding($false)
|
||||
$Writer = New-Object System.IO.StreamWriter((Join-Path $FrameworkDir "Info.plist"), $false, $Encoding)
|
||||
$InfoPlistXml.Save($Writer)
|
||||
$Writer.Close()
|
||||
|
||||
# .NET XML document inserts two square brackets at the end of the DOCTYPE tag
|
||||
# It's perfectly valid XML, but we're dealing with plists here and dyld will not be able to read the file
|
||||
((Get-Content -Path (Join-Path $FrameworkDir "Info.plist") -Raw) -Replace 'PropertyList-1.0.dtd"\[\]', 'PropertyList-1.0.dtd"') | Set-Content -Path (Join-Path $FrameworkDir "Info.plist")
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload native components
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ironrdp-${{ matrix.os }}-universal
|
||||
path: dependencies/runtimes/${{ matrix.os }}-universal
|
||||
|
||||
build-managed:
|
||||
name: Managed build
|
||||
needs: [build-universal]
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Check out ${{ github.repository }}
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Add msbuild to PATH
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Install ios workload
|
||||
run: dotnet workload install ios
|
||||
|
||||
- name: Prepare dependencies
|
||||
run: |
|
||||
New-Item -ItemType Directory -Path "dependencies/runtimes" | Out-Null
|
||||
shell: pwsh
|
||||
|
||||
- name: Download native components
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dependencies/runtimes
|
||||
|
||||
- name: Rename dependencies
|
||||
run: |
|
||||
Set-Location "dependencies/runtimes"
|
||||
$(Get-Item ".\ironrdp-*") | ForEach-Object { Rename-Item $_ $_.Name.Replace("ironrdp-", "") }
|
||||
Get-ChildItem * -Recurse
|
||||
shell: pwsh
|
||||
|
||||
- name: Build Devolutions.IronRdp (managed)
|
||||
run: |
|
||||
# net8.0 target packaged as Devolutions.IronRdp
|
||||
dotnet build .\ffi\dotnet\Devolutions.IronRdp\Devolutions.IronRdp.csproj -c Release
|
||||
# net9.0-ios target packaged as Devolutions.IronRdp.iOS
|
||||
dotnet build .\ffi\dotnet\Devolutions.IronRdp\Devolutions.IronRdp.csproj -c Release /p:PackageId=Devolutions.IronRdp.iOS
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload managed components
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ironrdp-nupkg
|
||||
path: ffi/dotnet/Devolutions.IronRdp/bin/Release/*.nupkg
|
||||
|
||||
publish:
|
||||
name: Publish NuGet package
|
||||
environment: nuget-publish
|
||||
if: ${{ needs.preflight.outputs.dry-run == 'false' }}
|
||||
needs: [preflight, build-managed]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Download NuGet package artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ironrdp-nupkg
|
||||
path: package
|
||||
|
||||
- name: NuGet login (OIDC)
|
||||
uses: NuGet/login@v1
|
||||
id: nuget-login
|
||||
with:
|
||||
user: ${{ secrets.NUGET_BOT_USERNAME }}
|
||||
|
||||
- name: Publish to nuget.org
|
||||
run: |
|
||||
$Files = Get-ChildItem -Recurse package/*.nupkg
|
||||
|
||||
foreach ($File in $Files) {
|
||||
$PushCmd = @(
|
||||
'dotnet',
|
||||
'nuget',
|
||||
'push',
|
||||
"$File",
|
||||
'--api-key',
|
||||
'${{ steps.nuget-login.outputs.NUGET_API_KEY }}',
|
||||
'--source',
|
||||
'https://api.nuget.org/v3/index.json',
|
||||
'--skip-duplicate'
|
||||
)
|
||||
|
||||
Write-Host "Publishing $($File.Name)..."
|
||||
$PushCmd = $PushCmd -Join ' '
|
||||
Invoke-Expression $PushCmd
|
||||
}
|
||||
shell: pwsh
|
||||
|
||||
notify:
|
||||
name: Notify failure
|
||||
if: ${{ always() && contains(needs.*.result, 'failure') && github.event_name == 'schedule' }}
|
||||
needs: [preflight, build-native, build-universal, build-managed]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_ARCHITECTURE }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
steps:
|
||||
- name: Send slack notification
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v1.26.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*${{ github.repository }}* :fire::fire::fire::fire::fire: \n The scheduled build for *${{ github.repository }}* is <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|broken>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
86
.github/workflows/release-crates.yml
vendored
Normal file
86
.github/workflows/release-crates.yml
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
name: Release crates
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
# Create a PR with the new versions and changelog, preparing the next release.
|
||||
open-pr:
|
||||
name: Open release PR
|
||||
environment: cratesio-publish
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: release-plz-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 512
|
||||
|
||||
- name: Run release-plz
|
||||
id: release-plz
|
||||
uses: Devolutions/actions-public/release-plz@v1
|
||||
with:
|
||||
command: release-pr
|
||||
git-name: Devolutions Bot
|
||||
git-email: bot@devolutions.net
|
||||
github-token: ${{ secrets.DEVOLUTIONSBOT_WRITE_TOKEN }}
|
||||
|
||||
- name: Update fuzz/Cargo.lock
|
||||
if: ${{ steps.release-plz.outputs.did-open-pr == 'true' }}
|
||||
run: |
|
||||
$prRaw = '${{ steps.release-plz.outputs.pr }}'
|
||||
Write-Host "prRaw: $prRaw"
|
||||
|
||||
$pr = $prRaw | ConvertFrom-Json
|
||||
Write-Host "pr: $pr"
|
||||
|
||||
Write-Host "Fetch branch $($pr.head_branch)"
|
||||
git fetch origin "$($pr.head_branch)"
|
||||
|
||||
Write-Host "Switch to branch $($pr.head_branch)"
|
||||
git checkout "$($pr.head_branch)"
|
||||
|
||||
Write-Host "Update ./fuzz/Cargo.lock"
|
||||
cargo update --manifest-path ./fuzz/Cargo.toml
|
||||
|
||||
Write-Host "Update last commit"
|
||||
git add ./fuzz/Cargo.lock
|
||||
git commit --amend --no-edit
|
||||
|
||||
Write-Host "Update the release pull request"
|
||||
git push --force
|
||||
shell: pwsh
|
||||
|
||||
# Release unpublished packages.
|
||||
release:
|
||||
name: Release crates
|
||||
environment: cratesio-publish
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 512
|
||||
|
||||
- name: Authenticate with crates.io
|
||||
id: auth
|
||||
uses: rust-lang/crates-io-auth-action@v1
|
||||
|
||||
- name: Run release-plz
|
||||
uses: Devolutions/actions-public/release-plz@v1
|
||||
with:
|
||||
command: release
|
||||
registry-token: ${{ steps.auth.outputs.token }}
|
||||
24
.gitignore
vendored
24
.gitignore
vendored
|
|
@ -1,2 +1,22 @@
|
|||
Cargo.lock
|
||||
target/
|
||||
# Build artifacts
|
||||
/target
|
||||
/dependencies
|
||||
# Local cargo root
|
||||
/.cargo/local_root
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# Coverage
|
||||
/docs/coverage
|
||||
|
||||
# Editor/IDE files
|
||||
*~
|
||||
/tags
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sw?
|
||||
|
|
|
|||
345
ARCHITECTURE.md
Normal file
345
ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
# Architecture
|
||||
|
||||
This document describes the high-level architecture of IronRDP.
|
||||
|
||||
> Roughly, it takes 2x more time to write a patch if you are unfamiliar with the
|
||||
> project, but it takes 10x more time to figure out where you should change the
|
||||
> code.
|
||||
|
||||
[Source](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html)
|
||||
|
||||
## Code Map
|
||||
|
||||
This section talks briefly about various important directories and data structures.
|
||||
|
||||
Note also which crates are **API Boundaries**.
|
||||
Remember, [rules at the boundary are different](https://www.tedinski.com/2018/02/06/system-boundaries.html).
|
||||
|
||||
### Core Tier
|
||||
|
||||
Set of foundational libraries for which strict quality standards must be observed.
|
||||
Note that all crates in this tier are **API Boundaries**.
|
||||
Pay attention to the "**Architecture Invariant**" sections.
|
||||
|
||||
**Architectural Invariant**: doing I/O is not allowed for these crates.
|
||||
|
||||
**Architectural Invariant**: all these crates must be fuzzed.
|
||||
|
||||
**Architectural Invariant**: must be `#[no_std]`-compatible (optionally using the `alloc` crate). Usage of the standard
|
||||
library must be opt-in through a feature flag called `std` that is enabled by default. When the `alloc` crate is optional,
|
||||
a feature flag called `alloc` must exist to enable its use.
|
||||
|
||||
**Architectural Invariant**: no platform-dependant code (`#[cfg(windows)]` and such).
|
||||
|
||||
**Architectural Invariant**: no non-essential dependency is allowed.
|
||||
|
||||
**Architectural Invariant**: no proc-macro dependency. Dependencies such as `syn` should be pushed
|
||||
as far as possible from the foundational crates so it doesn’t become too much of a compilation
|
||||
bottleneck. [Compilation time is a multiplier for everything][why-care-about-build-time].
|
||||
The paper [Developer Productivity For Humans, Part 4: Build Latency, Predictability,
|
||||
and Developer Productivity][developer-productivity] by Ciera Jaspan and Collin Green, Google
|
||||
researchers, also elaborates on why it is important to keep build times low.
|
||||
|
||||
**Architectural Invariant**: unless the performance, usability or ergonomic gain is really worth
|
||||
it, the amount of [monomorphization] incurred in downstream user code should be minimal to avoid
|
||||
binary bloating and to keep the compilation as parallel as possible. Large generic functions should
|
||||
be avoided if possible.
|
||||
|
||||
[why-care-about-build-time]: https://matklad.github.io/2021/09/04/fast-rust-builds.html#Why-Care-About-Build-Times
|
||||
[developer-productivity]: https://www.computer.org/csdl/magazine/so/2023/04/10176199/1OAJyfknInm
|
||||
[monomorphization]: https://rustc-dev-guide.rust-lang.org/backend/monomorph.html
|
||||
|
||||
#### [`crates/ironrdp`](./crates/ironrdp)
|
||||
|
||||
Meta crate re-exporting important crates.
|
||||
|
||||
**Architectural Invariant**: this crate re-exports other crates and does not provide anything else.
|
||||
|
||||
#### [`crates/ironrdp-core`](./crates/ironrdp-core)
|
||||
|
||||
Common traits and types.
|
||||
|
||||
This crate is motivated by the fact that only a few items are required to build most of the other crates such as the virtual channels.
|
||||
To move up these crates up in the compilation tree, `ironrdp-core` must remain small, with very few dependencies.
|
||||
It contains the most "low-context" building blocks.
|
||||
|
||||
Most notable traits are `Decode` and `Encode` which are used to define a common interface for PDU encoding and decoding.
|
||||
These are object-safe, and must remain so.
|
||||
|
||||
Most notable types are `ReadCursor`, `WriteCursor` and `WriteBuf` which are used pervasively for encoding and decoding in a `no-std` manner.
|
||||
|
||||
#### [`crates/ironrdp-pdu`](./crates/ironrdp-pdu)
|
||||
|
||||
PDU encoding and decoding.
|
||||
|
||||
_TODO_: clean up the dependencies
|
||||
|
||||
#### [`crates/ironrdp-graphics`](./crates/ironrdp-graphics)
|
||||
|
||||
Image processing primitives.
|
||||
|
||||
_TODO_: break down into multiple smaller crates
|
||||
|
||||
_TODO_: clean up the dependencies
|
||||
|
||||
#### [`crates/ironrdp-svc`](./crates/ironrdp-svc)
|
||||
|
||||
Traits to implement RDP static virtual channels.
|
||||
|
||||
#### [`crates/ironrdp-dvc`](./crates/ironrdp-dvc)
|
||||
|
||||
DRDYNVC static channel implementation and traits to implement dynamic virtual channels.
|
||||
|
||||
#### [`crates/ironrdp-cliprdr`](./crates/ironrdp-cliprdr)
|
||||
|
||||
CLIPRDR static channel for clipboard implemented as described in MS-RDPECLIP.
|
||||
|
||||
#### [`crates/ironrdp-rdpdr`](./crates/ironrdp-rdpdr)
|
||||
|
||||
RDPDR channel implementation.
|
||||
|
||||
#### [`crates/ironrdp-rdpsnd`](./crates/ironrdp-rdpsnd)
|
||||
|
||||
RDPSND static channel for audio output implemented as described in MS-RDPEA.
|
||||
|
||||
#### [`crates/ironrdp-connector`](./crates/ironrdp-connector)
|
||||
|
||||
State machines to drive an RDP connection sequence.
|
||||
|
||||
#### [`crates/ironrdp-session`](./crates/ironrdp-session)
|
||||
|
||||
State machines to drive an RDP session.
|
||||
|
||||
#### [`crates/ironrdp-input`](./crates/ironrdp-input)
|
||||
|
||||
Utilities to manage and build input packets.
|
||||
|
||||
#### [`crates/ironrdp-rdcleanpath`](./crates/ironrdp-rdcleanpath)
|
||||
|
||||
RDCleanPath PDU structure used by IronRDP web client and Devolutions Gateway.
|
||||
|
||||
#### [`crates/ironrdp-error`](./crates/ironrdp-error)
|
||||
|
||||
Lightweight and `no_std`-compatible generic `Error` and `Report` types.
|
||||
The `Error` type wraps a custom consumer-defined type for domain-specific details (such as `PduErrorKind`).
|
||||
|
||||
#### [`crates/ironrdp-propertyset`](./crates/ironrdp-propertyset)
|
||||
|
||||
The main type is `PropertySet`, a key-value store for configuration options.
|
||||
|
||||
#### [`crates/ironrdp-rdpfile`](./crates/ironrdp-rdpfile)
|
||||
|
||||
Loader and writer for the .RDP file format.
|
||||
|
||||
### Extra Tier
|
||||
|
||||
Higher level libraries and binaries built on top of the core tier.
|
||||
Guidelines and constraints are relaxed to some extent.
|
||||
|
||||
#### [`crates/ironrdp-blocking`](./crates/ironrdp-blocking)
|
||||
|
||||
Blocking I/O abstraction wrapping the state machines conveniently.
|
||||
|
||||
This crate is an **API Boundary**.
|
||||
|
||||
#### [`crates/ironrdp-async`](./crates/ironrdp-async)
|
||||
|
||||
Provides `Future`s wrapping the state machines conveniently.
|
||||
|
||||
This crate is an **API Boundary**.
|
||||
|
||||
#### [`crates/ironrdp-tokio`](./crates/ironrdp-tokio)
|
||||
|
||||
`Framed*` traits implementation above `tokio`’s traits.
|
||||
|
||||
This crate is an **API Boundary**.
|
||||
|
||||
#### [`crates/ironrdp-futures`](./crates/ironrdp-futures)
|
||||
|
||||
`Framed*` traits implementation above `futures`’s traits.
|
||||
|
||||
This crate is an **API Boundary**.
|
||||
|
||||
#### [`crates/ironrdp-tls`](./crates/ironrdp-tls)
|
||||
|
||||
TLS boilerplate common with most IronRDP clients.
|
||||
|
||||
NOTE: it’s not yet clear if this crate is an API Boundary or an implementation detail for the native clients.
|
||||
|
||||
#### [`crates/ironrdp-client`](./crates/ironrdp-client)
|
||||
|
||||
Portable RDP client without GPU acceleration.
|
||||
|
||||
#### [`crates/ironrdp-web`](./crates/ironrdp-web)
|
||||
|
||||
WebAssembly high-level bindings targeting web browsers.
|
||||
|
||||
This crate is an **API Boundary** (WASM module).
|
||||
|
||||
#### [`web-client/iron-remote-desktop`](./web-client/iron-remote-desktop)
|
||||
|
||||
Core frontend UI used by `iron-svelte-client` as a Web Component.
|
||||
|
||||
This crate is an **API Boundary**.
|
||||
|
||||
#### [`web-client/iron-remote-desktop-rdp`](./web-client/iron-remote-desktop-rdp)
|
||||
|
||||
Implementation of the TypeScript interfaces exposed by WebAssembly bindings from `ironrdp-web` and used by `iron-svelte-client`.
|
||||
|
||||
This crate is an **API Boundary**.
|
||||
|
||||
#### [`web-client/iron-svelte-client`](./web-client/iron-svelte-client)
|
||||
|
||||
Web-based frontend using `Svelte` and `Material` frameworks.
|
||||
|
||||
#### [`crates/ironrdp-cliprdr-native`](./crates/ironrdp-cliprdr-native)
|
||||
|
||||
Native CLIPRDR backend implementations.
|
||||
|
||||
#### [`crates/ironrdp-cfg`](./crates/ironrdp-cfg)
|
||||
|
||||
IronRDP-related utilities for ironrdp-propertyset.
|
||||
|
||||
### Internal Tier
|
||||
|
||||
Crates that are only used inside the IronRDP project, not meant to be published.
|
||||
This is mostly test case generators, fuzzing oracles, build tools, and so on.
|
||||
|
||||
**Architecture Invariant**: these crates are not, and will never be, an **API Boundary**.
|
||||
|
||||
#### [`crates/ironrdp-pdu-generators`](./crates/ironrdp-pdu-generators)
|
||||
|
||||
`proptest` generators for `ironrdp-pdu` types.
|
||||
|
||||
#### [`crates/ironrdp-session-generators`](./crates/ironrdp-session-generators)
|
||||
|
||||
`proptest` generators for `ironrdp-session` types.
|
||||
|
||||
#### [`crates/ironrdp-testsuite-core`](./crates/ironrdp-testsuite-core)
|
||||
|
||||
Contains all integration tests for code living in the core tier, in a single binary, organized in modules.
|
||||
|
||||
**Architectural Invariant**: no dependency from another tier is allowed. It must be the case that
|
||||
compiling and running the core test suite does not require building any library from the extra tier.
|
||||
This is to keep iteration time short.
|
||||
|
||||
#### [`crates/ironrdp-testsuite-extra`](./crates/ironrdp-testsuite-extra)
|
||||
|
||||
Contains all integration tests for code living in the extra tier, in a single binary, organized in modules.
|
||||
|
||||
#### [`crates/ironrdp-fuzzing`](./crates/ironrdp-fuzzing)
|
||||
|
||||
Provides test case generators and oracles for use with fuzzing.
|
||||
|
||||
#### [`fuzz`](./fuzz)
|
||||
|
||||
Fuzz targets for code in core tier.
|
||||
|
||||
#### [`xtask`](./xtask)
|
||||
|
||||
IronRDP’s free-form automation using Rust code.
|
||||
|
||||
### Community Tier
|
||||
|
||||
Crates provided and maintained by the community. Core maintainers will not invest a lot of time into
|
||||
these. One or several community maintainers are associated to each one.
|
||||
|
||||
The IronRDP team is happy to accept new crates but may not necessarily commit to keeping them
|
||||
working when changing foundational libraries. We promise to notify you if such a crate breaks, and
|
||||
will always try to fix things when it's a minor change.
|
||||
|
||||
#### [`crates/ironrdp-acceptor`](./crates/ironrdp-acceptor) (@mihneabuz)
|
||||
|
||||
State machines to drive an RDP connection acceptance sequence
|
||||
|
||||
#### [`crates/ironrdp-server`](./crates/ironrdp-server) (@mihneabuz)
|
||||
|
||||
Extendable skeleton for implementing custom RDP servers.
|
||||
|
||||
#### [`crates/ironrdp-mstsgu`](./crates/ironrdp-mstsgu) (@steffengy)
|
||||
|
||||
Terminal Services Gateway Server Protocol implementation.
|
||||
|
||||
#### [`crates/ironrdp-glutin-renderer`](./crates/ironrdp-glutin-renderer) (no maintainer)
|
||||
|
||||
`glutin` primitives for OpenGL rendering.
|
||||
|
||||
#### [`crates/ironrdp-client-glutin`](./crates/ironrdp-client-glutin) (no maintainer)
|
||||
|
||||
GPU-accelerated RDP client using glutin.
|
||||
|
||||
#### [`crates/ironrdp-replay-client`](./crates/ironrdp-replay-client) (no maintainer)
|
||||
|
||||
Utility tool to replay RDP graphics pipeline for debugging purposes.
|
||||
|
||||
## Cross-Cutting Concerns
|
||||
|
||||
This section talks about the things which are everywhere and nowhere in particular.
|
||||
|
||||
### General
|
||||
|
||||
- Dependency injection when runtime information is necessary in core tier crates (no system call such as `gethostname`)
|
||||
- Keep non-portable code out of core tier crates
|
||||
- Make crate `no_std`-compatible wherever possible
|
||||
- Facilitate fuzzing
|
||||
- In libraries, provide concrete error types either hand-crafted or using `thiserror` crate
|
||||
- In binaries, use the convenient catch-all error type `anyhow::Error`
|
||||
- Free-form automation a-la `make` following [`cargo xtask`](https://github.com/matklad/cargo-xtask) specification
|
||||
|
||||
### Avoid I/O wherever possible
|
||||
|
||||
**Architecture Invariant**: core tier crates must never interact with the outside world. Only extra tier crates
|
||||
such as `ironrdp-client`, `ironrdp-web` or `ironrdp-async` are allowed to do I/O.
|
||||
|
||||
### Continuous integration
|
||||
|
||||
We use GitHub action and our workflows simply run `cargo xtask`.
|
||||
The expectation is that, if `cargo xtask ci` passes locally, the CI will be green as well.
|
||||
|
||||
**Architecture Invariant**: `cargo xtask ci` and CI workflow must be logically equivalents. It must
|
||||
be the case that a successful `cargo xtask ci` run implies a successful CI workflow run and vice versa.
|
||||
|
||||
### Testing
|
||||
|
||||
#### Test at the boundaries (test features, not code)
|
||||
|
||||
We should focus on testing the public API of libraries (keyword: **API boundary**).
|
||||
That’s why most (if not all) tests should go into the `ironrdp-testsuite-core` and `ironrdp-testsuite-extra` crates.
|
||||
|
||||
#### Do not depend on external resources
|
||||
|
||||
**Architecture Invariant**: tests do not depend on any kind of external resources, they are perfectly reproducible.
|
||||
|
||||
#### Fuzzing
|
||||
|
||||
See [`fuzz/README.md`](./fuzz/README.md).
|
||||
|
||||
#### Readability
|
||||
|
||||
Do not include huge binary chunks directly in source files (`*.rs`). Place these in separate files (`*.bin`, `*.bmp`)
|
||||
and include them using macros such as `include_bytes!` or `include_str!`.
|
||||
|
||||
#### Use `expect-test` for snapshot testing
|
||||
|
||||
When comparing structured data (e.g.: error results, decoded PDUs), use `expect-test`. It is both easy to create
|
||||
and maintain such tests. When something affecting the representation is changed, simply run the test again with
|
||||
`UPDATE_EXPECT=1` env variable to magically update the code.
|
||||
|
||||
See:
|
||||
|
||||
- <https://matklad.github.io/2021/05/31/how-to-test.html#Expect-Tests>
|
||||
- <https://docs.rs/expect-test/latest/expect_test/>
|
||||
|
||||
TODO: take further inspiration from rust-analyzer
|
||||
|
||||
- https://github.com/rust-lang/rust-analyzer/blob/d7c99931d05e3723d878bea5dc26766791fa4e69/docs/dev/architecture.md#testing
|
||||
- https://matklad.github.io/2021/05/31/how-to-test.html
|
||||
|
||||
#### Use `rstest` for fixture-based testing
|
||||
|
||||
When a test can be generalized for multiple inputs, use [`rstest`](https://github.com/la10736/rstest) to avoid code duplication.
|
||||
|
||||
#### Use `proptest` for property testing
|
||||
|
||||
It allows to test that certain properties of your code hold for arbitrary inputs, and if a failure
|
||||
is found, automatically finds the minimal test case to reproduce the problem.
|
||||
7207
Cargo.lock
generated
Normal file
7207
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
211
Cargo.toml
211
Cargo.toml
|
|
@ -1,6 +1,213 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"ironrdp",
|
||||
"ironrdp_client"
|
||||
"crates/*",
|
||||
"benches",
|
||||
"xtask",
|
||||
"ffi",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
# FIXME: fix compilation
|
||||
exclude = [
|
||||
"crates/ironrdp-client-glutin",
|
||||
"crates/ironrdp-glutin-renderer",
|
||||
"crates/ironrdp-replay-client",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
homepage = "https://github.com/Devolutions/IronRDP"
|
||||
repository = "https://github.com/Devolutions/IronRDP"
|
||||
authors = ["Devolutions Inc. <infos@devolutions.net>", "Teleport <goteleport.com>"]
|
||||
keywords = ["rdp", "remote-desktop", "network", "client", "protocol"]
|
||||
categories = ["network-programming"]
|
||||
|
||||
[workspace.dependencies]
|
||||
# Note that for better cross-tooling interactions, do not use workspace
|
||||
# dependencies for anything that is not "workspace internal" (e.g.: mostly
|
||||
# dev-dependencies). E.g.: release-plz can’t detect that a dependency has been
|
||||
# updated in a way warranting a version bump in the dependant if no commit is
|
||||
# touching a file associated to the crate. It is technically okay to use that
|
||||
# for "private" (i.e.: not used in the public API) dependencies too, but we
|
||||
# still want to make follow-up releases to stay up to date with the community,
|
||||
# even for private dependencies.
|
||||
expect-test = "1"
|
||||
proptest = "1.4"
|
||||
rstest = "0.26"
|
||||
|
||||
# Note: we are trying to move away from using these crates.
|
||||
# They are being kept around for now for legacy compatibility,
|
||||
# but new usage should be avoided.
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
# == Safer unsafe == #
|
||||
unsafe_op_in_unsafe_fn = "warn"
|
||||
invalid_reference_casting = "warn"
|
||||
unused_unsafe = "warn"
|
||||
missing_unsafe_on_extern = "warn"
|
||||
unsafe_attr_outside_unsafe = "warn"
|
||||
|
||||
# == Correctness == #
|
||||
ambiguous_negative_literals = "warn"
|
||||
keyword_idents_2024 = "warn" # FIXME: remove when switched to 2024 edition
|
||||
|
||||
# == Style, readability == #
|
||||
elided_lifetimes_in_paths = "warn" # https://quinedot.github.io/rust-learning/dont-hide.html
|
||||
absolute_paths_not_starting_with_crate = "warn"
|
||||
single_use_lifetimes = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
unused_qualifications = "warn"
|
||||
keyword_idents = "warn"
|
||||
noop_method_call = "warn"
|
||||
macro_use_extern_crate = "warn"
|
||||
redundant_imports = "warn"
|
||||
redundant_lifetimes = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
# missing_docs = "warn" # TODO: NOTE(@CBenoit): we probably want to ensure this in core tier crates only
|
||||
|
||||
# == Compile-time / optimization == #
|
||||
unused_crate_dependencies = "warn"
|
||||
unused_macro_rules = "warn"
|
||||
|
||||
# == Extra-pedantic rustc == #
|
||||
unit_bindings = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
|
||||
# == Safer unsafe == #
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
unnecessary_safety_comment = "warn"
|
||||
multiple_unsafe_ops_per_block = "warn"
|
||||
missing_safety_doc = "warn"
|
||||
transmute_ptr_to_ptr = "warn"
|
||||
as_ptr_cast_mut = "warn"
|
||||
as_pointer_underscore = "warn"
|
||||
cast_ptr_alignment = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
ptr_cast_constness = "warn"
|
||||
|
||||
# == Correctness == #
|
||||
as_conversions = "warn"
|
||||
cast_lossless = "warn"
|
||||
cast_possible_truncation = "warn"
|
||||
cast_possible_wrap = "warn"
|
||||
cast_sign_loss = "warn"
|
||||
filetype_is_file = "warn"
|
||||
float_cmp = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
float_cmp_const = "warn"
|
||||
as_underscore = "warn"
|
||||
unwrap_used = "warn"
|
||||
large_stack_frames = "warn"
|
||||
mem_forget = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
non_ascii_literal = "warn"
|
||||
panic = "warn"
|
||||
precedence_bits = "warn"
|
||||
rc_mutex = "warn"
|
||||
same_name_method = "warn"
|
||||
string_slice = "warn"
|
||||
suspicious_xor_used_as_pow = "warn"
|
||||
unused_result_ok = "warn"
|
||||
missing_panics_doc = "warn"
|
||||
|
||||
# == Style, readability == #
|
||||
semicolon_outside_block = "warn" # With semicolon-outside-block-ignore-multiline = true
|
||||
clone_on_ref_ptr = "warn"
|
||||
cloned_instead_of_copied = "warn"
|
||||
pub_without_shorthand = "warn"
|
||||
infinite_loop = "warn"
|
||||
empty_enum_variants_with_brackets = "warn"
|
||||
deref_by_slicing = "warn"
|
||||
multiple_inherent_impl = "warn"
|
||||
map_with_unused_argument_over_ranges = "warn"
|
||||
partial_pub_fields = "warn"
|
||||
trait_duplication_in_bounds = "warn"
|
||||
type_repetition_in_bounds = "warn"
|
||||
checked_conversions = "warn"
|
||||
get_unwrap = "warn"
|
||||
similar_names = "warn" # Reduce risk of confusing similar names together, and protects against typos when variable shadowing was intended.
|
||||
str_to_string = "warn"
|
||||
string_to_string = "warn"
|
||||
std_instead_of_core = "warn"
|
||||
separated_literal_suffix = "warn"
|
||||
unused_self = "warn"
|
||||
useless_let_if_seq = "warn"
|
||||
string_add = "warn"
|
||||
range_plus_one = "warn"
|
||||
self_named_module_files = "warn"
|
||||
# TODO: partial_pub_fields = "warn" (should we enable only in pdu crates?)
|
||||
redundant_type_annotations = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
try_err = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
|
||||
# == Compile-time / optimization == #
|
||||
doc_include_without_cfg = "warn"
|
||||
inline_always = "warn"
|
||||
large_include_file = "warn"
|
||||
or_fun_call = "warn"
|
||||
rc_buffer = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
unnecessary_box_returns = "warn"
|
||||
large_futures = "warn"
|
||||
|
||||
# == Extra-pedantic clippy == #
|
||||
allow_attributes = "warn"
|
||||
cfg_not_test = "warn"
|
||||
disallowed_script_idents = "warn"
|
||||
non_zero_suggestions = "warn"
|
||||
renamed_function_params = "warn"
|
||||
unused_trait_names = "warn"
|
||||
collection_is_never_read = "warn"
|
||||
copy_iterator = "warn"
|
||||
expl_impl_clone_on_copy = "warn"
|
||||
implicit_clone = "warn"
|
||||
large_types_passed_by_value = "warn"
|
||||
redundant_clone = "warn"
|
||||
alloc_instead_of_core = "warn"
|
||||
empty_drop = "warn"
|
||||
return_self_not_must_use = "warn"
|
||||
wildcard_dependencies = "warn"
|
||||
wildcard_imports = "warn"
|
||||
|
||||
# == Let’s not merge unintended eprint!/print! statements in libraries == #
|
||||
print_stderr = "warn"
|
||||
print_stdout = "warn"
|
||||
dbg_macro = "warn"
|
||||
todo = "warn"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[profile.production]
|
||||
inherits = "release"
|
||||
lto = true
|
||||
|
||||
[profile.production-ffi]
|
||||
inherits = "release"
|
||||
strip = "symbols"
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[profile.production-wasm]
|
||||
inherits = "release"
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
|
||||
[profile.test.package.proptest]
|
||||
opt-level = 3
|
||||
|
||||
[profile.test.package.rand_chacha]
|
||||
opt-level = 3
|
||||
|
||||
[patch.crates-io]
|
||||
# FIXME: We need to catch up with Diplomat upstream again, but this is a significant amount of work.
|
||||
# In the meantime, we use this forked version which fixes an undefined behavior in the code expanded by the bridge macro.
|
||||
diplomat = { git = "https://github.com/CBenoit/diplomat", rev = "6dc806e80162b6b39509a04a2835744236cd2396" }
|
||||
|
|
|
|||
72
README.md
72
README.md
|
|
@ -1,4 +1,74 @@
|
|||
# IronRDP
|
||||
|
||||
A Rust implementation of the Microsoft Remote Desktop Protocol, with a focus on security.
|
||||
[](https://docs.rs/ironrdp/) [](https://crates.io/crates/ironrdp)
|
||||
|
||||
A collection of Rust crates providing an implementation of the Microsoft Remote Desktop Protocol, with a focus on security.
|
||||
|
||||
## Demonstration
|
||||
|
||||
<https://user-images.githubusercontent.com/3809077/202049929-76f42471-aeb0-41da-9118-0dc6ea491bd2.mp4>
|
||||
|
||||
## Video Codec Support
|
||||
|
||||
Supported codecs:
|
||||
|
||||
- Uncompressed raw bitmap
|
||||
- Interleaved Run-Length Encoding (RLE) Bitmap Codec
|
||||
- RDP 6.0 Bitmap Compression
|
||||
- Microsoft RemoteFX (RFX)
|
||||
|
||||
## Examples
|
||||
|
||||
### [`ironrdp-client`](https://github.com/Devolutions/IronRDP/tree/master/crates/ironrdp-client)
|
||||
|
||||
A full-fledged RDP client based on IronRDP crates suite, and implemented using non-blocking, asynchronous I/O.
|
||||
|
||||
```shell
|
||||
cargo run --bin ironrdp-client -- <HOSTNAME> --username <USERNAME> --password <PASSWORD>
|
||||
```
|
||||
|
||||
### [`screenshot`](https://github.com/Devolutions/IronRDP/blob/master/crates/ironrdp/examples/screenshot.rs)
|
||||
|
||||
Example of utilizing IronRDP in a blocking, synchronous fashion.
|
||||
|
||||
This example showcases the use of IronRDP in a blocking manner. It
|
||||
demonstrates how to create a basic RDP client with just a few hundred lines
|
||||
of code by leveraging the IronRDP crates suite.
|
||||
|
||||
In this basic client implementation, the client establishes a connection
|
||||
with the destination server, decodes incoming graphics updates, and saves the
|
||||
resulting output as a BMP image file on the disk.
|
||||
|
||||
```shell
|
||||
cargo run --example=screenshot -- --host <HOSTNAME> --username <USERNAME> --password <PASSWORD> --output out.bmp
|
||||
```
|
||||
|
||||
### How to enable RemoteFX on server
|
||||
|
||||
Run the following PowerShell commands, and reboot.
|
||||
|
||||
```pwsh
|
||||
Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows NT\Terminal Services' -Name 'ColorDepth' -Type DWORD -Value 5
|
||||
Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows NT\Terminal Services' -Name 'fEnableVirtualizedGraphics' -Type DWORD -Value 1
|
||||
```
|
||||
|
||||
Alternatively, you may change a few group policies using `gpedit.msc`:
|
||||
|
||||
1. Run `gpedit.msc`.
|
||||
|
||||
2. Enable `Computer Configuration/Administrative Templates/Windows Components/Remote Desktop Services/Remote Desktop Session Host/Remote Session Environment/RemoteFX for Windows Server 2008 R2/Configure RemoteFX`
|
||||
|
||||
3. Enable `Computer Configuration/Administrative Templates/Windows Components/Remote Desktop Services/Remote Desktop Session Host/Remote Session Environment/Enable RemoteFX encoding for RemoteFX clients designed for Windows Server 2008 R2 SP1`
|
||||
|
||||
4. Enable `Computer Configuration/Administrative Templates/Windows Components/Remote Desktop Services/Remote Desktop Session Host/Remote Session Environment/Limit maximum color depth`
|
||||
|
||||
5. Reboot.
|
||||
|
||||
## Architecture
|
||||
|
||||
See the [ARCHITECTURE.md](https://github.com/Devolutions/IronRDP/blob/master/ARCHITECTURE.md) document.
|
||||
|
||||
## Getting help
|
||||
|
||||
- Report bugs in the [issue tracker](https://github.com/Devolutions/IronRDP/issues)
|
||||
- Discuss the project on the [matrix room](https://matrix.to/#/#IronRDP:matrix.org)
|
||||
|
|
|
|||
623
STYLE.md
Normal file
623
STYLE.md
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
Our approach to "clean code" is two-fold:
|
||||
- we avoid blocking PRs on style changes, but
|
||||
- at the same time, the codebase is constantly refactored.
|
||||
|
||||
It is explicitly OK for a reviewer to flag only some nits in the PR, and then send a follow-up cleanup PR for things which are easier to explain by example, cc'ing the original author.
|
||||
Sending small cleanup PRs (like renaming a single local variable) is encouraged.
|
||||
These PRs are easy to merge and very welcomed.
|
||||
|
||||
When reviewing pull requests prefer extending this document to leaving non-reusable comments on the pull request itself.
|
||||
|
||||
# Style
|
||||
|
||||
## Formatting for sizes / lengths (e.g.: in `Encode::size()` and `FIXED_PART_SIZE` definitions)
|
||||
|
||||
Use an inline comment for each field of the structure.
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
const FIXED_PART_SIZE: usize = 1 /* Version */ + 1 /* Endianness */ + 2 /* CommonHeaderLength */ + 4 /* Filler */;
|
||||
|
||||
// GOOD
|
||||
const FIXED_PART_SIZE: usize = 1 // Version
|
||||
+ 1 // Endianness
|
||||
+ 2 // CommonHeaderLength
|
||||
+ 4; // Filler
|
||||
|
||||
// GOOD
|
||||
fn size(&self) -> usize {
|
||||
4 // ReturnCode
|
||||
+ 4 // cBytes
|
||||
+ self.reader_names.size() // mszReaderNames
|
||||
+ 4 // dwState
|
||||
+ 4 // dwProtocol
|
||||
+ self.atr.len() // pbAtr
|
||||
+ 4 // cbAtrLen
|
||||
}
|
||||
|
||||
// BAD
|
||||
const FIXED_PART_SIZE: usize = 1 + 1 + 2 + 4;
|
||||
|
||||
// BAD
|
||||
const FIXED_PART_SIZE: usize = size_of::<u8>() + size_of::<u8>() + size_of::<u16>() + size_of::<u32>();
|
||||
|
||||
// BAD
|
||||
fn size(&self) -> usize {
|
||||
size_of::<u32>() * 5 + self.reader_names.size() + self.atr.len()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**Rationale**: boring and readable, having a comment with the name of the field is useful when following along the documentation.
|
||||
Here is an excerpt illustrating this:
|
||||
|
||||

|
||||
|
||||
`size_of::<u8>()` by itself is not really more useful than writing `1` directly.
|
||||
The size of `u8` is not going to change, and it’s not hard to predict.
|
||||
The struct also does not necessarily directly hold a `u8` as-is, and it may be hard to correlate a wrapper type with the corresponding `size_of::<u8>()`.
|
||||
The memory representation of the wrapper type may differ from its network representation, so it’s not possible to always replace with `size_of::<Wrapper>()` instead.
|
||||
|
||||
## Error handling
|
||||
|
||||
### Return type
|
||||
|
||||
Use `crate_name::Result` (e.g.: `anyhow::Result`) rather than just `Result`.
|
||||
|
||||
**Rationale:** makes it immediately clear what result that is.
|
||||
|
||||
Exception: it’s not necessary when the type alias is clear enough (e.g.: `ConnectionResult`).
|
||||
|
||||
### Formatting of error messages
|
||||
|
||||
A single sentence which:
|
||||
- is short and concise,
|
||||
- does not start by a capital letter, and
|
||||
- does not contain trailing punctuation.
|
||||
|
||||
This is the convention adopted by the Rust project:
|
||||
- [Rust API Guidelines][api-guidelines-errors]
|
||||
- [std::error::Error][std-error-trait]
|
||||
|
||||
Also, use proper abbreviation casing, e.g., IPv4 and IPv6 (not ipv4/ipv6).
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
"invalid X.509 certificate"
|
||||
|
||||
// BAD
|
||||
"Invalid X.509 certificate."
|
||||
```
|
||||
|
||||
**Rationale**: it’s easier to compose with other error messages.
|
||||
|
||||
To illustrate with terminal error reports:
|
||||
```
|
||||
// GOOD
|
||||
Error: invalid server license, caused by invalid X.509 certificate, caused by unexpected ASN.1 DER tag: expected SEQUENCE, got CONTEXT-SPECIFIC [19] (primitive)
|
||||
|
||||
// BAD
|
||||
Error: Invalid server license., Caused by Invalid X.509 certificate., Caused by Unexpected ASN.1 DER tag: expected SEQUENCE, got CONTEXT-SPECIFIC [19] (primitive)
|
||||
```
|
||||
|
||||
The error reporter (e.g.: `ironrdp_error::ErrorReport`) is responsible for adding the punctuation and/or capitalizing the text down the line.
|
||||
|
||||
[api-guidelines-errors]: https://rust-lang.github.io/api-guidelines/interoperability.html#error-types-are-meaningful-and-well-behaved-c-good-err
|
||||
[std-error-trait]: https://doc.rust-lang.org/stable/std/error/trait.Error.html
|
||||
|
||||
## Logging
|
||||
|
||||
If any, the human-readable message should start with a capital letter and not end with a period.
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
info!("Connect to RDP host");
|
||||
|
||||
// BAD
|
||||
info!("connect to RDP host.");
|
||||
```
|
||||
|
||||
**Rationale**: consistency.
|
||||
Log messages are typically not composed together like error messages, so it’s fine to start with a capital letter.
|
||||
|
||||
Use tracing ability to [record structured fields][tracing-fields].
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
info!(%server_addr, "Looked up server address");
|
||||
|
||||
// BAD
|
||||
info!("Looked up server address: {server_addr}");
|
||||
```
|
||||
|
||||
**Rationale**: structured diagnostic information is tracing’s strength.
|
||||
It’s possible to retrieve the records emitted by tracing in a structured manner.
|
||||
|
||||
Name fields after what already exist consistently as much as possible.
|
||||
For example, errors are typically recorded as fields named `error`.
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
error!(?error, "Active stage failed");
|
||||
error!(error = ?e, "Active stage failed");
|
||||
error!(%error, "Active stage failed");
|
||||
error!(error = format!("{err:#}"), "Active stage failed");
|
||||
|
||||
// BAD
|
||||
error!(?e, "Active stage failed");
|
||||
error!(%err, "Active stage failed");
|
||||
```
|
||||
|
||||
**Rationale**: consistency.
|
||||
We can rely on this to filter and collect diagnostics.
|
||||
|
||||
[tracing-fields]: https://docs.rs/tracing/latest/tracing/index.html#recording-fields
|
||||
|
||||
## Helper functions
|
||||
|
||||
Avoid creating single-use helper functions:
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
let buf = {
|
||||
let mut buf = WriteBuf::new();
|
||||
buf.write_u32(42);
|
||||
buf
|
||||
};
|
||||
|
||||
// BAD
|
||||
let buf = prepare_buf(42);
|
||||
|
||||
// Somewhere else
|
||||
fn prepare_buf(value: u32) -> WriteBuf {
|
||||
let mut buf = WriteBuf::new();
|
||||
buf.write_u32(value);
|
||||
buf
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:** single-use functions change frequently, adding or removing parameters adds churn.
|
||||
A block serves just as well to delineate a bit of logic, but has access to all the context.
|
||||
Re-using originally single-purpose function often leads to bad coupling.
|
||||
|
||||
Exception: if you want to make use of `return` or `?`.
|
||||
|
||||
## Local helper functions
|
||||
|
||||
Put nested helper functions at the end of the enclosing functions (this requires using return statement).
|
||||
Don't nest more than one level deep.
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
fn func() -> u32 {
|
||||
return helper();
|
||||
|
||||
fn helper() -> u32 {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
|
||||
// BAD
|
||||
fn func() -> u32 {
|
||||
fn helper() -> u32 {
|
||||
/* ... */
|
||||
}
|
||||
|
||||
helper()
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:** consistency, improved top-down readability.
|
||||
|
||||
## Documentation
|
||||
|
||||
### Doc comments should link to reference documents
|
||||
|
||||
Add links to specification and/or other relevant documents in doc comments.
|
||||
Include verbatim the name of the section or the description of the item from the specification.
|
||||
Use reference-style links for readability.
|
||||
Do not make the link too long.
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
|
||||
/// [2.2.3.3.8] Server Drive Query Information Request (DR_DRIVE_QUERY_INFORMATION_REQ)
|
||||
///
|
||||
/// The server issues a query information request on a redirected file system device.
|
||||
///
|
||||
/// [2.2.3.3.8]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/e43dcd68-2980-40a9-9238-344b6cf94946
|
||||
pub struct ServerDriveQueryInformationRequest {
|
||||
/* snip */
|
||||
}
|
||||
|
||||
// BAD (no doc comment)
|
||||
|
||||
pub struct ServerDriveQueryInformationRequest {
|
||||
/* snip */
|
||||
}
|
||||
|
||||
// BAD (non reference-style links make barely readable, very long lines)
|
||||
|
||||
/// [2.2.3.3.8](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/e43dcd68-2980-40a9-9238-344b6cf94946) Server Drive Query Information Request (DR_DRIVE_QUERY_INFORMATION_REQ)
|
||||
///
|
||||
/// The server issues a query information request on a redirected file system device.
|
||||
pub struct ServerDriveQueryInformationRequest {
|
||||
/* snip */
|
||||
}
|
||||
|
||||
// BAD (long link)
|
||||
|
||||
/// [2.2.3.3.8 Server Drive Query Information Request (DR_DRIVE_QUERY_INFORMATION_REQ)]
|
||||
///
|
||||
/// The server issues a query information request on a redirected file system device.
|
||||
///
|
||||
/// [2.2.3.3.8 Server Drive Query Information Request (DR_DRIVE_QUERY_INFORMATION_REQ)]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/e43dcd68-2980-40a9-9238-344b6cf94946
|
||||
pub struct ServerDriveQueryInformationRequest {
|
||||
/* snip */
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale**: consistency.
|
||||
Easy cross-referencing between code and reference documents.
|
||||
|
||||
### Inline code comments are proper sentences
|
||||
|
||||
Style inline code comments as proper sentences.
|
||||
Start with a capital letter, end with a dot.
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
|
||||
// When building a library, `-` in the artifact name are replaced by `_`.
|
||||
let artifact_name = format!("{}.wasm", package.replace('-', "_"));
|
||||
|
||||
// BAD
|
||||
|
||||
// when building a library, `-` in the artifact name are replaced by `_`
|
||||
let artifact_name = format!("{}.wasm", package.replace('-', "_"));
|
||||
```
|
||||
|
||||
**Rationale:** writing a sentence (or maybe even a paragraph) rather just "a comment" creates a more appropriate frame of mind.
|
||||
It tricks you into writing down more of the context you keep in your head while coding.
|
||||
|
||||
Exception: no period for brief comments (e.g., `// VER`, `// RSV`, `// ATYP`)
|
||||
|
||||
### "Sentence per line" style
|
||||
|
||||
For `.md` and `.adoc` files, prefer a sentence-per-line format, don't wrap lines.
|
||||
If the line is too long, you want to split the sentence in two.
|
||||
|
||||
**Rationale:** much easier to edit the text and read the diff, see [this link][asciidoctor-practices].
|
||||
|
||||
[asciidoctor-practices]: https://asciidoctor.org/docs/asciidoc-recommended-practices/#one-sentence-per-line
|
||||
|
||||
## Invariants
|
||||
|
||||
Recommended reads:
|
||||
|
||||
- <https://en.wikipedia.org/wiki/Invariant_(mathematics)#Invariants_in_computer_science>
|
||||
- <https://en.wikipedia.org/wiki/Loop_invariant>
|
||||
- <https://en.wikipedia.org/wiki/Class_invariant>
|
||||
- <https://matklad.github.io/2023/10/06/what-is-an-invariant.html>
|
||||
- <https://matklad.github.io/2023/09/13/comparative-analysis.html>
|
||||
|
||||
### Write down invariants clearly
|
||||
|
||||
Write down invariants using `INVARIANT:` code comments.
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
|
||||
// INVARIANT: for i in 0..lo: xs[i] < x
|
||||
|
||||
// BAD
|
||||
|
||||
// for i in 0..lo: xs[i] < x
|
||||
```
|
||||
|
||||
**Rationale**: invariants should be upheld at all times.
|
||||
It’s useful to keep invariants in mind when analyzing the flow of the code.
|
||||
It’s easy to look up the local invariants when programming "in the small".
|
||||
|
||||
For field invariants, a doc comment should come at the place where they are declared, inside the type definition.
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
struct BitmapInfoHeader {
|
||||
/// INVARIANT: `width.abs() <= u16::MAX`
|
||||
width: i32,
|
||||
}
|
||||
|
||||
// BAD
|
||||
|
||||
/// INVARIANT: `width.abs() <= u16::MAX`
|
||||
struct BitmapInfoHeader {
|
||||
width: i32,
|
||||
}
|
||||
|
||||
// BAD
|
||||
struct BitmapInfoHeader {
|
||||
width: i32,
|
||||
}
|
||||
|
||||
impl BitmapInfoHeader {
|
||||
fn new(width: i32) -> Option<BitmapInfoHeader> {
|
||||
// INVARIANT: width.abs() <= u16::MAX
|
||||
if !(width.abs() <= i32::from(u16::MAX)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(BitmapInfoHeader { width })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale**: it’s easy to find about the invariant.
|
||||
The invariant will show up in the documentation (typically available by hovering the item in IDEs).
|
||||
|
||||
For loop invariants, the comment should come before or at the beginning of the loop.
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
|
||||
/// Computes the smallest index such that, if `x` is inserted at this index, the array remains sorted.
|
||||
fn insertion_point(xs: &[i32], x: i32) -> usize {
|
||||
let mut lo = 0;
|
||||
let mut hi = xs.len();
|
||||
|
||||
while lo < hi {
|
||||
// INVARIANT: for i in 0..lo: xs[i] < x
|
||||
// INVARIANT: for i in hi..: x <= xs[i]
|
||||
|
||||
let mid = lo + (hi - lo) / 2;
|
||||
if xs[mid] < x {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
|
||||
lo
|
||||
}
|
||||
|
||||
// BAD
|
||||
fn insertion_point(xs: &[i32], x: i32) -> usize {
|
||||
let mut lo = 0;
|
||||
let mut hi = xs.len();
|
||||
|
||||
while lo < hi {
|
||||
let mid = lo + (hi - lo) / 2;
|
||||
if xs[mid] < x {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
|
||||
// INVARIANT: for i in 0..lo: xs[i] < x
|
||||
// INVARIANT: for i in hi..: x <= xs[i]
|
||||
|
||||
lo
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale**: improved top-down readability, only read forward, no need to backtrack.
|
||||
|
||||
For function output invariants, the comment should be specified in the doc comment.
|
||||
(However, consider [enforcing this invariant][parse-dont-validate] using [the type system][type-safety] instead.)
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
|
||||
/// Computes the stride of an uncompressed RGB bitmap.
|
||||
///
|
||||
/// INVARIANT: `width <= output (stride) <= width * 4`
|
||||
fn rgb_bmp_stride(width: u16, bit_count: u16) -> usize {
|
||||
assert!(bit_count <= 32);
|
||||
let stride = /* ... */;
|
||||
stride
|
||||
}
|
||||
|
||||
// BAD
|
||||
|
||||
/// Computes the stride of an uncompressed RGB bitmap.
|
||||
fn rgb_bmp_stride(width: u16, bit_count: u16) -> usize {
|
||||
assert!(bit_count <= 32);
|
||||
// INVARIANT: width <= stride <= width * 4
|
||||
let stride = /* ... */;
|
||||
stride
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale**: it’s easy to find about the invariant.
|
||||
The invariant will show up in the documentation (typically available by hovering the item in IDEs).
|
||||
|
||||
[parse-dont-validate]: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
|
||||
[type-safety]: https://www.parsonsmatt.org/2017/10/11/type_safety_back_and_forth.html
|
||||
|
||||
### Explain non-obvious assumptions by referencing the invariants
|
||||
|
||||
Explain clearly non-obvious assumptions and invariants relied upon (e.g.: when disabling a lint locally).
|
||||
When referencing invariants, do not use the `INVARIANT:` comment prefix which is reserved for defining them.
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
|
||||
// Per invariants: width * dst_n_samples <= 10_000 * 4 < usize::MAX
|
||||
#[allow(clippy::arithmetic_side_effects)]
|
||||
let dst_stride = usize::from(width) * dst_n_samples;
|
||||
|
||||
// BAD
|
||||
#[allow(clippy::arithmetic_side_effects)]
|
||||
let dst_stride = usize::from(width) * dst_n_samples;
|
||||
|
||||
// BAD
|
||||
|
||||
// INVARIANT: width * dst_n_samples <= 10_000 * 4 < usize::MAX
|
||||
#[allow(clippy::arithmetic_side_effects)]
|
||||
let dst_stride = usize::from(width) * dst_n_samples;
|
||||
```
|
||||
|
||||
**Rationale**: make the assumption obvious.
|
||||
The code is easier to review.
|
||||
No one will lose time refactoring based on the wrong assumption.
|
||||
|
||||
### State invariants positively
|
||||
|
||||
Establish invariants positively.
|
||||
Prefer `if !invariant` to `if negated_invariant`.
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
if !(idx < len) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// GOOD
|
||||
check_invariant(idx < len)?;
|
||||
|
||||
// GOOD
|
||||
ensure!(idx < len);
|
||||
|
||||
// GOOD
|
||||
debug_assert!(idx < len);
|
||||
|
||||
// GOOD
|
||||
if idx < len {
|
||||
/* ... */
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
// BAD
|
||||
if idx >= len {
|
||||
return None;
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:** it's useful to see the invariant relied upon by the rest of the function clearly spelled out.
|
||||
|
||||
### Strongly prefer `<` and `<=` over `>` and `>=`
|
||||
|
||||
Use `<` and `<=` operators instead of `>` and `>=`.
|
||||
|
||||
```rust
|
||||
/// GOOD
|
||||
if lo <= x && x <= hi {}
|
||||
if x < lo || hi < x {}
|
||||
|
||||
/// BAD
|
||||
if x >= lo && x <= hi {}
|
||||
if x < lo || x > hi {}
|
||||
```
|
||||
|
||||
**Rationale**: consistent, canonicalized form that is trivial to visualize by reading from left to right.
|
||||
Things are naturally ordered from small to big like in the [number line].
|
||||
|
||||
[number line]: https://en.wikipedia.org/wiki/Number_line
|
||||
|
||||
## Context parameters
|
||||
|
||||
Some parameters are threaded unchanged through many function calls.
|
||||
They determine the "context" of the operation.
|
||||
Pass such parameters first, not last.
|
||||
If there are several context parameters, consider [packing them into a `struct Ctx` and passing it as `&self`][ra-ctx-struct].
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
fn do_something(connector: &mut ClientConnector, certificate: &[u8]) {
|
||||
let public_key = extract_public_key(certificate);
|
||||
do_something_else(connector, public_key, |kind| /* … */);
|
||||
}
|
||||
|
||||
fn do_something_else(connector: &mut ClientConnector, public_key: &[u8], op: impl Fn(KeyKind) -> bool) {
|
||||
/* ... */
|
||||
}
|
||||
|
||||
// BAD
|
||||
fn do_something(certificate: &[u8], connector: &mut ClientConnector) {
|
||||
let public_key = extract_public_key(certificate);
|
||||
do_something_else(|kind| /* … */, connector, public_key);
|
||||
}
|
||||
|
||||
fn do_something_else(op: impl Fn(KeyKind) -> bool, connector: &mut ClientConnector, public_key: &[u8]) {
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:** consistency.
|
||||
Context-first works better when non-context parameter is a lambda.
|
||||
|
||||
[ra-ctx-struct]: https://github.com/rust-lang/rust-analyzer/blob/76633199f4316b9c659d4ec0c102774d693cd940/crates/ide-db/src/path_transform.rs#L192-L339
|
||||
|
||||
# Runtime and compile time performance
|
||||
|
||||
## Avoid allocations
|
||||
|
||||
Avoid writing code which is slower than it needs to be.
|
||||
Don't allocate a `Vec` where an iterator would do, don't allocate strings needlessly.
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
let second_word = text.split(' ').nth(1)?;
|
||||
|
||||
// BAD
|
||||
let words: Vec<&str> = text.split(' ').collect();
|
||||
let second_word = words.get(1)?;
|
||||
```
|
||||
|
||||
**Rationale:** not allocating is almost always faster.
|
||||
|
||||
## Push allocations to the call site
|
||||
|
||||
If allocation is inevitable, let the caller allocate the resource:
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
fn frobnicate(s: String) {
|
||||
/* snip */
|
||||
}
|
||||
|
||||
// BAD
|
||||
fn frobnicate(s: &str) {
|
||||
let s = s.to_string();
|
||||
/* snip */
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:** reveals the costs.
|
||||
It is also more efficient when the caller already owns the allocation.
|
||||
|
||||
## Avoid monomorphization
|
||||
|
||||
Avoid making a lot of code type parametric, *especially* on the boundaries between crates.
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
fn frobnicate(f: impl FnMut()) {
|
||||
frobnicate_impl(&mut f)
|
||||
}
|
||||
fn frobnicate_impl(f: &mut dyn FnMut()) {
|
||||
/* lots of code */
|
||||
}
|
||||
|
||||
// BAD
|
||||
fn frobnicate(f: impl FnMut()) {
|
||||
/* lots of code */
|
||||
}
|
||||
```
|
||||
|
||||
Avoid `AsRef` polymorphism, it pays back only for widely used libraries:
|
||||
|
||||
```rust
|
||||
// GOOD
|
||||
fn frobnicate(f: &Path) { }
|
||||
|
||||
// BAD
|
||||
fn frobnicate(f: impl AsRef<Path>) { }
|
||||
```
|
||||
|
||||
**Rationale:** Rust uses monomorphization to compile generic code, meaning that for each instantiation of a generic functions with concrete types, the function is compiled afresh, *per crate*.
|
||||
This allows for fantastic performance, but leads to increased compile times.
|
||||
Runtime performance obeys the 80/20 rule (Pareto Principle) — only a small fraction of code is hot.
|
||||
Compile time **does not** obey this rule — all code has to be compiled.
|
||||
32
benches/Cargo.toml
Normal file
32
benches/Cargo.toml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
[package]
|
||||
name = "benches"
|
||||
version = "0.0.0"
|
||||
description = "IronRDP benchmarks"
|
||||
publish = false
|
||||
edition.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "perfenc"
|
||||
path = "src/perfenc.rs"
|
||||
|
||||
[features]
|
||||
default = ["qoi", "qoiz"]
|
||||
qoi = ["ironrdp/qoi"]
|
||||
qoiz = ["ironrdp/qoiz"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.99"
|
||||
async-trait = "0.1.89"
|
||||
bytesize = "2.3"
|
||||
ironrdp = { path = "../crates/ironrdp", features = [
|
||||
"server",
|
||||
"pdu",
|
||||
"__bench",
|
||||
] }
|
||||
pico-args = "0.5.0"
|
||||
tokio = { version = "1", features = ["sync", "fs", "time"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
210
benches/src/perfenc.rs
Normal file
210
benches/src/perfenc.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
#![allow(unused_crate_dependencies)] // False positives because there are both a library and a binary.
|
||||
#![allow(clippy::print_stderr)]
|
||||
#![allow(clippy::print_stdout)]
|
||||
|
||||
use core::num::{NonZeroU16, NonZeroUsize};
|
||||
use core::time::Duration;
|
||||
use std::io::Write as _;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use ironrdp::pdu::rdp::capability_sets::{CmdFlags, EntropyBits};
|
||||
use ironrdp::server::bench::encoder::{UpdateEncoder, UpdateEncoderCodecs};
|
||||
use ironrdp::server::{BitmapUpdate, DesktopSize, DisplayUpdate, PixelFormat, RdpServerDisplayUpdates};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt as _;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
setup_logging()?;
|
||||
let mut args = pico_args::Arguments::from_env();
|
||||
|
||||
if args.contains(["-h", "--help"]) {
|
||||
println!("Usage: perfenc [OPTIONS] <RGBX_INPUT_FILENAME>");
|
||||
println!();
|
||||
println!("Measure the performance of the IronRDP server encoder, given a raw RGBX video input file.");
|
||||
println!();
|
||||
println!("Options:");
|
||||
println!(" --width <WIDTH> Width of the display (default: 3840)");
|
||||
println!(" --height <HEIGHT> Height of the display (default: 2400)");
|
||||
println!(" --codec <CODEC> Codec to use (default: remotefx)");
|
||||
println!(" Valid values: qoi, qoiz, remotefx, bitmap, none");
|
||||
println!(" --fps <FPS> Frames per second (default: none)");
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let width = args.opt_value_from_str("--width")?.unwrap_or(3840);
|
||||
let height = args.opt_value_from_str("--height")?.unwrap_or(2400);
|
||||
let codec = args.opt_value_from_str("--codec")?.unwrap_or_else(OptCodec::default);
|
||||
let fps = args.opt_value_from_str("--fps")?.unwrap_or(0);
|
||||
|
||||
let filename: String = args.free_from_str().context("missing RGBX input filename")?;
|
||||
let file = File::open(&filename)
|
||||
.await
|
||||
.with_context(|| format!("Failed to open file: {filename}"))?;
|
||||
|
||||
let mut flags = CmdFlags::all();
|
||||
let mut update_codecs = UpdateEncoderCodecs::new();
|
||||
|
||||
match codec {
|
||||
OptCodec::RemoteFX => update_codecs.set_remotefx(Some((EntropyBits::Rlgr3, 0))),
|
||||
OptCodec::Bitmap => {
|
||||
flags -= CmdFlags::SET_SURFACE_BITS;
|
||||
}
|
||||
OptCodec::None => {}
|
||||
#[cfg(feature = "qoi")]
|
||||
OptCodec::Qoi => update_codecs.set_qoi(Some(0)),
|
||||
#[cfg(feature = "qoiz")]
|
||||
OptCodec::QoiZ => update_codecs.set_qoiz(Some(0)),
|
||||
};
|
||||
|
||||
let mut encoder = UpdateEncoder::new(DesktopSize { width, height }, flags, update_codecs)
|
||||
.context("failed to initialize update encoder")?;
|
||||
|
||||
let mut total_raw = 0u64;
|
||||
let mut total_enc = 0u64;
|
||||
let mut n_updates = 0u64;
|
||||
let mut updates = DisplayUpdates::new(file, DesktopSize { width, height }, fps);
|
||||
while let Some(up) = updates.next_update().await? {
|
||||
if let DisplayUpdate::Bitmap(ref up) = up {
|
||||
total_raw += u64::try_from(up.data.len())?;
|
||||
} else {
|
||||
eprintln!("Invalid update");
|
||||
break;
|
||||
}
|
||||
let mut iter = encoder.update(up);
|
||||
loop {
|
||||
let Some(frag) = iter.next().await else {
|
||||
break;
|
||||
};
|
||||
let len = u64::try_from(frag?.data.len())?;
|
||||
total_enc += len;
|
||||
}
|
||||
n_updates += 1;
|
||||
print!(".");
|
||||
std::io::stdout().flush()?;
|
||||
}
|
||||
println!();
|
||||
|
||||
#[expect(clippy::as_conversions, reason = "casting u64 to f64")]
|
||||
let ratio = total_enc as f64 / total_raw as f64;
|
||||
let percent = 100.0 - ratio * 100.0;
|
||||
println!("Encoder: {encoder:?}");
|
||||
println!("Nb updates: {n_updates:?}");
|
||||
println!(
|
||||
"Sum of bytes: {}/{} ({:.2}%)",
|
||||
bytesize::ByteSize(total_enc),
|
||||
bytesize::ByteSize(total_raw),
|
||||
percent,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct DisplayUpdates {
|
||||
file: File,
|
||||
desktop_size: DesktopSize,
|
||||
fps: u64,
|
||||
last_update_time: Option<Instant>,
|
||||
}
|
||||
|
||||
impl DisplayUpdates {
|
||||
fn new(file: File, desktop_size: DesktopSize, fps: u64) -> Self {
|
||||
Self {
|
||||
file,
|
||||
desktop_size,
|
||||
fps,
|
||||
last_update_time: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RdpServerDisplayUpdates for DisplayUpdates {
|
||||
async fn next_update(&mut self) -> anyhow::Result<Option<DisplayUpdate>> {
|
||||
let stride = self.desktop_size.width as usize * 4;
|
||||
let frame_size = stride * self.desktop_size.height as usize;
|
||||
let mut buf = vec![0u8; frame_size];
|
||||
// FIXME: AsyncReadExt::read_exact is not cancellation safe.
|
||||
self.file.read_exact(&mut buf).await.context("read exact")?;
|
||||
|
||||
let now = Instant::now();
|
||||
if let Some(last_update_time) = self.last_update_time {
|
||||
let elapsed = now - last_update_time;
|
||||
if self.fps > 0 && elapsed < Duration::from_millis(1000 / self.fps) {
|
||||
sleep(Duration::from_millis(
|
||||
1000 / self.fps
|
||||
- u64::try_from(elapsed.as_millis())
|
||||
.context("invalid `elapsed millis`: out of range integral conversion")?,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
self.last_update_time = Some(now);
|
||||
|
||||
let up = DisplayUpdate::Bitmap(BitmapUpdate {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: NonZeroU16::new(self.desktop_size.width).context("width cannot be zero")?,
|
||||
height: NonZeroU16::new(self.desktop_size.height).context("height cannot be zero")?,
|
||||
format: PixelFormat::RgbX32,
|
||||
data: buf.into(),
|
||||
stride: NonZeroUsize::new(stride).context("stride cannot be zero")?,
|
||||
});
|
||||
Ok(Some(up))
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_logging() -> anyhow::Result<()> {
|
||||
use tracing::metadata::LevelFilter;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
let fmt_layer = tracing_subscriber::fmt::layer().compact();
|
||||
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::WARN.into())
|
||||
.with_env_var("IRONRDP_LOG")
|
||||
.from_env_lossy();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt_layer)
|
||||
.with(env_filter)
|
||||
.try_init()
|
||||
.context("failed to set tracing global subscriber")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum OptCodec {
|
||||
RemoteFX,
|
||||
Bitmap,
|
||||
None,
|
||||
#[cfg(feature = "qoi")]
|
||||
Qoi,
|
||||
#[cfg(feature = "qoiz")]
|
||||
QoiZ,
|
||||
}
|
||||
|
||||
impl Default for OptCodec {
|
||||
fn default() -> Self {
|
||||
Self::RemoteFX
|
||||
}
|
||||
}
|
||||
|
||||
impl core::str::FromStr for OptCodec {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"remotefx" => Ok(Self::RemoteFX),
|
||||
"bitmap" => Ok(Self::Bitmap),
|
||||
"none" => Ok(Self::None),
|
||||
#[cfg(feature = "qoi")]
|
||||
"qoi" => Ok(Self::Qoi),
|
||||
#[cfg(feature = "qoiz")]
|
||||
"qoiz" => Ok(Self::QoiZ),
|
||||
_ => anyhow::bail!("unknown codec: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
13
ci/build.sh
13
ci/build.sh
|
|
@ -1,13 +0,0 @@
|
|||
set -ex
|
||||
|
||||
cargo fmt --all -- --check
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
cargo build
|
||||
cargo build --release
|
||||
|
||||
cargo build --all --exclude=ironrdp_client --target wasm32-unknown-unknown
|
||||
cargo build --all --exclude=ironrdp_client --target wasm32-unknown-unknown --release
|
||||
|
||||
cargo test
|
||||
cargo test --release
|
||||
94
cliff.toml
Normal file
94
cliff.toml
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Configuration file for git-cliff
|
||||
|
||||
[changelog]
|
||||
trim = false
|
||||
|
||||
header = """
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
"""
|
||||
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
body = """
|
||||
{% if version -%}
|
||||
## [[{{ version | trim_start_matches(pat="v") }}]{%- if release_link -%}({{ release_link }}){% endif %}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{%- else -%}
|
||||
## [Unreleased]
|
||||
{%- endif %}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") -%}
|
||||
|
||||
### {{ group | upper_first }}
|
||||
|
||||
{%- for commit in commits %}
|
||||
|
||||
{%- set message = commit.message | upper_first %}
|
||||
|
||||
{%- if commit.breaking %}
|
||||
{%- set breaking = "[**breaking**] " %}
|
||||
{%- else %}
|
||||
{%- set breaking = "" %}
|
||||
{%- endif %}
|
||||
|
||||
{%- set short_sha = commit.id | truncate(length=10, end="") %}
|
||||
{%- set commit_url = "https://github.com/Devolutions/IronRDP/commit/" ~ commit.id %}
|
||||
{%- set commit_link = "[" ~ short_sha ~ "](" ~ commit_url ~ ")" %}
|
||||
|
||||
- {{ breaking }}{{ message }} ({{ commit_link }}) \
|
||||
{% if commit.body %}\n\n {{ commit.body | replace(from="\n", to="\n ") }}{% endif %}
|
||||
{%- endfor %}
|
||||
|
||||
{% endfor -%}
|
||||
"""
|
||||
|
||||
footer = ""
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = false
|
||||
filter_commits = false
|
||||
date_order = false
|
||||
protect_breaking_commits = true
|
||||
sort_commits = "oldest"
|
||||
|
||||
commit_preprocessors = [
|
||||
# Replace the issue number with the link.
|
||||
{ pattern = "\\(#([0-9]+)\\)", replace = "([#${1}](https://github.com/Devolutions/IronRDP/issues/${1}))" },
|
||||
# Replace commit sha1 with the link.
|
||||
{ pattern = '([a-f0-9]{10})([a-f0-9]{30})', replace = "[${0}](https://github.com/Devolutions/IronRDP/commit/${1}${2})" },
|
||||
]
|
||||
|
||||
# regex for parsing and grouping commits
|
||||
# <!-- <NUMBER> --> is a trick to control the section order: https://github.com/orhun/git-cliff/issues/9#issuecomment-914521594
|
||||
commit_parsers = [
|
||||
{ message = "^chore", skip = true },
|
||||
{ message = "^style", skip = true },
|
||||
{ message = "^refactor", skip = true },
|
||||
{ message = "^test", skip = true },
|
||||
{ message = "^ci", skip = true },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ footer = "^[Cc]hangelog: ?ignore", skip = true },
|
||||
|
||||
{ message = "(?i)security", group = "<!-- 0 -->Security" },
|
||||
{ body = "(?i)security", group = "<!-- 0 -->Security" },
|
||||
{ footer = "^[Ss]ecurity: ?yes", group = "<!-- 0 -->Security" },
|
||||
|
||||
{ message = "^feat", group = "<!-- 1 -->Features" },
|
||||
|
||||
{ message = "^revert", group = "<!-- 3 -->Revert" },
|
||||
{ message = "^fix", group = "<!-- 4 -->Bug Fixes" },
|
||||
{ message = "^perf", group = "<!-- 5 -->Performance" },
|
||||
{ message = "^doc", group = "<!-- 6 -->Documentation" },
|
||||
{ message = "^build", group = "<!-- 7 -->Build" },
|
||||
|
||||
{ message = "(?i)improve", group = "<!-- 2 -->Improvements" },
|
||||
{ message = "(?i)adjust", group = "<!-- 2 -->Improvements" },
|
||||
{ message = "(?i)change", group = "<!-- 2 -->Improvements" },
|
||||
|
||||
{ message = ".*", group = "<!-- 99 -->Please Sort" },
|
||||
]
|
||||
6
clippy.toml
Normal file
6
clippy.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
msrv = "1.87"
|
||||
semicolon-outside-block-ignore-multiline = true
|
||||
accept-comment-above-statement = true
|
||||
accept-comment-above-attributes = true
|
||||
allow-panic-in-tests = true
|
||||
allow-unwrap-in-tests = true
|
||||
39
crates/iron-remote-desktop/CHANGELOG.md
Normal file
39
crates/iron-remote-desktop/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [[0.7.0](https://github.com/Devolutions/IronRDP/compare/iron-remote-desktop-v0.6.0...iron-remote-desktop-v0.7.0)] - 2025-09-29
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- [**breaking**] Changed onClipboardChanged to not consume the input (#992) ([6127e13c83](https://github.com/Devolutions/IronRDP/commit/6127e13c836d06764d483b6b55188fd23a4314a2))
|
||||
|
||||
## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/iron-remote-desktop-v0.5.0...iron-remote-desktop-v0.6.0)] - 2025-08-29
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- [**breaking**] Extend `DeviceEvent.wheelRotations` event to support passing rotation units other than pixels (#952) ([23c0cc2c36](https://github.com/Devolutions/IronRDP/commit/23c0cc2c365159d24330a89ec4015121b67bccb6))
|
||||
|
||||
## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/iron-remote-desktop-v0.4.0...iron-remote-desktop-v0.5.0)] - 2025-08-29
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- [**breaking**] Remove the `remote_received_format_list_callback` method from Session common API (#935) ([5b948e2161](https://github.com/Devolutions/IronRDP/commit/5b948e2161b08b13d32bdbb480b26c8fa44d42f7))
|
||||
|
||||
## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/iron-remote-desktop-v0.3.0...iron-remote-desktop-v0.4.0)] - 2025-06-27
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- [**breaking**] Add `canvas_resized_callback` method to `SessionBuilder` trait (#842) ([f6285c5989](https://github.com/Devolutions/IronRDP/commit/f6285c598915c8afb07553c765648d85ac4140cb))
|
||||
|
||||
## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/iron-remote-desktop-v0.2.0...iron-remote-desktop-v0.3.0)] - 2025-06-03
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- [**breaking**] Rename extension_call to invoke_extension (#803) ([f68cd06ac3](https://github.com/Devolutions/IronRDP/commit/f68cd06ac3705608e6f2ac6bde684d9ae906ea53))
|
||||
|
||||
|
||||
34
crates/iron-remote-desktop/Cargo.toml
Normal file
34
crates/iron-remote-desktop/Cargo.toml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "iron-remote-desktop"
|
||||
version = "0.7.0"
|
||||
readme = "README.md"
|
||||
description = "Helper crate for building WASM modules compatible with iron-remote-desktop WebComponent"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[features]
|
||||
panic_hook = ["dep:console_error_panic_hook"]
|
||||
|
||||
[dependencies]
|
||||
# WASM
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["HtmlCanvasElement"] }
|
||||
tracing-web = "0.1"
|
||||
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||
# code size when deploying.
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
|
||||
# Logging
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["time"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
8
crates/iron-remote-desktop/README.md
Normal file
8
crates/iron-remote-desktop/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Iron Remote Desktop — Helper Crate
|
||||
|
||||
Helper crate for building WASM modules compatible with the `iron-remote-desktop` WebComponent.
|
||||
|
||||
Implement the `RemoteDesktopApi` trait on a Rust type, and call the `make_bridge!` on
|
||||
it to generate the WASM API that is expected by `iron-remote-desktop`.
|
||||
|
||||
See the `ironrdp-web` crate in the repository to see how it is used in practice.
|
||||
23
crates/iron-remote-desktop/src/clipboard.rs
Normal file
23
crates/iron-remote-desktop/src/clipboard.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use wasm_bindgen::JsValue;
|
||||
|
||||
pub trait ClipboardData {
|
||||
type Item: ClipboardItem;
|
||||
|
||||
fn create() -> Self;
|
||||
|
||||
fn add_text(&mut self, mime_type: &str, text: &str);
|
||||
|
||||
fn add_binary(&mut self, mime_type: &str, binary: &[u8]);
|
||||
|
||||
fn items(&self) -> &[Self::Item];
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.items().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ClipboardItem {
|
||||
fn mime_type(&self) -> &str;
|
||||
|
||||
fn value(&self) -> impl Into<JsValue>;
|
||||
}
|
||||
10
crates/iron-remote-desktop/src/cursor.rs
Normal file
10
crates/iron-remote-desktop/src/cursor.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#[derive(Debug)]
|
||||
pub enum CursorStyle {
|
||||
Default,
|
||||
Hidden,
|
||||
Url {
|
||||
data: String,
|
||||
hotspot_x: u16,
|
||||
hotspot_y: u16,
|
||||
},
|
||||
}
|
||||
16
crates/iron-remote-desktop/src/desktop_size.rs
Normal file
16
crates/iron-remote-desktop/src/desktop_size.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct DesktopSize {
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DesktopSize {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn create(width: u16, height: u16) -> Self {
|
||||
DesktopSize { width, height }
|
||||
}
|
||||
}
|
||||
26
crates/iron-remote-desktop/src/error.rs
Normal file
26
crates/iron-remote-desktop/src/error.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub trait IronError {
|
||||
fn backtrace(&self) -> String;
|
||||
|
||||
fn kind(&self) -> IronErrorKind;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[wasm_bindgen]
|
||||
pub enum IronErrorKind {
|
||||
/// Catch-all error kind
|
||||
General,
|
||||
/// Incorrect password used
|
||||
WrongPassword,
|
||||
/// Unable to login to machine
|
||||
LogonFailure,
|
||||
/// Insufficient permission, server denied access
|
||||
AccessDenied,
|
||||
/// Something wrong happened when sending or receiving the RDCleanPath message
|
||||
RDCleanPath,
|
||||
/// Couldn’t connect to proxy
|
||||
ProxyConnect,
|
||||
/// Protocol negotiation failed
|
||||
NegotiationFailure,
|
||||
}
|
||||
76
crates/iron-remote-desktop/src/extension.rs
Normal file
76
crates/iron-remote-desktop/src/extension.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! extension_match {
|
||||
( @ $jsval:expr, $value:ident, String, $operation:block ) => {{
|
||||
if let Some($value) = $jsval.as_string() {
|
||||
$operation
|
||||
} else {
|
||||
warn!("Unexpected value for extension {}", stringify!($ident));
|
||||
}
|
||||
}};
|
||||
( @ $jsval:expr, $value:ident, f64, $operation:block ) => {{
|
||||
if let Some($value) = $jsval.as_f64() {
|
||||
$operation
|
||||
} else {
|
||||
warn!("Unexpected value for extension {}", stringify!($ident));
|
||||
}
|
||||
}};
|
||||
( @ $jsval:expr, $value:ident, bool, $operation:block ) => {{
|
||||
if let Some($value) = $jsval.as_bool() {
|
||||
$operation
|
||||
} else {
|
||||
warn!("Unexpected value for extension {}", stringify!($ident));
|
||||
}
|
||||
}};
|
||||
( @ $jsval:expr, $value:ident, JsValue, $operation:block ) => {{
|
||||
let $value = $jsval;
|
||||
$operation
|
||||
}};
|
||||
|
||||
( match $ext:ident ; $( | $value:ident : $ty:ident | $operation:block ; )* ) => {
|
||||
let ident = $ext.ident();
|
||||
|
||||
match ident {
|
||||
$( stringify!($value) => $crate::extension_match!( @ $ext.into_value(), $value, $ty, $operation ), )*
|
||||
unknown_extension => ::tracing::warn!("Unknown extension: {unknown_extension}"),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Extension {
|
||||
ident: String,
|
||||
value: JsValue,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Extension {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn create(ident: String, value: JsValue) -> Self {
|
||||
Self { ident, value }
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::allow_attributes,
|
||||
reason = "Unfortunately, expect attribute doesn't work with clippy::multiple_inherent_impl lint"
|
||||
)]
|
||||
#[allow(
|
||||
clippy::multiple_inherent_impl,
|
||||
reason = "We don't want to expose these methods to JS"
|
||||
)]
|
||||
impl Extension {
|
||||
pub fn ident(&self) -> &str {
|
||||
self.ident.as_str()
|
||||
}
|
||||
|
||||
pub fn value(&self) -> &JsValue {
|
||||
&self.value
|
||||
}
|
||||
|
||||
pub fn into_value(self) -> JsValue {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
34
crates/iron-remote-desktop/src/input.rs
Normal file
34
crates/iron-remote-desktop/src/input.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub enum RotationUnit {
|
||||
Pixel,
|
||||
Line,
|
||||
Page,
|
||||
}
|
||||
|
||||
pub trait DeviceEvent {
|
||||
fn mouse_button_pressed(button: u8) -> Self;
|
||||
|
||||
fn mouse_button_released(button: u8) -> Self;
|
||||
|
||||
fn mouse_move(x: u16, y: u16) -> Self;
|
||||
|
||||
fn wheel_rotations(vertical: bool, rotation_amount: i16, rotation_unit: RotationUnit) -> Self;
|
||||
|
||||
fn key_pressed(scancode: u16) -> Self;
|
||||
|
||||
fn key_released(scancode: u16) -> Self;
|
||||
|
||||
fn unicode_pressed(unicode: char) -> Self;
|
||||
|
||||
fn unicode_released(unicode: char) -> Self;
|
||||
}
|
||||
|
||||
pub trait InputTransaction {
|
||||
type DeviceEvent: DeviceEvent;
|
||||
|
||||
fn create() -> Self;
|
||||
|
||||
fn add_event(&mut self, event: Self::DeviceEvent);
|
||||
}
|
||||
480
crates/iron-remote-desktop/src/lib.rs
Normal file
480
crates/iron-remote-desktop/src/lib.rs
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
|
||||
|
||||
mod clipboard;
|
||||
mod cursor;
|
||||
mod desktop_size;
|
||||
mod error;
|
||||
mod extension;
|
||||
mod input;
|
||||
mod session;
|
||||
|
||||
pub use clipboard::{ClipboardData, ClipboardItem};
|
||||
pub use cursor::CursorStyle;
|
||||
pub use desktop_size::DesktopSize;
|
||||
pub use error::{IronError, IronErrorKind};
|
||||
pub use extension::Extension;
|
||||
pub use input::{DeviceEvent, InputTransaction, RotationUnit};
|
||||
pub use session::{Session, SessionBuilder, SessionTerminationInfo};
|
||||
|
||||
pub trait RemoteDesktopApi {
|
||||
type Session: Session;
|
||||
type SessionBuilder: SessionBuilder;
|
||||
type SessionTerminationInfo: SessionTerminationInfo;
|
||||
type DeviceEvent: DeviceEvent;
|
||||
type InputTransaction: InputTransaction;
|
||||
type ClipboardData: ClipboardData;
|
||||
type ClipboardItem: ClipboardItem;
|
||||
type Error: IronError;
|
||||
|
||||
/// Called before the logger is set.
|
||||
fn pre_setup() {}
|
||||
|
||||
/// Called after the logger is set.
|
||||
fn post_setup() {}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! make_bridge {
|
||||
($api:ty) => {
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub struct Session(<$api as $crate::RemoteDesktopApi>::Session);
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub struct SessionBuilder(<$api as $crate::RemoteDesktopApi>::SessionBuilder);
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub struct SessionTerminationInfo(<$api as $crate::RemoteDesktopApi>::SessionTerminationInfo);
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub struct DeviceEvent(<$api as $crate::RemoteDesktopApi>::DeviceEvent);
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub struct InputTransaction(<$api as $crate::RemoteDesktopApi>::InputTransaction);
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub struct ClipboardData(<$api as $crate::RemoteDesktopApi>::ClipboardData);
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub struct ClipboardItem(<$api as $crate::RemoteDesktopApi>::ClipboardItem);
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub struct IronError(<$api as $crate::RemoteDesktopApi>::Error);
|
||||
|
||||
impl From<<$api as $crate::RemoteDesktopApi>::Session> for Session {
|
||||
fn from(value: <$api as $crate::RemoteDesktopApi>::Session) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<<$api as $crate::RemoteDesktopApi>::SessionBuilder> for SessionBuilder {
|
||||
fn from(value: <$api as $crate::RemoteDesktopApi>::SessionBuilder) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<<$api as $crate::RemoteDesktopApi>::SessionTerminationInfo> for SessionTerminationInfo {
|
||||
fn from(value: <$api as $crate::RemoteDesktopApi>::SessionTerminationInfo) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<<$api as $crate::RemoteDesktopApi>::DeviceEvent> for DeviceEvent {
|
||||
fn from(value: <$api as $crate::RemoteDesktopApi>::DeviceEvent) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<<$api as $crate::RemoteDesktopApi>::InputTransaction> for InputTransaction {
|
||||
fn from(value: <$api as $crate::RemoteDesktopApi>::InputTransaction) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<<$api as $crate::RemoteDesktopApi>::ClipboardData> for ClipboardData {
|
||||
fn from(value: <$api as $crate::RemoteDesktopApi>::ClipboardData) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<<$api as $crate::RemoteDesktopApi>::ClipboardItem> for ClipboardItem {
|
||||
fn from(value: <$api as $crate::RemoteDesktopApi>::ClipboardItem) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<<$api as $crate::RemoteDesktopApi>::Error> for IronError {
|
||||
fn from(value: <$api as $crate::RemoteDesktopApi>::Error) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
#[doc(hidden)]
|
||||
pub fn setup(log_level: &str) {
|
||||
<$api as $crate::RemoteDesktopApi>::pre_setup();
|
||||
$crate::internal::setup(log_level);
|
||||
<$api as $crate::RemoteDesktopApi>::post_setup();
|
||||
}
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
#[doc(hidden)]
|
||||
impl Session {
|
||||
pub async fn run(&self) -> Result<SessionTerminationInfo, IronError> {
|
||||
$crate::Session::run(&self.0)
|
||||
.await
|
||||
.map(SessionTerminationInfo)
|
||||
.map_err(IronError)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = desktopSize)]
|
||||
pub fn desktop_size(&self) -> $crate::DesktopSize {
|
||||
$crate::Session::desktop_size(&self.0)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = applyInputs)]
|
||||
pub fn apply_inputs(&self, transaction: InputTransaction) -> Result<(), IronError> {
|
||||
$crate::Session::apply_inputs(&self.0, transaction.0).map_err(IronError)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = releaseAllInputs)]
|
||||
pub fn release_all_inputs(&self) -> Result<(), IronError> {
|
||||
$crate::Session::release_all_inputs(&self.0).map_err(IronError)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = synchronizeLockKeys)]
|
||||
pub fn synchronize_lock_keys(
|
||||
&self,
|
||||
scroll_lock: bool,
|
||||
num_lock: bool,
|
||||
caps_lock: bool,
|
||||
kana_lock: bool,
|
||||
) -> Result<(), IronError> {
|
||||
$crate::Session::synchronize_lock_keys(&self.0, scroll_lock, num_lock, caps_lock, kana_lock)
|
||||
.map_err(IronError)
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) -> Result<(), IronError> {
|
||||
$crate::Session::shutdown(&self.0).map_err(IronError)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = onClipboardPaste)]
|
||||
pub async fn on_clipboard_paste(&self, content: &ClipboardData) -> Result<(), IronError> {
|
||||
$crate::Session::on_clipboard_paste(&self.0, &content.0)
|
||||
.await
|
||||
.map_err(IronError)
|
||||
}
|
||||
|
||||
pub fn resize(
|
||||
&self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale_factor: Option<u32>,
|
||||
physical_width: Option<u32>,
|
||||
physical_height: Option<u32>,
|
||||
) {
|
||||
$crate::Session::resize(
|
||||
&self.0,
|
||||
width,
|
||||
height,
|
||||
scale_factor,
|
||||
physical_width,
|
||||
physical_height,
|
||||
);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = supportsUnicodeKeyboardShortcuts)]
|
||||
pub fn supports_unicode_keyboard_shortcuts(&self) -> bool {
|
||||
$crate::Session::supports_unicode_keyboard_shortcuts(&self.0)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = invokeExtension)]
|
||||
pub fn invoke_extension(
|
||||
&self,
|
||||
ext: $crate::Extension,
|
||||
) -> Result<$crate::internal::wasm_bindgen::JsValue, IronError> {
|
||||
<<$api as $crate::RemoteDesktopApi>::Session as $crate::Session>::invoke_extension(&self.0, ext)
|
||||
.map_err(IronError)
|
||||
}
|
||||
}
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
#[doc(hidden)]
|
||||
impl SessionBuilder {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn create() -> Self {
|
||||
Self(<<$api as $crate::RemoteDesktopApi>::SessionBuilder as $crate::SessionBuilder>::create())
|
||||
}
|
||||
|
||||
pub fn username(&self, username: String) -> Self {
|
||||
Self($crate::SessionBuilder::username(&self.0, username))
|
||||
}
|
||||
|
||||
pub fn destination(&self, destination: String) -> Self {
|
||||
Self($crate::SessionBuilder::destination(&self.0, destination))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = serverDomain)]
|
||||
pub fn server_domain(&self, server_domain: String) -> Self {
|
||||
Self($crate::SessionBuilder::server_domain(&self.0, server_domain))
|
||||
}
|
||||
|
||||
pub fn password(&self, password: String) -> Self {
|
||||
Self($crate::SessionBuilder::password(&self.0, password))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = proxyAddress)]
|
||||
pub fn proxy_address(&self, address: String) -> Self {
|
||||
Self($crate::SessionBuilder::proxy_address(&self.0, address))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = authToken)]
|
||||
pub fn auth_token(&self, token: String) -> Self {
|
||||
Self($crate::SessionBuilder::auth_token(&self.0, token))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = desktopSize)]
|
||||
pub fn desktop_size(&self, desktop_size: $crate::DesktopSize) -> Self {
|
||||
Self($crate::SessionBuilder::desktop_size(&self.0, desktop_size))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = renderCanvas)]
|
||||
pub fn render_canvas(&self, canvas: $crate::internal::web_sys::HtmlCanvasElement) -> Self {
|
||||
Self($crate::SessionBuilder::render_canvas(&self.0, canvas))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setCursorStyleCallback)]
|
||||
pub fn set_cursor_style_callback(&self, callback: $crate::internal::web_sys::js_sys::Function) -> Self {
|
||||
Self($crate::SessionBuilder::set_cursor_style_callback(
|
||||
&self.0, callback,
|
||||
))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setCursorStyleCallbackContext)]
|
||||
pub fn set_cursor_style_callback_context(&self, context: $crate::internal::wasm_bindgen::JsValue) -> Self {
|
||||
Self($crate::SessionBuilder::set_cursor_style_callback_context(
|
||||
&self.0, context,
|
||||
))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = remoteClipboardChangedCallback)]
|
||||
pub fn remote_clipboard_changed_callback(
|
||||
&self,
|
||||
callback: $crate::internal::web_sys::js_sys::Function,
|
||||
) -> Self {
|
||||
Self($crate::SessionBuilder::remote_clipboard_changed_callback(
|
||||
&self.0, callback,
|
||||
))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = forceClipboardUpdateCallback)]
|
||||
pub fn force_clipboard_update_callback(
|
||||
&self,
|
||||
callback: $crate::internal::web_sys::js_sys::Function,
|
||||
) -> Self {
|
||||
Self($crate::SessionBuilder::force_clipboard_update_callback(
|
||||
&self.0, callback,
|
||||
))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = canvasResizedCallback)]
|
||||
pub fn canvas_resized_callback(&self, callback: $crate::internal::web_sys::js_sys::Function) -> Self {
|
||||
Self($crate::SessionBuilder::canvas_resized_callback(&self.0, callback))
|
||||
}
|
||||
|
||||
pub fn extension(&self, ext: $crate::Extension) -> Self {
|
||||
Self($crate::SessionBuilder::extension(&self.0, ext))
|
||||
}
|
||||
|
||||
pub async fn connect(&self) -> Result<Session, IronError> {
|
||||
$crate::SessionBuilder::connect(&self.0)
|
||||
.await
|
||||
.map(Session)
|
||||
.map_err(IronError)
|
||||
}
|
||||
}
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
#[doc(hidden)]
|
||||
impl SessionTerminationInfo {
|
||||
pub fn reason(&self) -> String {
|
||||
$crate::SessionTerminationInfo::reason(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
#[doc(hidden)]
|
||||
impl DeviceEvent {
|
||||
#[wasm_bindgen(js_name = mouseButtonPressed)]
|
||||
pub fn mouse_button_pressed(button: u8) -> Self {
|
||||
Self(
|
||||
<<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::mouse_button_pressed(
|
||||
button,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = mouseButtonReleased)]
|
||||
pub fn mouse_button_released(button: u8) -> Self {
|
||||
Self(
|
||||
<<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::mouse_button_released(
|
||||
button,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = mouseMove)]
|
||||
pub fn mouse_move(x: u16, y: u16) -> Self {
|
||||
Self(<<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::mouse_move(x, y))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = wheelRotations)]
|
||||
pub fn wheel_rotations(vertical: bool, rotation_amount: i16, rotation_unit: $crate::RotationUnit) -> Self {
|
||||
Self(
|
||||
<<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::wheel_rotations(
|
||||
vertical,
|
||||
rotation_amount,
|
||||
rotation_unit,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = keyPressed)]
|
||||
pub fn key_pressed(scancode: u16) -> Self {
|
||||
Self(<<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::key_pressed(scancode))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = keyReleased)]
|
||||
pub fn key_released(scancode: u16) -> Self {
|
||||
Self(<<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::key_released(scancode))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = unicodePressed)]
|
||||
pub fn unicode_pressed(unicode: char) -> Self {
|
||||
Self(<<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::unicode_pressed(unicode))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = unicodeReleased)]
|
||||
pub fn unicode_released(unicode: char) -> Self {
|
||||
Self(
|
||||
<<$api as $crate::RemoteDesktopApi>::DeviceEvent as $crate::DeviceEvent>::unicode_released(unicode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
#[doc(hidden)]
|
||||
impl InputTransaction {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn create() -> Self {
|
||||
Self(<<$api as $crate::RemoteDesktopApi>::InputTransaction as $crate::InputTransaction>::create())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = addEvent)]
|
||||
pub fn add_event(&mut self, event: DeviceEvent) {
|
||||
$crate::InputTransaction::add_event(&mut self.0, event.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
#[doc(hidden)]
|
||||
impl ClipboardData {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn create() -> Self {
|
||||
Self(<<$api as $crate::RemoteDesktopApi>::ClipboardData as $crate::ClipboardData>::create())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = addText)]
|
||||
pub fn add_text(&mut self, mime_type: &str, text: &str) {
|
||||
$crate::ClipboardData::add_text(&mut self.0, mime_type, text);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = addBinary)]
|
||||
pub fn add_binary(&mut self, mime_type: &str, binary: &[u8]) {
|
||||
$crate::ClipboardData::add_binary(&mut self.0, mime_type, binary);
|
||||
}
|
||||
|
||||
pub fn items(&self) -> Vec<ClipboardItem> {
|
||||
$crate::ClipboardData::items(&self.0)
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.map(ClipboardItem)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = isEmpty)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
$crate::ClipboardData::is_empty(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
#[doc(hidden)]
|
||||
impl ClipboardItem {
|
||||
#[wasm_bindgen(js_name = mimeType)]
|
||||
pub fn mime_type(&self) -> String {
|
||||
$crate::ClipboardItem::mime_type(&self.0).to_owned()
|
||||
}
|
||||
|
||||
pub fn value(&self) -> $crate::internal::wasm_bindgen::JsValue {
|
||||
$crate::ClipboardItem::value(&self.0).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[$crate::internal::wasm_bindgen::prelude::wasm_bindgen]
|
||||
#[doc(hidden)]
|
||||
impl IronError {
|
||||
pub fn backtrace(&self) -> String {
|
||||
$crate::IronError::backtrace(&self.0)
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> $crate::IronErrorKind {
|
||||
$crate::IronError::kind(&self.0)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod internal {
|
||||
#[doc(hidden)]
|
||||
pub use wasm_bindgen;
|
||||
#[doc(hidden)]
|
||||
pub use web_sys;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn setup(log_level: &str) {
|
||||
// When the `console_error_panic_hook` feature is enabled, we can call the
|
||||
// `set_panic_hook` function at least once during initialization, and then
|
||||
// we will get better error messages if our code ever panics.
|
||||
//
|
||||
// For more details see
|
||||
// https://github.com/rustwasm/console_error_panic_hook#readme
|
||||
#[cfg(feature = "panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
if let Ok(level) = log_level.parse::<tracing::Level>() {
|
||||
set_logger_once(level);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_logger_once(level: tracing::Level) {
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
use tracing_subscriber::fmt::time::UtcTime;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_web::MakeConsoleWriter;
|
||||
|
||||
static INIT: std::sync::Once = std::sync::Once::new();
|
||||
|
||||
INIT.call_once(|| {
|
||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||
.with_ansi(false)
|
||||
.with_timer(UtcTime::rfc_3339()) // std::time is not available in browsers
|
||||
.with_writer(MakeConsoleWriter);
|
||||
|
||||
let level_filter = LevelFilter::from_level(level);
|
||||
|
||||
tracing_subscriber::registry().with(fmt_layer).with(level_filter).init();
|
||||
})
|
||||
}
|
||||
}
|
||||
106
crates/iron-remote-desktop/src/session.rs
Normal file
106
crates/iron-remote-desktop/src/session.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
use wasm_bindgen::JsValue;
|
||||
use web_sys::{js_sys, HtmlCanvasElement};
|
||||
|
||||
use crate::clipboard::ClipboardData;
|
||||
use crate::error::IronError;
|
||||
use crate::input::InputTransaction;
|
||||
use crate::{DesktopSize, Extension};
|
||||
|
||||
pub trait SessionBuilder {
|
||||
type Session: Session;
|
||||
type Error: IronError;
|
||||
|
||||
fn create() -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn username(&self, username: String) -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn destination(&self, destination: String) -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn server_domain(&self, server_domain: String) -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn password(&self, password: String) -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn proxy_address(&self, address: String) -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn auth_token(&self, token: String) -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn desktop_size(&self, desktop_size: DesktopSize) -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn render_canvas(&self, canvas: HtmlCanvasElement) -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn set_cursor_style_callback(&self, callback: js_sys::Function) -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn set_cursor_style_callback_context(&self, context: JsValue) -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn remote_clipboard_changed_callback(&self, callback: js_sys::Function) -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn force_clipboard_update_callback(&self, callback: js_sys::Function) -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn canvas_resized_callback(&self, callback: js_sys::Function) -> Self;
|
||||
|
||||
#[must_use]
|
||||
fn extension(&self, ext: Extension) -> Self;
|
||||
|
||||
#[expect(async_fn_in_trait)]
|
||||
async fn connect(&self) -> Result<Self::Session, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait Session {
|
||||
type SessionTerminationInfo: SessionTerminationInfo;
|
||||
type InputTransaction: InputTransaction;
|
||||
type ClipboardData: ClipboardData;
|
||||
type Error: IronError;
|
||||
|
||||
fn run(&self) -> impl core::future::Future<Output = Result<Self::SessionTerminationInfo, Self::Error>>;
|
||||
|
||||
fn desktop_size(&self) -> DesktopSize;
|
||||
|
||||
fn apply_inputs(&self, transaction: Self::InputTransaction) -> Result<(), Self::Error>;
|
||||
|
||||
fn release_all_inputs(&self) -> Result<(), Self::Error>;
|
||||
|
||||
fn synchronize_lock_keys(
|
||||
&self,
|
||||
scroll_lock: bool,
|
||||
num_lock: bool,
|
||||
caps_lock: bool,
|
||||
kana_lock: bool,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
fn shutdown(&self) -> Result<(), Self::Error>;
|
||||
|
||||
fn on_clipboard_paste(
|
||||
&self,
|
||||
content: &Self::ClipboardData,
|
||||
) -> impl core::future::Future<Output = Result<(), Self::Error>>;
|
||||
|
||||
fn resize(
|
||||
&self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale_factor: Option<u32>,
|
||||
physical_width: Option<u32>,
|
||||
physical_height: Option<u32>,
|
||||
);
|
||||
|
||||
fn supports_unicode_keyboard_shortcuts(&self) -> bool;
|
||||
|
||||
fn invoke_extension(&self, ext: Extension) -> Result<JsValue, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait SessionTerminationInfo {
|
||||
fn reason(&self) -> String;
|
||||
}
|
||||
77
crates/ironrdp-acceptor/CHANGELOG.md
Normal file
77
crates/ironrdp-acceptor/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.7.0...ironrdp-acceptor-v0.8.0)] - 2025-12-18
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- [**breaking**] Use static dispatch for NetworkClient trait ([#1043](https://github.com/Devolutions/IronRDP/issues/1043)) ([bca6d190a8](https://github.com/Devolutions/IronRDP/commit/bca6d190a870708468534d224ff225a658767a9a))
|
||||
|
||||
- Rename `AsyncNetworkClient` to `NetworkClient`
|
||||
- Replace dynamic dispatch (`Option<&mut dyn ...>`) with static dispatch
|
||||
using generics (`&mut N where N: NetworkClient`)
|
||||
- Reorder `connect_finalize` parameters for consistency across crates
|
||||
|
||||
## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.5.0...ironrdp-acceptor-v0.6.0)] - 2025-07-08
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- [**breaking**] Support for server-side Kerberos (#839) ([33530212c4](https://github.com/Devolutions/IronRDP/commit/33530212c42bf28c875ac078ed2408657831b417))
|
||||
|
||||
## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.4.0...ironrdp-acceptor-v0.5.0)] - 2025-05-27
|
||||
|
||||
### <!-- 1 -->Features
|
||||
|
||||
- Make the CredsspSequence type public ([5abd9ff8e0](https://github.com/Devolutions/IronRDP/commit/5abd9ff8e0da8ea48c6747526c4b703a39bf4972))
|
||||
|
||||
## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.3.1...ironrdp-acceptor-v0.4.0)] - 2025-03-12
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump ironrdp-pdu
|
||||
|
||||
## [[0.3.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.3.0...ironrdp-acceptor-v0.3.1)] - 2025-03-12
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa))
|
||||
|
||||
## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.2.1...ironrdp-acceptor-v0.3.0)] - 2025-01-28
|
||||
|
||||
### <!-- 0 -->Security
|
||||
|
||||
- Allow using basic RDP/no security ([7c72a9f9bb](https://github.com/Devolutions/IronRDP/commit/7c72a9f9bbe726d6f9f2377c19e9a672d8d086d5))
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Drop unexpected PDUs during deactivation-reactivation ([63963182b5](https://github.com/Devolutions/IronRDP/commit/63963182b5af6ad45dc638e93de4b8a0b565c7d3))
|
||||
|
||||
The current behavior of handling unmatched PDUs in fn read_by_hint()
|
||||
isn't good enough. An unexpected PDUs may be received and fail to be
|
||||
decoded during Acceptor::step().
|
||||
|
||||
Change the code to simply drop unexpected PDUs (as opposed to attempting
|
||||
to replay the unmatched leftover, which isn't clearly needed)
|
||||
|
||||
- Reattach existing channels ([c4587b537c](https://github.com/Devolutions/IronRDP/commit/c4587b537c7c0a148e11bc365bc3df88e2c92312))
|
||||
|
||||
I couldn't find any explicit behaviour described in the specification,
|
||||
but apparently, we must just keep the channel state as they were during
|
||||
reactivation. This fixes various state issues during client resize.
|
||||
|
||||
- Do not restart static channels on reactivation ([82c7c2f5b0](https://github.com/Devolutions/IronRDP/commit/82c7c2f5b08c44b1a4f6b04c13ad24d9e2ffa371))
|
||||
|
||||
### <!-- 6 -->Documentation
|
||||
|
||||
- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b))
|
||||
|
||||
## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.2.0...ironrdp-acceptor-v0.2.1)] - 2024-12-14
|
||||
|
||||
### Other
|
||||
|
||||
- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a))
|
||||
28
crates/ironrdp-acceptor/Cargo.toml
Normal file
28
crates/ironrdp-acceptor/Cargo.toml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "ironrdp-acceptor"
|
||||
version = "0.8.0"
|
||||
readme = "README.md"
|
||||
description = "State machines to drive an RDP connection acceptance sequence"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["alloc"] } # public
|
||||
ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6" } # public
|
||||
ironrdp-svc = { path = "../ironrdp-svc", version = "0.5" } # public
|
||||
ironrdp-connector = { path = "../ironrdp-connector", version = "0.8" } # public
|
||||
ironrdp-async = { path = "../ironrdp-async", version = "0.8" } # public
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
1
crates/ironrdp-acceptor/LICENSE-APACHE
Symbolic link
1
crates/ironrdp-acceptor/LICENSE-APACHE
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-APACHE
|
||||
1
crates/ironrdp-acceptor/LICENSE-MIT
Symbolic link
1
crates/ironrdp-acceptor/LICENSE-MIT
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-MIT
|
||||
9
crates/ironrdp-acceptor/README.md
Normal file
9
crates/ironrdp-acceptor/README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# IronRDP Acceptor
|
||||
|
||||
State machines to drive an RDP connection acceptance sequence.
|
||||
|
||||
For now, it requires the [Tokio runtime](https://tokio.rs/).
|
||||
|
||||
This crate is part of the [IronRDP] project.
|
||||
|
||||
[IronRDP]: https://github.com/Devolutions/IronRDP
|
||||
198
crates/ironrdp-acceptor/src/channel_connection.rs
Normal file
198
crates/ironrdp-acceptor/src/channel_connection.rs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use ironrdp_connector::{
|
||||
reason_err, ConnectorError, ConnectorErrorExt as _, ConnectorResult, Sequence, State, Written,
|
||||
};
|
||||
use ironrdp_core::WriteBuf;
|
||||
use ironrdp_pdu::mcs;
|
||||
use ironrdp_pdu::x224::X224;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChannelConnectionSequence {
|
||||
state: ChannelConnectionState,
|
||||
user_channel_id: u16,
|
||||
channel_ids: Option<HashSet<u16>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub enum ChannelConnectionState {
|
||||
#[default]
|
||||
Consumed,
|
||||
|
||||
WaitErectDomainRequest,
|
||||
WaitAttachUserRequest,
|
||||
SendAttachUserConfirm,
|
||||
WaitChannelJoinRequest {
|
||||
remaining: HashSet<u16>,
|
||||
},
|
||||
SendChannelJoinConfirm {
|
||||
remaining: HashSet<u16>,
|
||||
channel_id: u16,
|
||||
},
|
||||
AllJoined,
|
||||
}
|
||||
|
||||
impl State for ChannelConnectionState {
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Consumed => "Consumed",
|
||||
Self::WaitErectDomainRequest => "WaitErectDomainRequest",
|
||||
Self::WaitAttachUserRequest => "WaitAttachUserRequest",
|
||||
Self::SendAttachUserConfirm => "SendAttachUserConfirm",
|
||||
Self::WaitChannelJoinRequest { .. } => "WaitChannelJoinRequest",
|
||||
Self::SendChannelJoinConfirm { .. } => "SendChannelJoinConfirm",
|
||||
Self::AllJoined { .. } => "AllJoined",
|
||||
}
|
||||
}
|
||||
|
||||
fn is_terminal(&self) -> bool {
|
||||
matches!(self, Self::AllJoined { .. })
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn core::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sequence for ChannelConnectionSequence {
|
||||
fn next_pdu_hint(&self) -> Option<&dyn ironrdp_pdu::PduHint> {
|
||||
match &self.state {
|
||||
ChannelConnectionState::Consumed => None,
|
||||
ChannelConnectionState::WaitErectDomainRequest => Some(&ironrdp_pdu::X224_HINT),
|
||||
ChannelConnectionState::WaitAttachUserRequest => Some(&ironrdp_pdu::X224_HINT),
|
||||
ChannelConnectionState::SendAttachUserConfirm => None,
|
||||
ChannelConnectionState::WaitChannelJoinRequest { .. } => Some(&ironrdp_pdu::X224_HINT),
|
||||
ChannelConnectionState::SendChannelJoinConfirm { .. } => None,
|
||||
ChannelConnectionState::AllJoined => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn state(&self) -> &dyn State {
|
||||
&self.state
|
||||
}
|
||||
|
||||
fn step(&mut self, input: &[u8], output: &mut WriteBuf) -> ConnectorResult<Written> {
|
||||
let (written, next_state) = match core::mem::take(&mut self.state) {
|
||||
ChannelConnectionState::WaitErectDomainRequest => {
|
||||
let erect_domain_request = ironrdp_core::decode::<X224<mcs::ErectDomainPdu>>(input)
|
||||
.map_err(ConnectorError::decode)
|
||||
.map(|p| p.0)?;
|
||||
|
||||
debug!(message = ?erect_domain_request, "Received");
|
||||
|
||||
(Written::Nothing, ChannelConnectionState::WaitAttachUserRequest)
|
||||
}
|
||||
|
||||
ChannelConnectionState::WaitAttachUserRequest => {
|
||||
let attach_user_request = ironrdp_core::decode::<X224<mcs::AttachUserRequest>>(input)
|
||||
.map_err(ConnectorError::decode)
|
||||
.map(|p| p.0)?;
|
||||
|
||||
debug!(message = ?attach_user_request, "Received");
|
||||
|
||||
(Written::Nothing, ChannelConnectionState::SendAttachUserConfirm)
|
||||
}
|
||||
|
||||
ChannelConnectionState::SendAttachUserConfirm => {
|
||||
let attach_user_confirm = mcs::AttachUserConfirm {
|
||||
result: 0,
|
||||
initiator_id: self.user_channel_id,
|
||||
};
|
||||
|
||||
debug!(message = ?attach_user_confirm, "Send");
|
||||
|
||||
let written =
|
||||
ironrdp_core::encode_buf(&X224(attach_user_confirm), output).map_err(ConnectorError::encode)?;
|
||||
|
||||
let next_state = match self.channel_ids.take() {
|
||||
Some(channel_ids) => ChannelConnectionState::WaitChannelJoinRequest { remaining: channel_ids },
|
||||
None => ChannelConnectionState::AllJoined,
|
||||
};
|
||||
|
||||
(Written::from_size(written)?, next_state)
|
||||
}
|
||||
|
||||
ChannelConnectionState::WaitChannelJoinRequest { mut remaining } => {
|
||||
let channel_request = ironrdp_core::decode::<X224<mcs::ChannelJoinRequest>>(input)
|
||||
.map_err(ConnectorError::decode)
|
||||
.map(|p| p.0)?;
|
||||
|
||||
debug!(message = ?channel_request, "Received");
|
||||
|
||||
let is_expected = remaining.remove(&channel_request.channel_id);
|
||||
|
||||
if !is_expected {
|
||||
return Err(reason_err!(
|
||||
"ChannelJoinConfirm",
|
||||
"unexpected channel_id in MCS Channel Join Request: got {}, expected one of: {:?}",
|
||||
channel_request.channel_id,
|
||||
remaining,
|
||||
));
|
||||
}
|
||||
|
||||
(
|
||||
Written::Nothing,
|
||||
ChannelConnectionState::SendChannelJoinConfirm {
|
||||
remaining,
|
||||
channel_id: channel_request.channel_id,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
ChannelConnectionState::SendChannelJoinConfirm { remaining, channel_id } => {
|
||||
let channel_confirm = mcs::ChannelJoinConfirm {
|
||||
result: 0,
|
||||
initiator_id: self.user_channel_id,
|
||||
requested_channel_id: channel_id,
|
||||
channel_id,
|
||||
};
|
||||
|
||||
debug!(message = ?channel_confirm, "Send");
|
||||
|
||||
let written =
|
||||
ironrdp_core::encode_buf(&X224(channel_confirm), output).map_err(ConnectorError::encode)?;
|
||||
|
||||
let next_state = if remaining.is_empty() {
|
||||
ChannelConnectionState::AllJoined
|
||||
} else {
|
||||
ChannelConnectionState::WaitChannelJoinRequest { remaining }
|
||||
};
|
||||
|
||||
(Written::from_size(written)?, next_state)
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.state = next_state;
|
||||
Ok(written)
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelConnectionSequence {
|
||||
pub fn new(user_channel_id: u16, io_channel_id: u16, other_channels: Vec<u16>) -> Self {
|
||||
Self {
|
||||
state: ChannelConnectionState::WaitErectDomainRequest,
|
||||
user_channel_id,
|
||||
channel_ids: Some(
|
||||
vec![user_channel_id, io_channel_id]
|
||||
.into_iter()
|
||||
.chain(other_channels)
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn skip_channel_join(user_channel_id: u16) -> Self {
|
||||
Self {
|
||||
state: ChannelConnectionState::WaitErectDomainRequest,
|
||||
user_channel_id,
|
||||
channel_ids: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.state.is_terminal()
|
||||
}
|
||||
}
|
||||
760
crates/ironrdp-acceptor/src/connection.rs
Normal file
760
crates/ironrdp-acceptor/src/connection.rs
Normal file
|
|
@ -0,0 +1,760 @@
|
|||
use core::mem;
|
||||
|
||||
use ironrdp_connector::{
|
||||
encode_x224_packet, general_err, reason_err, ConnectorError, ConnectorErrorExt as _, ConnectorResult, DesktopSize,
|
||||
Sequence, State, Written,
|
||||
};
|
||||
use ironrdp_core::{decode, WriteBuf};
|
||||
use ironrdp_pdu as pdu;
|
||||
use ironrdp_pdu::nego::SecurityProtocol;
|
||||
use ironrdp_pdu::x224::X224;
|
||||
use ironrdp_svc::{StaticChannelSet, SvcServerProcessor};
|
||||
use pdu::rdp::capability_sets::CapabilitySet;
|
||||
use pdu::rdp::client_info::Credentials;
|
||||
use pdu::rdp::headers::ShareControlPdu;
|
||||
use pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, ServerSetErrorInfoPdu};
|
||||
use pdu::rdp::server_license::{LicensePdu, LicensingErrorMessage};
|
||||
use pdu::{gcc, mcs, nego, rdp};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use super::channel_connection::ChannelConnectionSequence;
|
||||
use super::finalization::FinalizationSequence;
|
||||
use crate::util::{self, wrap_share_data};
|
||||
|
||||
const IO_CHANNEL_ID: u16 = 1003;
|
||||
const USER_CHANNEL_ID: u16 = 1002;
|
||||
|
||||
pub struct Acceptor {
|
||||
pub(crate) state: AcceptorState,
|
||||
security: SecurityProtocol,
|
||||
io_channel_id: u16,
|
||||
user_channel_id: u16,
|
||||
desktop_size: DesktopSize,
|
||||
server_capabilities: Vec<CapabilitySet>,
|
||||
static_channels: StaticChannelSet,
|
||||
saved_for_reactivation: AcceptorState,
|
||||
pub(crate) creds: Option<Credentials>,
|
||||
reactivation: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AcceptorResult {
|
||||
pub static_channels: StaticChannelSet,
|
||||
pub capabilities: Vec<CapabilitySet>,
|
||||
pub input_events: Vec<Vec<u8>>,
|
||||
pub user_channel_id: u16,
|
||||
pub io_channel_id: u16,
|
||||
pub reactivation: bool,
|
||||
}
|
||||
|
||||
impl Acceptor {
|
||||
pub fn new(
|
||||
security: SecurityProtocol,
|
||||
desktop_size: DesktopSize,
|
||||
capabilities: Vec<CapabilitySet>,
|
||||
creds: Option<Credentials>,
|
||||
) -> Self {
|
||||
Self {
|
||||
security,
|
||||
state: AcceptorState::InitiationWaitRequest,
|
||||
user_channel_id: USER_CHANNEL_ID,
|
||||
io_channel_id: IO_CHANNEL_ID,
|
||||
desktop_size,
|
||||
server_capabilities: capabilities,
|
||||
static_channels: StaticChannelSet::new(),
|
||||
saved_for_reactivation: Default::default(),
|
||||
creds,
|
||||
reactivation: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_deactivation_reactivation(
|
||||
mut consumed: Acceptor,
|
||||
static_channels: StaticChannelSet,
|
||||
desktop_size: DesktopSize,
|
||||
) -> ConnectorResult<Self> {
|
||||
let AcceptorState::CapabilitiesSendServer {
|
||||
early_capability,
|
||||
channels,
|
||||
} = consumed.saved_for_reactivation
|
||||
else {
|
||||
return Err(general_err!("invalid acceptor state"));
|
||||
};
|
||||
|
||||
for cap in consumed.server_capabilities.iter_mut() {
|
||||
if let CapabilitySet::Bitmap(cap) = cap {
|
||||
cap.desktop_width = desktop_size.width;
|
||||
cap.desktop_height = desktop_size.height;
|
||||
}
|
||||
}
|
||||
let state = AcceptorState::CapabilitiesSendServer {
|
||||
early_capability,
|
||||
channels: channels.clone(),
|
||||
};
|
||||
let saved_for_reactivation = AcceptorState::CapabilitiesSendServer {
|
||||
early_capability,
|
||||
channels,
|
||||
};
|
||||
Ok(Self {
|
||||
security: consumed.security,
|
||||
state,
|
||||
user_channel_id: consumed.user_channel_id,
|
||||
io_channel_id: consumed.io_channel_id,
|
||||
desktop_size,
|
||||
server_capabilities: consumed.server_capabilities,
|
||||
static_channels,
|
||||
saved_for_reactivation,
|
||||
creds: consumed.creds,
|
||||
reactivation: true,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn attach_static_channel<T>(&mut self, channel: T)
|
||||
where
|
||||
T: SvcServerProcessor + 'static,
|
||||
{
|
||||
self.static_channels.insert(channel);
|
||||
}
|
||||
|
||||
pub fn reached_security_upgrade(&self) -> Option<SecurityProtocol> {
|
||||
match self.state {
|
||||
AcceptorState::SecurityUpgrade { .. } => Some(self.security),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if state is not [AcceptorState::SecurityUpgrade].
|
||||
pub fn mark_security_upgrade_as_done(&mut self) {
|
||||
assert!(self.reached_security_upgrade().is_some());
|
||||
self.step(&[], &mut WriteBuf::new()).expect("transition to next state");
|
||||
debug_assert!(self.reached_security_upgrade().is_none());
|
||||
}
|
||||
|
||||
pub fn should_perform_credssp(&self) -> bool {
|
||||
matches!(self.state, AcceptorState::Credssp { .. })
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if state is not [AcceptorState::Credssp].
|
||||
pub fn mark_credssp_as_done(&mut self) {
|
||||
assert!(self.should_perform_credssp());
|
||||
let res = self.step(&[], &mut WriteBuf::new()).expect("transition to next state");
|
||||
debug_assert!(!self.should_perform_credssp());
|
||||
assert_eq!(res, Written::Nothing);
|
||||
}
|
||||
|
||||
pub fn get_result(&mut self) -> Option<AcceptorResult> {
|
||||
match mem::take(&mut self.state) {
|
||||
AcceptorState::Accepted {
|
||||
channels: _channels, // TODO: what about ChannelDef?
|
||||
client_capabilities,
|
||||
input_events,
|
||||
} => Some(AcceptorResult {
|
||||
static_channels: mem::take(&mut self.static_channels),
|
||||
capabilities: client_capabilities,
|
||||
input_events,
|
||||
user_channel_id: self.user_channel_id,
|
||||
io_channel_id: self.io_channel_id,
|
||||
reactivation: self.reactivation,
|
||||
}),
|
||||
previous_state => {
|
||||
self.state = previous_state;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub enum AcceptorState {
|
||||
#[default]
|
||||
Consumed,
|
||||
|
||||
InitiationWaitRequest,
|
||||
InitiationSendConfirm {
|
||||
requested_protocol: SecurityProtocol,
|
||||
},
|
||||
SecurityUpgrade {
|
||||
requested_protocol: SecurityProtocol,
|
||||
protocol: SecurityProtocol,
|
||||
},
|
||||
Credssp {
|
||||
requested_protocol: SecurityProtocol,
|
||||
protocol: SecurityProtocol,
|
||||
},
|
||||
BasicSettingsWaitInitial {
|
||||
requested_protocol: SecurityProtocol,
|
||||
protocol: SecurityProtocol,
|
||||
},
|
||||
BasicSettingsSendResponse {
|
||||
requested_protocol: SecurityProtocol,
|
||||
protocol: SecurityProtocol,
|
||||
early_capability: Option<gcc::ClientEarlyCapabilityFlags>,
|
||||
channels: Vec<(u16, Option<gcc::ChannelDef>)>,
|
||||
},
|
||||
ChannelConnection {
|
||||
protocol: SecurityProtocol,
|
||||
early_capability: Option<gcc::ClientEarlyCapabilityFlags>,
|
||||
channels: Vec<(u16, gcc::ChannelDef)>,
|
||||
connection: ChannelConnectionSequence,
|
||||
},
|
||||
RdpSecurityCommencement {
|
||||
protocol: SecurityProtocol,
|
||||
early_capability: Option<gcc::ClientEarlyCapabilityFlags>,
|
||||
channels: Vec<(u16, gcc::ChannelDef)>,
|
||||
},
|
||||
SecureSettingsExchange {
|
||||
protocol: SecurityProtocol,
|
||||
early_capability: Option<gcc::ClientEarlyCapabilityFlags>,
|
||||
channels: Vec<(u16, gcc::ChannelDef)>,
|
||||
},
|
||||
LicensingExchange {
|
||||
early_capability: Option<gcc::ClientEarlyCapabilityFlags>,
|
||||
channels: Vec<(u16, gcc::ChannelDef)>,
|
||||
},
|
||||
CapabilitiesSendServer {
|
||||
early_capability: Option<gcc::ClientEarlyCapabilityFlags>,
|
||||
channels: Vec<(u16, gcc::ChannelDef)>,
|
||||
},
|
||||
MonitorLayoutSend {
|
||||
channels: Vec<(u16, gcc::ChannelDef)>,
|
||||
},
|
||||
CapabilitiesWaitConfirm {
|
||||
channels: Vec<(u16, gcc::ChannelDef)>,
|
||||
},
|
||||
ConnectionFinalization {
|
||||
finalization: FinalizationSequence,
|
||||
channels: Vec<(u16, gcc::ChannelDef)>,
|
||||
client_capabilities: Vec<CapabilitySet>,
|
||||
},
|
||||
Accepted {
|
||||
channels: Vec<(u16, gcc::ChannelDef)>,
|
||||
client_capabilities: Vec<CapabilitySet>,
|
||||
input_events: Vec<Vec<u8>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl State for AcceptorState {
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Consumed => "Consumed",
|
||||
Self::InitiationWaitRequest => "InitiationWaitRequest",
|
||||
Self::InitiationSendConfirm { .. } => "InitiationSendConfirm",
|
||||
Self::SecurityUpgrade { .. } => "SecurityUpgrade",
|
||||
Self::Credssp { .. } => "Credssp",
|
||||
Self::BasicSettingsWaitInitial { .. } => "BasicSettingsWaitInitial",
|
||||
Self::BasicSettingsSendResponse { .. } => "BasicSettingsSendResponse",
|
||||
Self::ChannelConnection { .. } => "ChannelConnection",
|
||||
Self::RdpSecurityCommencement { .. } => "RdpSecurityCommencement",
|
||||
Self::SecureSettingsExchange { .. } => "SecureSettingsExchange",
|
||||
Self::LicensingExchange { .. } => "LicensingExchange",
|
||||
Self::CapabilitiesSendServer { .. } => "CapabilitiesSendServer",
|
||||
Self::MonitorLayoutSend { .. } => "MonitorLayoutSend",
|
||||
Self::CapabilitiesWaitConfirm { .. } => "CapabilitiesWaitConfirm",
|
||||
Self::ConnectionFinalization { .. } => "ConnectionFinalization",
|
||||
Self::Accepted { .. } => "Connected",
|
||||
}
|
||||
}
|
||||
|
||||
fn is_terminal(&self) -> bool {
|
||||
matches!(self, Self::Accepted { .. })
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn core::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sequence for Acceptor {
|
||||
fn next_pdu_hint(&self) -> Option<&dyn pdu::PduHint> {
|
||||
match &self.state {
|
||||
AcceptorState::Consumed => None,
|
||||
AcceptorState::InitiationWaitRequest => Some(&pdu::X224_HINT),
|
||||
AcceptorState::InitiationSendConfirm { .. } => None,
|
||||
AcceptorState::SecurityUpgrade { .. } => None,
|
||||
AcceptorState::Credssp { .. } => None,
|
||||
AcceptorState::BasicSettingsWaitInitial { .. } => Some(&pdu::X224_HINT),
|
||||
AcceptorState::BasicSettingsSendResponse { .. } => None,
|
||||
AcceptorState::ChannelConnection { connection, .. } => connection.next_pdu_hint(),
|
||||
AcceptorState::RdpSecurityCommencement { .. } => None,
|
||||
AcceptorState::SecureSettingsExchange { .. } => Some(&pdu::X224_HINT),
|
||||
AcceptorState::LicensingExchange { .. } => None,
|
||||
AcceptorState::CapabilitiesSendServer { .. } => None,
|
||||
AcceptorState::MonitorLayoutSend { .. } => None,
|
||||
AcceptorState::CapabilitiesWaitConfirm { .. } => Some(&pdu::X224_HINT),
|
||||
AcceptorState::ConnectionFinalization { finalization, .. } => finalization.next_pdu_hint(),
|
||||
AcceptorState::Accepted { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn state(&self) -> &dyn State {
|
||||
&self.state
|
||||
}
|
||||
|
||||
fn step(&mut self, input: &[u8], output: &mut WriteBuf) -> ConnectorResult<Written> {
|
||||
let prev_state = mem::take(&mut self.state);
|
||||
|
||||
let (written, next_state) = match prev_state {
|
||||
AcceptorState::InitiationWaitRequest => {
|
||||
let connection_request = decode::<X224<nego::ConnectionRequest>>(input)
|
||||
.map_err(ConnectorError::decode)
|
||||
.map(|p| p.0)?;
|
||||
|
||||
debug!(message = ?connection_request, "Received");
|
||||
|
||||
(
|
||||
Written::Nothing,
|
||||
AcceptorState::InitiationSendConfirm {
|
||||
requested_protocol: connection_request.protocol,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
AcceptorState::InitiationSendConfirm { requested_protocol } => {
|
||||
let protocols = requested_protocol & self.security;
|
||||
let protocol = if protocols.intersects(SecurityProtocol::HYBRID_EX) {
|
||||
SecurityProtocol::HYBRID_EX
|
||||
} else if protocols.intersects(SecurityProtocol::HYBRID) {
|
||||
SecurityProtocol::HYBRID
|
||||
} else if protocols.intersects(SecurityProtocol::SSL) {
|
||||
SecurityProtocol::SSL
|
||||
} else if self.security.is_empty() {
|
||||
SecurityProtocol::empty()
|
||||
} else {
|
||||
return Err(ConnectorError::general("failed to negotiate security protocol"));
|
||||
};
|
||||
let connection_confirm = nego::ConnectionConfirm::Response {
|
||||
flags: nego::ResponseFlags::empty(),
|
||||
protocol,
|
||||
};
|
||||
|
||||
debug!(message = ?connection_confirm, "Send");
|
||||
|
||||
let written =
|
||||
ironrdp_core::encode_buf(&X224(connection_confirm), output).map_err(ConnectorError::encode)?;
|
||||
|
||||
(
|
||||
Written::from_size(written)?,
|
||||
AcceptorState::SecurityUpgrade {
|
||||
requested_protocol,
|
||||
protocol,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
AcceptorState::SecurityUpgrade {
|
||||
requested_protocol,
|
||||
protocol,
|
||||
} => {
|
||||
debug!(?requested_protocol);
|
||||
let next_state = if protocol.intersects(SecurityProtocol::HYBRID | SecurityProtocol::HYBRID_EX) {
|
||||
AcceptorState::Credssp {
|
||||
requested_protocol,
|
||||
protocol,
|
||||
}
|
||||
} else {
|
||||
AcceptorState::BasicSettingsWaitInitial {
|
||||
requested_protocol,
|
||||
protocol,
|
||||
}
|
||||
};
|
||||
(Written::Nothing, next_state)
|
||||
}
|
||||
|
||||
AcceptorState::Credssp {
|
||||
requested_protocol,
|
||||
protocol,
|
||||
} => (
|
||||
Written::Nothing,
|
||||
AcceptorState::BasicSettingsWaitInitial {
|
||||
requested_protocol,
|
||||
protocol,
|
||||
},
|
||||
),
|
||||
|
||||
AcceptorState::BasicSettingsWaitInitial {
|
||||
requested_protocol,
|
||||
protocol,
|
||||
} => {
|
||||
let x224_payload = decode::<X224<pdu::x224::X224Data<'_>>>(input)
|
||||
.map_err(ConnectorError::decode)
|
||||
.map(|p| p.0)?;
|
||||
let settings_initial =
|
||||
decode::<mcs::ConnectInitial>(x224_payload.data.as_ref()).map_err(ConnectorError::decode)?;
|
||||
|
||||
debug!(message = ?settings_initial, "Received");
|
||||
|
||||
let gcc_blocks = settings_initial.conference_create_request.into_gcc_blocks();
|
||||
let early_capability = gcc_blocks.core.optional_data.early_capability_flags;
|
||||
|
||||
let joined: Vec<_> = gcc_blocks
|
||||
.network
|
||||
.map(|network| {
|
||||
network
|
||||
.channels
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
self.static_channels
|
||||
.get_by_channel_name(&c.name)
|
||||
.map(|(type_id, _)| (type_id, c))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
#[expect(clippy::arithmetic_side_effects)] // IO channel ID is not big enough for overflowing.
|
||||
let channels = joined
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, channel)| {
|
||||
let channel_id = u16::try_from(i).expect("always in the range") + self.io_channel_id + 1;
|
||||
if let Some((type_id, c)) = channel {
|
||||
self.static_channels.attach_channel_id(type_id, channel_id);
|
||||
(channel_id, Some(c))
|
||||
} else {
|
||||
(channel_id, None)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
(
|
||||
Written::Nothing,
|
||||
AcceptorState::BasicSettingsSendResponse {
|
||||
requested_protocol,
|
||||
protocol,
|
||||
early_capability,
|
||||
channels,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
AcceptorState::BasicSettingsSendResponse {
|
||||
requested_protocol,
|
||||
protocol,
|
||||
early_capability,
|
||||
channels,
|
||||
} => {
|
||||
let channel_ids: Vec<u16> = channels.iter().map(|&(i, _)| i).collect();
|
||||
|
||||
let skip_channel_join = early_capability
|
||||
.is_some_and(|client| client.contains(gcc::ClientEarlyCapabilityFlags::SUPPORT_SKIP_CHANNELJOIN));
|
||||
|
||||
let server_blocks = create_gcc_blocks(
|
||||
self.io_channel_id,
|
||||
channel_ids.clone(),
|
||||
requested_protocol,
|
||||
skip_channel_join,
|
||||
);
|
||||
|
||||
let settings_response = mcs::ConnectResponse {
|
||||
conference_create_response: gcc::ConferenceCreateResponse::new(self.user_channel_id, server_blocks)
|
||||
.map_err(ConnectorError::decode)?,
|
||||
called_connect_id: 1,
|
||||
domain_parameters: mcs::DomainParameters::target(),
|
||||
};
|
||||
|
||||
debug!(message = ?settings_response, "Send");
|
||||
|
||||
let written = encode_x224_packet(&settings_response, output)?;
|
||||
let channels = channels.into_iter().filter_map(|(i, c)| c.map(|c| (i, c))).collect();
|
||||
|
||||
(
|
||||
Written::from_size(written)?,
|
||||
AcceptorState::ChannelConnection {
|
||||
protocol,
|
||||
early_capability,
|
||||
channels,
|
||||
connection: if skip_channel_join {
|
||||
ChannelConnectionSequence::skip_channel_join(self.user_channel_id)
|
||||
} else {
|
||||
ChannelConnectionSequence::new(self.user_channel_id, self.io_channel_id, channel_ids)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
AcceptorState::ChannelConnection {
|
||||
protocol,
|
||||
early_capability,
|
||||
channels,
|
||||
mut connection,
|
||||
} => {
|
||||
let written = connection.step(input, output)?;
|
||||
let state = if connection.is_done() {
|
||||
AcceptorState::RdpSecurityCommencement {
|
||||
protocol,
|
||||
early_capability,
|
||||
channels,
|
||||
}
|
||||
} else {
|
||||
AcceptorState::ChannelConnection {
|
||||
protocol,
|
||||
early_capability,
|
||||
channels,
|
||||
connection,
|
||||
}
|
||||
};
|
||||
|
||||
(written, state)
|
||||
}
|
||||
|
||||
AcceptorState::RdpSecurityCommencement {
|
||||
protocol,
|
||||
early_capability,
|
||||
channels,
|
||||
..
|
||||
} => (
|
||||
Written::Nothing,
|
||||
AcceptorState::SecureSettingsExchange {
|
||||
protocol,
|
||||
early_capability,
|
||||
channels,
|
||||
},
|
||||
),
|
||||
|
||||
AcceptorState::SecureSettingsExchange {
|
||||
protocol,
|
||||
early_capability,
|
||||
channels,
|
||||
} => {
|
||||
let data: X224<mcs::SendDataRequest<'_>> = decode(input).map_err(ConnectorError::decode)?;
|
||||
let data = data.0;
|
||||
let client_info: rdp::ClientInfoPdu =
|
||||
decode(data.user_data.as_ref()).map_err(ConnectorError::decode)?;
|
||||
|
||||
debug!(message = ?client_info, "Received");
|
||||
|
||||
if !protocol.intersects(SecurityProtocol::HYBRID | SecurityProtocol::HYBRID_EX) {
|
||||
let creds = client_info.client_info.credentials;
|
||||
|
||||
if self.creds.as_ref() != Some(&creds) {
|
||||
// FIXME: How authorization should be denied with standard RDP security?
|
||||
// Since standard RDP security is not a priority, we just send a ServerDeniedConnection ServerSetErrorInfo PDU.
|
||||
let info = ServerSetErrorInfoPdu(ErrorInfo::ProtocolIndependentCode(
|
||||
ProtocolIndependentCode::ServerDeniedConnection,
|
||||
));
|
||||
|
||||
debug!(message = ?info, "Send");
|
||||
|
||||
util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &info, output)?;
|
||||
|
||||
return Err(ConnectorError::general("invalid credentials"));
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
Written::Nothing,
|
||||
AcceptorState::LicensingExchange {
|
||||
early_capability,
|
||||
channels,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
AcceptorState::LicensingExchange {
|
||||
early_capability,
|
||||
channels,
|
||||
} => {
|
||||
let license: LicensePdu = LicensingErrorMessage::new_valid_client()
|
||||
.map_err(ConnectorError::encode)?
|
||||
.into();
|
||||
|
||||
debug!(message = ?license, "Send");
|
||||
|
||||
let written =
|
||||
util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &license, output)?;
|
||||
|
||||
self.saved_for_reactivation = AcceptorState::CapabilitiesSendServer {
|
||||
early_capability,
|
||||
channels: channels.clone(),
|
||||
};
|
||||
|
||||
(
|
||||
Written::from_size(written)?,
|
||||
AcceptorState::CapabilitiesSendServer {
|
||||
early_capability,
|
||||
channels,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
AcceptorState::CapabilitiesSendServer {
|
||||
early_capability,
|
||||
channels,
|
||||
} => {
|
||||
let demand_active = rdp::headers::ShareControlHeader {
|
||||
share_id: 0,
|
||||
pdu_source: self.io_channel_id,
|
||||
share_control_pdu: ShareControlPdu::ServerDemandActive(rdp::capability_sets::ServerDemandActive {
|
||||
pdu: rdp::capability_sets::DemandActive {
|
||||
source_descriptor: "".into(),
|
||||
capability_sets: self.server_capabilities.clone(),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
debug!(message = ?demand_active, "Send");
|
||||
|
||||
let written = util::encode_send_data_indication(
|
||||
self.user_channel_id,
|
||||
self.io_channel_id,
|
||||
&demand_active,
|
||||
output,
|
||||
)?;
|
||||
|
||||
let layout_flag = gcc::ClientEarlyCapabilityFlags::SUPPORT_MONITOR_LAYOUT_PDU;
|
||||
let next_state = if early_capability.is_some_and(|c| c.contains(layout_flag)) {
|
||||
AcceptorState::MonitorLayoutSend { channels }
|
||||
} else {
|
||||
AcceptorState::CapabilitiesWaitConfirm { channels }
|
||||
};
|
||||
|
||||
(Written::from_size(written)?, next_state)
|
||||
}
|
||||
|
||||
AcceptorState::MonitorLayoutSend { channels } => {
|
||||
let monitor_layout =
|
||||
rdp::headers::ShareDataPdu::MonitorLayout(rdp::finalization_messages::MonitorLayoutPdu {
|
||||
monitors: vec![gcc::Monitor {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: i32::from(self.desktop_size.width),
|
||||
bottom: i32::from(self.desktop_size.height),
|
||||
flags: gcc::MonitorFlags::PRIMARY,
|
||||
}],
|
||||
});
|
||||
|
||||
debug!(message = ?monitor_layout, "Send");
|
||||
|
||||
let share_data = wrap_share_data(monitor_layout, self.io_channel_id);
|
||||
|
||||
let written =
|
||||
util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &share_data, output)?;
|
||||
|
||||
(
|
||||
Written::from_size(written)?,
|
||||
AcceptorState::CapabilitiesWaitConfirm { channels },
|
||||
)
|
||||
}
|
||||
|
||||
AcceptorState::CapabilitiesWaitConfirm { ref channels } => {
|
||||
let message = decode::<X224<mcs::McsMessage<'_>>>(input)
|
||||
.map_err(ConnectorError::decode)
|
||||
.map(|p| p.0);
|
||||
let message = match message {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
if self.reactivation {
|
||||
debug!("Dropping unexpected PDU during reactivation");
|
||||
self.state = prev_state;
|
||||
return Ok(Written::Nothing);
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
match message {
|
||||
mcs::McsMessage::SendDataRequest(data) => {
|
||||
let capabilities_confirm = decode::<rdp::headers::ShareControlHeader>(data.user_data.as_ref())
|
||||
.map_err(ConnectorError::decode);
|
||||
let capabilities_confirm = match capabilities_confirm {
|
||||
Ok(capabilities_confirm) => capabilities_confirm,
|
||||
Err(e) => {
|
||||
if self.reactivation {
|
||||
debug!("Dropping unexpected PDU during reactivation");
|
||||
self.state = prev_state;
|
||||
return Ok(Written::Nothing);
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
debug!(message = ?capabilities_confirm, "Received");
|
||||
|
||||
let ShareControlPdu::ClientConfirmActive(confirm) = capabilities_confirm.share_control_pdu
|
||||
else {
|
||||
return Err(ConnectorError::general("expected client confirm active"));
|
||||
};
|
||||
|
||||
(
|
||||
Written::Nothing,
|
||||
AcceptorState::ConnectionFinalization {
|
||||
channels: channels.clone(),
|
||||
finalization: FinalizationSequence::new(self.user_channel_id, self.io_channel_id),
|
||||
client_capabilities: confirm.pdu.capability_sets,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
mcs::McsMessage::DisconnectProviderUltimatum(ultimatum) => {
|
||||
return Err(reason_err!("received disconnect ultimatum", "{:?}", ultimatum.reason))
|
||||
}
|
||||
|
||||
_ => {
|
||||
warn!(?message, "Unexpected MCS message received");
|
||||
|
||||
(Written::Nothing, prev_state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AcceptorState::ConnectionFinalization {
|
||||
mut finalization,
|
||||
channels,
|
||||
client_capabilities,
|
||||
} => {
|
||||
let written = finalization.step(input, output)?;
|
||||
|
||||
let state = if finalization.is_done() {
|
||||
AcceptorState::Accepted {
|
||||
channels,
|
||||
client_capabilities,
|
||||
input_events: finalization.into_input_events(),
|
||||
}
|
||||
} else {
|
||||
AcceptorState::ConnectionFinalization {
|
||||
finalization,
|
||||
channels,
|
||||
client_capabilities,
|
||||
}
|
||||
};
|
||||
|
||||
(written, state)
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.state = next_state;
|
||||
Ok(written)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_gcc_blocks(
|
||||
io_channel: u16,
|
||||
channel_ids: Vec<u16>,
|
||||
requested: SecurityProtocol,
|
||||
skip_channel_join: bool,
|
||||
) -> gcc::ServerGccBlocks {
|
||||
gcc::ServerGccBlocks {
|
||||
core: gcc::ServerCoreData {
|
||||
version: gcc::RdpVersion::V5_PLUS,
|
||||
optional_data: gcc::ServerCoreOptionalData {
|
||||
client_requested_protocols: Some(requested),
|
||||
early_capability_flags: skip_channel_join
|
||||
.then_some(gcc::ServerEarlyCapabilityFlags::SKIP_CHANNELJOIN_SUPPORTED),
|
||||
},
|
||||
},
|
||||
security: gcc::ServerSecurityData::no_security(),
|
||||
network: gcc::ServerNetworkData {
|
||||
channel_ids,
|
||||
io_channel,
|
||||
},
|
||||
message_channel: None,
|
||||
multi_transport_channel: None,
|
||||
}
|
||||
}
|
||||
184
crates/ironrdp-acceptor/src/credssp.rs
Normal file
184
crates/ironrdp-acceptor/src/credssp.rs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
use ironrdp_async::NetworkClient;
|
||||
use ironrdp_connector::sspi::credssp::{
|
||||
CredSspServer, CredentialsProxy, ServerError, ServerMode, ServerState, TsRequest,
|
||||
};
|
||||
use ironrdp_connector::sspi::generator::{Generator, GeneratorState};
|
||||
use ironrdp_connector::sspi::negotiate::ProtocolConfig;
|
||||
use ironrdp_connector::sspi::{self, AuthIdentity, KerberosServerConfig, NegotiateConfig, NetworkRequest, Username};
|
||||
use ironrdp_connector::{
|
||||
custom_err, general_err, ConnectorError, ConnectorErrorKind, ConnectorResult, ServerName, Written,
|
||||
};
|
||||
use ironrdp_core::{other_err, WriteBuf};
|
||||
use ironrdp_pdu::PduHint;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum CredsspState {
|
||||
Ongoing,
|
||||
Finished,
|
||||
ServerError(sspi::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct CredsspTsRequestHint;
|
||||
|
||||
const CREDSSP_TS_REQUEST_HINT: CredsspTsRequestHint = CredsspTsRequestHint;
|
||||
|
||||
impl PduHint for CredsspTsRequestHint {
|
||||
fn find_size(&self, bytes: &[u8]) -> ironrdp_core::DecodeResult<Option<(bool, usize)>> {
|
||||
match TsRequest::read_length(bytes) {
|
||||
Ok(length) => Ok(Some((true, length))),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None),
|
||||
Err(e) => Err(other_err!("CredsspTsRequestHint", source: e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type CredsspProcessGenerator<'a> =
|
||||
Generator<'a, NetworkRequest, sspi::Result<Vec<u8>>, Result<ServerState, ServerError>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CredsspSequence<'a> {
|
||||
server: CredSspServer<CredentialsProxyImpl<'a>>,
|
||||
state: CredsspState,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CredentialsProxyImpl<'a> {
|
||||
credentials: &'a AuthIdentity,
|
||||
}
|
||||
|
||||
impl<'a> CredentialsProxyImpl<'a> {
|
||||
fn new(credentials: &'a AuthIdentity) -> Self {
|
||||
Self { credentials }
|
||||
}
|
||||
}
|
||||
|
||||
impl CredentialsProxy for CredentialsProxyImpl<'_> {
|
||||
type AuthenticationData = AuthIdentity;
|
||||
|
||||
fn auth_data_by_user(&mut self, username: &Username) -> std::io::Result<Self::AuthenticationData> {
|
||||
if username.account_name() != self.credentials.username.account_name() {
|
||||
return Err(std::io::Error::other("invalid username"));
|
||||
}
|
||||
|
||||
let mut data = self.credentials.clone();
|
||||
// keep the original user/domain
|
||||
data.username = username.clone();
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_generator(
|
||||
generator: &mut CredsspProcessGenerator<'_>,
|
||||
network_client: &mut impl NetworkClient,
|
||||
) -> Result<ServerState, ServerError> {
|
||||
let mut state = generator.start();
|
||||
|
||||
loop {
|
||||
match state {
|
||||
GeneratorState::Suspended(request) => {
|
||||
let response = network_client.send(&request).await.map_err(|err| ServerError {
|
||||
ts_request: None,
|
||||
error: sspi::Error::new(sspi::ErrorKind::InternalError, err),
|
||||
})?;
|
||||
state = generator.resume(Ok(response));
|
||||
}
|
||||
GeneratorState::Completed(client_state) => break client_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CredsspSequence<'a> {
|
||||
pub fn next_pdu_hint(&self) -> ConnectorResult<Option<&dyn PduHint>> {
|
||||
match &self.state {
|
||||
CredsspState::Ongoing => Ok(Some(&CREDSSP_TS_REQUEST_HINT)),
|
||||
CredsspState::Finished => Ok(None),
|
||||
CredsspState::ServerError(err) => Err(custom_err!("Credssp server error", err.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
creds: &'a AuthIdentity,
|
||||
client_computer_name: ServerName,
|
||||
public_key: Vec<u8>,
|
||||
krb_config: Option<KerberosServerConfig>,
|
||||
) -> ConnectorResult<Self> {
|
||||
let client_computer_name = client_computer_name.into_inner();
|
||||
let credentials = CredentialsProxyImpl::new(creds);
|
||||
|
||||
let credssp_config: Box<dyn ProtocolConfig> = if let Some(krb_config) = krb_config {
|
||||
Box::new(krb_config)
|
||||
} else {
|
||||
Box::<sspi::ntlm::NtlmConfig>::default()
|
||||
};
|
||||
|
||||
let server = CredSspServer::new(
|
||||
public_key,
|
||||
credentials,
|
||||
ServerMode::Negotiate(NegotiateConfig {
|
||||
protocol_config: credssp_config,
|
||||
package_list: None,
|
||||
client_computer_name,
|
||||
}),
|
||||
)
|
||||
.map_err(|e| ConnectorError::new("CredSSP", ConnectorErrorKind::Credssp(e)))?;
|
||||
|
||||
let sequence = Self {
|
||||
server,
|
||||
state: CredsspState::Ongoing,
|
||||
};
|
||||
|
||||
Ok(sequence)
|
||||
}
|
||||
|
||||
/// Returns Some(ts_request) when a TS request is received from client,
|
||||
pub fn decode_client_message(&mut self, input: &[u8]) -> ConnectorResult<Option<TsRequest>> {
|
||||
match self.state {
|
||||
CredsspState::Ongoing => {
|
||||
let message = TsRequest::from_buffer(input).map_err(|e| custom_err!("TsRequest", e))?;
|
||||
debug!(?message, "Received");
|
||||
Ok(Some(message))
|
||||
}
|
||||
_ => Err(general_err!(
|
||||
"attempted to feed client request to CredSSP sequence in an unexpected state"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_ts_request(&mut self, request: TsRequest) -> CredsspProcessGenerator<'_> {
|
||||
self.server.process(request)
|
||||
}
|
||||
|
||||
pub fn handle_process_result(
|
||||
&mut self,
|
||||
result: Result<ServerState, ServerError>,
|
||||
output: &mut WriteBuf,
|
||||
) -> ConnectorResult<Written> {
|
||||
let (ts_request, next_state) = match result {
|
||||
Ok(ServerState::ReplyNeeded(ts_request)) => (Some(ts_request), CredsspState::Ongoing),
|
||||
Ok(ServerState::Finished(_id)) => (None, CredsspState::Finished),
|
||||
Err(err) => (
|
||||
err.ts_request.map(|ts_request| *ts_request),
|
||||
CredsspState::ServerError(err.error),
|
||||
),
|
||||
};
|
||||
|
||||
self.state = next_state;
|
||||
if let Some(ts_request) = ts_request {
|
||||
debug!(?ts_request, "Send");
|
||||
let length = usize::from(ts_request.buffer_len());
|
||||
let unfilled_buffer = output.unfilled_to(length);
|
||||
|
||||
ts_request
|
||||
.encode_ts_request(unfilled_buffer)
|
||||
.map_err(|e| custom_err!("TsRequest", e))?;
|
||||
|
||||
output.advance(length);
|
||||
|
||||
Ok(Written::from_size(length)?)
|
||||
} else {
|
||||
Ok(Written::Nothing)
|
||||
}
|
||||
}
|
||||
}
|
||||
249
crates/ironrdp-acceptor/src/finalization.rs
Normal file
249
crates/ironrdp-acceptor/src/finalization.rs
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
use ironrdp_connector::{ConnectorError, ConnectorErrorExt as _, ConnectorResult, Sequence, State, Written};
|
||||
use ironrdp_core::WriteBuf;
|
||||
use ironrdp_pdu::rdp;
|
||||
use ironrdp_pdu::x224::X224;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::util::{self, wrap_share_data};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FinalizationSequence {
|
||||
state: FinalizationState,
|
||||
user_channel_id: u16,
|
||||
io_channel_id: u16,
|
||||
|
||||
input_events: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub enum FinalizationState {
|
||||
#[default]
|
||||
Consumed,
|
||||
|
||||
WaitSynchronize,
|
||||
WaitControlCooperate,
|
||||
WaitRequestControl,
|
||||
WaitFontList,
|
||||
|
||||
SendSynchronizeConfirm,
|
||||
SendControlCooperateConfirm,
|
||||
SendGrantedControlConfirm,
|
||||
SendFontMap,
|
||||
|
||||
Finished,
|
||||
}
|
||||
|
||||
impl State for FinalizationState {
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Consumed => "Consumed",
|
||||
Self::WaitSynchronize => "WaitSynchronize",
|
||||
Self::WaitControlCooperate => "WaitControlCooperate",
|
||||
Self::WaitRequestControl => "WaitRequestControl",
|
||||
Self::WaitFontList => "WaitFontList",
|
||||
Self::SendSynchronizeConfirm => "SendSynchronizeConfirm",
|
||||
Self::SendControlCooperateConfirm => "SendControlCooperateConfirm",
|
||||
Self::SendGrantedControlConfirm => "SendGrantedControlConfirm",
|
||||
Self::SendFontMap => "SendFontMap",
|
||||
Self::Finished => "Finished",
|
||||
}
|
||||
}
|
||||
|
||||
fn is_terminal(&self) -> bool {
|
||||
matches!(self, Self::Finished { .. })
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn core::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sequence for FinalizationSequence {
|
||||
fn next_pdu_hint(&self) -> Option<&dyn ironrdp_pdu::PduHint> {
|
||||
match &self.state {
|
||||
FinalizationState::Consumed => None,
|
||||
FinalizationState::WaitSynchronize => Some(&ironrdp_pdu::X224Hint),
|
||||
FinalizationState::WaitControlCooperate => Some(&ironrdp_pdu::X224Hint),
|
||||
FinalizationState::WaitRequestControl => Some(&ironrdp_pdu::X224Hint),
|
||||
FinalizationState::WaitFontList => Some(&ironrdp_pdu::RdpHint),
|
||||
FinalizationState::SendSynchronizeConfirm => None,
|
||||
FinalizationState::SendControlCooperateConfirm => None,
|
||||
FinalizationState::SendGrantedControlConfirm => None,
|
||||
FinalizationState::SendFontMap => None,
|
||||
FinalizationState::Finished => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn state(&self) -> &dyn State {
|
||||
&self.state
|
||||
}
|
||||
|
||||
fn step(&mut self, input: &[u8], output: &mut WriteBuf) -> ConnectorResult<Written> {
|
||||
let (written, next_state) = match core::mem::take(&mut self.state) {
|
||||
FinalizationState::WaitSynchronize => {
|
||||
let synchronize = decode_share_control(input);
|
||||
|
||||
debug!(message = ?synchronize, "Received");
|
||||
|
||||
(Written::Nothing, FinalizationState::WaitControlCooperate)
|
||||
}
|
||||
|
||||
FinalizationState::WaitControlCooperate => {
|
||||
let cooperate = decode_share_control(input);
|
||||
|
||||
debug!(message = ?cooperate, "Received");
|
||||
|
||||
(Written::Nothing, FinalizationState::WaitRequestControl)
|
||||
}
|
||||
|
||||
FinalizationState::WaitRequestControl => {
|
||||
let control = decode_share_control(input)?;
|
||||
|
||||
debug!(message = ?control, "Received");
|
||||
|
||||
(Written::Nothing, FinalizationState::WaitFontList)
|
||||
}
|
||||
|
||||
FinalizationState::WaitFontList => match decode_font_list(input) {
|
||||
Ok(font_list) => {
|
||||
debug!(message = ?font_list, "Received");
|
||||
|
||||
(Written::Nothing, FinalizationState::SendSynchronizeConfirm)
|
||||
}
|
||||
|
||||
Err(()) => {
|
||||
self.input_events.push(input.to_vec());
|
||||
|
||||
(Written::Nothing, FinalizationState::WaitFontList)
|
||||
}
|
||||
},
|
||||
|
||||
FinalizationState::SendSynchronizeConfirm => {
|
||||
let synchronize_confirm = create_synchronize_confirm();
|
||||
|
||||
debug!(message = ?synchronize_confirm, "Send");
|
||||
|
||||
let share_data = wrap_share_data(synchronize_confirm, self.io_channel_id);
|
||||
let written =
|
||||
util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &share_data, output)?;
|
||||
|
||||
(
|
||||
Written::from_size(written)?,
|
||||
FinalizationState::SendControlCooperateConfirm,
|
||||
)
|
||||
}
|
||||
|
||||
FinalizationState::SendControlCooperateConfirm => {
|
||||
let cooperate_confirm = create_cooperate_confirm();
|
||||
|
||||
debug!(message = ?cooperate_confirm, "Send");
|
||||
|
||||
let share_data = wrap_share_data(cooperate_confirm, self.io_channel_id);
|
||||
let written =
|
||||
util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &share_data, output)?;
|
||||
|
||||
(
|
||||
Written::from_size(written)?,
|
||||
FinalizationState::SendGrantedControlConfirm,
|
||||
)
|
||||
}
|
||||
|
||||
FinalizationState::SendGrantedControlConfirm => {
|
||||
let control_confirm = create_control_confirm(self.user_channel_id);
|
||||
|
||||
debug!(message = ?control_confirm, "Send");
|
||||
|
||||
let share_data = wrap_share_data(control_confirm, self.io_channel_id);
|
||||
let written =
|
||||
util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &share_data, output)?;
|
||||
|
||||
(Written::from_size(written)?, FinalizationState::SendFontMap)
|
||||
}
|
||||
|
||||
FinalizationState::SendFontMap => {
|
||||
let font_map = create_font_map();
|
||||
|
||||
debug!(message = ?font_map, "Send");
|
||||
|
||||
let share_data = wrap_share_data(font_map, self.io_channel_id);
|
||||
let written =
|
||||
util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &share_data, output)?;
|
||||
|
||||
(Written::from_size(written)?, FinalizationState::Finished)
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.state = next_state;
|
||||
Ok(written)
|
||||
}
|
||||
}
|
||||
|
||||
impl FinalizationSequence {
|
||||
pub fn new(user_channel_id: u16, io_channel_id: u16) -> Self {
|
||||
Self {
|
||||
state: FinalizationState::WaitSynchronize,
|
||||
user_channel_id,
|
||||
io_channel_id,
|
||||
input_events: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_input_events(self) -> Vec<Vec<u8>> {
|
||||
self.input_events
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.state.is_terminal()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_synchronize_confirm() -> rdp::headers::ShareDataPdu {
|
||||
rdp::headers::ShareDataPdu::Synchronize(rdp::finalization_messages::SynchronizePdu { target_user_id: 0 })
|
||||
}
|
||||
|
||||
fn create_cooperate_confirm() -> rdp::headers::ShareDataPdu {
|
||||
rdp::headers::ShareDataPdu::Control(rdp::finalization_messages::ControlPdu {
|
||||
action: rdp::finalization_messages::ControlAction::Cooperate,
|
||||
grant_id: 0,
|
||||
control_id: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_control_confirm(user_id: u16) -> rdp::headers::ShareDataPdu {
|
||||
rdp::headers::ShareDataPdu::Control(rdp::finalization_messages::ControlPdu {
|
||||
action: rdp::finalization_messages::ControlAction::GrantedControl,
|
||||
grant_id: user_id,
|
||||
control_id: u32::from(rdp::capability_sets::SERVER_CHANNEL_ID),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_font_map() -> rdp::headers::ShareDataPdu {
|
||||
rdp::headers::ShareDataPdu::FontMap(rdp::finalization_messages::FontPdu::default())
|
||||
}
|
||||
|
||||
fn decode_share_control(input: &[u8]) -> ConnectorResult<rdp::headers::ShareControlHeader> {
|
||||
let data_request = ironrdp_core::decode::<X224<ironrdp_pdu::mcs::SendDataRequest<'_>>>(input)
|
||||
.map_err(ConnectorError::decode)
|
||||
.map(|p| p.0)?;
|
||||
let share_control = ironrdp_core::decode::<rdp::headers::ShareControlHeader>(data_request.user_data.as_ref())
|
||||
.map_err(ConnectorError::decode)?;
|
||||
Ok(share_control)
|
||||
}
|
||||
|
||||
fn decode_font_list(input: &[u8]) -> Result<rdp::finalization_messages::FontPdu, ()> {
|
||||
use ironrdp_pdu::rdp::headers::{ShareControlPdu, ShareDataPdu};
|
||||
|
||||
let share_control = decode_share_control(input).map_err(|_| ())?;
|
||||
|
||||
let ShareControlPdu::Data(data_pdu) = share_control.share_control_pdu else {
|
||||
return Err(());
|
||||
};
|
||||
|
||||
let ShareDataPdu::FontList(font_pdu) = data_pdu.share_data_pdu else {
|
||||
return Err(());
|
||||
};
|
||||
|
||||
Ok(font_pdu)
|
||||
}
|
||||
225
crates/ironrdp-acceptor/src/lib.rs
Normal file
225
crates/ironrdp-acceptor/src/lib.rs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
|
||||
|
||||
use ironrdp_async::{single_sequence_step, Framed, FramedRead, FramedWrite, NetworkClient, StreamWrapper};
|
||||
use ironrdp_connector::sspi::credssp::EarlyUserAuthResult;
|
||||
use ironrdp_connector::sspi::{AuthIdentity, KerberosServerConfig, Username};
|
||||
use ironrdp_connector::{custom_err, general_err, ConnectorResult, ServerName};
|
||||
use ironrdp_core::WriteBuf;
|
||||
use tracing::{debug, instrument, trace};
|
||||
|
||||
mod channel_connection;
|
||||
mod connection;
|
||||
pub mod credssp;
|
||||
mod finalization;
|
||||
mod util;
|
||||
|
||||
pub use ironrdp_connector::DesktopSize;
|
||||
use ironrdp_pdu::nego;
|
||||
|
||||
pub use self::channel_connection::{ChannelConnectionSequence, ChannelConnectionState};
|
||||
pub use self::connection::{Acceptor, AcceptorResult, AcceptorState};
|
||||
pub use self::finalization::{FinalizationSequence, FinalizationState};
|
||||
use crate::credssp::resolve_generator;
|
||||
|
||||
pub enum BeginResult<S>
|
||||
where
|
||||
S: StreamWrapper,
|
||||
{
|
||||
ShouldUpgrade(S::InnerStream),
|
||||
Continue(Framed<S>),
|
||||
}
|
||||
|
||||
pub async fn accept_begin<S>(mut framed: Framed<S>, acceptor: &mut Acceptor) -> ConnectorResult<BeginResult<S>>
|
||||
where
|
||||
S: FramedRead + FramedWrite + StreamWrapper,
|
||||
{
|
||||
let mut buf = WriteBuf::new();
|
||||
|
||||
loop {
|
||||
if let Some(security) = acceptor.reached_security_upgrade() {
|
||||
let result = if security.is_empty() {
|
||||
BeginResult::Continue(framed)
|
||||
} else {
|
||||
BeginResult::ShouldUpgrade(framed.into_inner_no_leftover())
|
||||
};
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
single_sequence_step(&mut framed, acceptor, &mut buf).await?;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn accept_credssp<S, N>(
|
||||
framed: &mut Framed<S>,
|
||||
acceptor: &mut Acceptor,
|
||||
network_client: &mut N,
|
||||
client_computer_name: ServerName,
|
||||
public_key: Vec<u8>,
|
||||
kerberos_config: Option<KerberosServerConfig>,
|
||||
) -> ConnectorResult<()>
|
||||
where
|
||||
S: FramedRead + FramedWrite,
|
||||
N: NetworkClient,
|
||||
{
|
||||
let mut buf = WriteBuf::new();
|
||||
|
||||
if acceptor.should_perform_credssp() {
|
||||
perform_credssp_step(
|
||||
framed,
|
||||
acceptor,
|
||||
network_client,
|
||||
&mut buf,
|
||||
client_computer_name,
|
||||
public_key,
|
||||
kerberos_config,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn accept_finalize<S>(
|
||||
mut framed: Framed<S>,
|
||||
acceptor: &mut Acceptor,
|
||||
) -> ConnectorResult<(Framed<S>, AcceptorResult)>
|
||||
where
|
||||
S: FramedRead + FramedWrite,
|
||||
{
|
||||
let mut buf = WriteBuf::new();
|
||||
|
||||
loop {
|
||||
if let Some(result) = acceptor.get_result() {
|
||||
return Ok((framed, result));
|
||||
}
|
||||
single_sequence_step(&mut framed, acceptor, &mut buf).await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all, ret)]
|
||||
async fn perform_credssp_step<S, N>(
|
||||
framed: &mut Framed<S>,
|
||||
acceptor: &mut Acceptor,
|
||||
network_client: &mut N,
|
||||
buf: &mut WriteBuf,
|
||||
client_computer_name: ServerName,
|
||||
public_key: Vec<u8>,
|
||||
kerberos_config: Option<KerberosServerConfig>,
|
||||
) -> ConnectorResult<()>
|
||||
where
|
||||
S: FramedRead + FramedWrite,
|
||||
N: NetworkClient,
|
||||
{
|
||||
assert!(acceptor.should_perform_credssp());
|
||||
let AcceptorState::Credssp { protocol, .. } = acceptor.state else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let result = credssp_loop(
|
||||
framed,
|
||||
acceptor,
|
||||
network_client,
|
||||
buf,
|
||||
client_computer_name,
|
||||
public_key,
|
||||
kerberos_config,
|
||||
)
|
||||
.await;
|
||||
|
||||
if protocol.intersects(nego::SecurityProtocol::HYBRID_EX) {
|
||||
trace!(?result, "HYBRID_EX");
|
||||
|
||||
let result = if result.is_ok() {
|
||||
EarlyUserAuthResult::Success
|
||||
} else {
|
||||
EarlyUserAuthResult::AccessDenied
|
||||
};
|
||||
|
||||
buf.clear();
|
||||
result
|
||||
.to_buffer(&mut *buf)
|
||||
.map_err(|e| ironrdp_connector::custom_err!("to_buffer", e))?;
|
||||
let response = &buf[..result.buffer_len()];
|
||||
framed
|
||||
.write_all(response)
|
||||
.await
|
||||
.map_err(|e| ironrdp_connector::custom_err!("write all", e))?;
|
||||
}
|
||||
|
||||
result?;
|
||||
|
||||
acceptor.mark_credssp_as_done();
|
||||
|
||||
return Ok(());
|
||||
|
||||
async fn credssp_loop<S, N>(
|
||||
framed: &mut Framed<S>,
|
||||
acceptor: &mut Acceptor,
|
||||
network_client: &mut N,
|
||||
buf: &mut WriteBuf,
|
||||
client_computer_name: ServerName,
|
||||
public_key: Vec<u8>,
|
||||
kerberos_config: Option<KerberosServerConfig>,
|
||||
) -> ConnectorResult<()>
|
||||
where
|
||||
S: FramedRead + FramedWrite,
|
||||
N: NetworkClient,
|
||||
{
|
||||
let creds = acceptor
|
||||
.creds
|
||||
.as_ref()
|
||||
.ok_or_else(|| general_err!("no credentials while doing credssp"))?;
|
||||
let username = Username::new(&creds.username, None).map_err(|e| custom_err!("invalid username", e))?;
|
||||
let identity = AuthIdentity {
|
||||
username,
|
||||
password: creds.password.clone().into(),
|
||||
};
|
||||
|
||||
let mut sequence =
|
||||
credssp::CredsspSequence::init(&identity, client_computer_name, public_key, kerberos_config)?;
|
||||
|
||||
loop {
|
||||
let Some(next_pdu_hint) = sequence.next_pdu_hint()? else {
|
||||
break;
|
||||
};
|
||||
|
||||
debug!(
|
||||
acceptor.state = ?acceptor.state,
|
||||
hint = ?next_pdu_hint,
|
||||
"Wait for PDU"
|
||||
);
|
||||
|
||||
let pdu = framed
|
||||
.read_by_hint(next_pdu_hint)
|
||||
.await
|
||||
.map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?;
|
||||
|
||||
trace!(length = pdu.len(), "PDU received");
|
||||
|
||||
let Some(ts_request) = sequence.decode_client_message(&pdu)? else {
|
||||
break;
|
||||
};
|
||||
|
||||
let result = {
|
||||
let mut generator = sequence.process_ts_request(ts_request);
|
||||
resolve_generator(&mut generator, network_client).await
|
||||
}; // drop generator
|
||||
|
||||
buf.clear();
|
||||
let written = sequence.handle_process_result(result, buf)?;
|
||||
|
||||
if let Some(response_len) = written.size() {
|
||||
let response = &buf[..response_len];
|
||||
trace!(response_len, "Send response");
|
||||
framed
|
||||
.write_all(response)
|
||||
.await
|
||||
.map_err(|e| ironrdp_connector::custom_err!("write all", e))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
41
crates/ironrdp-acceptor/src/util.rs
Normal file
41
crates/ironrdp-acceptor/src/util.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use ironrdp_connector::{ConnectorError, ConnectorErrorExt as _, ConnectorResult};
|
||||
use ironrdp_core::{encode_vec, Encode, WriteBuf};
|
||||
use ironrdp_pdu::rdp;
|
||||
use ironrdp_pdu::x224::X224;
|
||||
|
||||
pub(crate) fn encode_send_data_indication<T>(
|
||||
initiator_id: u16,
|
||||
channel_id: u16,
|
||||
user_msg: &T,
|
||||
buf: &mut WriteBuf,
|
||||
) -> ConnectorResult<usize>
|
||||
where
|
||||
T: Encode,
|
||||
{
|
||||
let user_data = encode_vec(user_msg).map_err(ConnectorError::encode)?;
|
||||
|
||||
let pdu = ironrdp_pdu::mcs::SendDataIndication {
|
||||
initiator_id,
|
||||
channel_id,
|
||||
user_data: Cow::Owned(user_data),
|
||||
};
|
||||
|
||||
let written = ironrdp_core::encode_buf(&X224(pdu), buf).map_err(ConnectorError::encode)?;
|
||||
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
pub(crate) fn wrap_share_data(pdu: rdp::headers::ShareDataPdu, io_channel_id: u16) -> rdp::headers::ShareControlHeader {
|
||||
rdp::headers::ShareControlHeader {
|
||||
share_id: 0,
|
||||
pdu_source: io_channel_id,
|
||||
share_control_pdu: rdp::headers::ShareControlPdu::Data(rdp::headers::ShareDataHeader {
|
||||
share_data_pdu: pdu,
|
||||
stream_priority: rdp::headers::StreamPriority::Undefined,
|
||||
compression_flags: rdp::headers::CompressionFlags::empty(),
|
||||
compression_type: rdp::client_info::CompressionType::K8,
|
||||
}),
|
||||
}
|
||||
}
|
||||
36
crates/ironrdp-ainput/CHANGELOG.md
Normal file
36
crates/ironrdp-ainput/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-ainput-v0.2.0...ironrdp-ainput-v0.2.1)] - 2025-05-27
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump bitflags from 2.9.0 to 2.9.1 in the patch group across 1 directory (#792) ([87ed315bc2](https://github.com/Devolutions/IronRDP/commit/87ed315bc28fdd2dcfea89b052fa620a7e346e5a))
|
||||
|
||||
|
||||
|
||||
## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-ainput-v0.1.2...ironrdp-ainput-v0.1.3)] - 2025-03-12
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa))
|
||||
|
||||
|
||||
## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-ainput-v0.1.1...ironrdp-ainput-v0.1.2)] - 2025-01-28
|
||||
|
||||
### <!-- 6 -->Documentation
|
||||
|
||||
- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b))
|
||||
|
||||
|
||||
|
||||
## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-ainput-v0.1.0...ironrdp-ainput-v0.1.1)] - 2024-12-14
|
||||
|
||||
### Other
|
||||
|
||||
- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a))
|
||||
27
crates/ironrdp-ainput/Cargo.toml
Normal file
27
crates/ironrdp-ainput/Cargo.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "ironrdp-ainput"
|
||||
version = "0.4.0"
|
||||
readme = "README.md"
|
||||
description = "AInput dynamic channel implementation"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public
|
||||
ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.4" } # public
|
||||
bitflags = "2.9"
|
||||
num-derive.workspace = true # TODO: remove
|
||||
num-traits.workspace = true # TODO: remove
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
1
crates/ironrdp-ainput/LICENSE-APACHE
Symbolic link
1
crates/ironrdp-ainput/LICENSE-APACHE
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-APACHE
|
||||
1
crates/ironrdp-ainput/LICENSE-MIT
Symbolic link
1
crates/ironrdp-ainput/LICENSE-MIT
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-MIT
|
||||
8
crates/ironrdp-ainput/README.md
Normal file
8
crates/ironrdp-ainput/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# IronRDP AInput
|
||||
|
||||
Implements the "Advanced Input" dynamic channel as defined from [Freerdp][here].
|
||||
|
||||
This crate is part of the [IronRDP] project.
|
||||
|
||||
[here]: https://github.com/FreeRDP/FreeRDP/blob/master/include/freerdp/channels/ainput.h
|
||||
[IronRDP]: https://github.com/Devolutions/IronRDP
|
||||
290
crates/ironrdp-ainput/src/lib.rs
Normal file
290
crates/ironrdp-ainput/src/lib.rs
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
|
||||
|
||||
use bitflags::bitflags;
|
||||
use ironrdp_core::{
|
||||
ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor,
|
||||
};
|
||||
use ironrdp_dvc::DvcEncode;
|
||||
use num_derive::FromPrimitive;
|
||||
use num_traits::FromPrimitive as _;
|
||||
// Advanced Input channel as defined from Freerdp, [here]:
|
||||
//
|
||||
// [here]: https://github.com/FreeRDP/FreeRDP/blob/master/include/freerdp/channels/ainput.h
|
||||
|
||||
const VERSION_MAJOR: u32 = 1;
|
||||
const VERSION_MINOR: u32 = 0;
|
||||
|
||||
pub const CHANNEL_NAME: &str = "FreeRDP::Advanced::Input";
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct MouseEventFlags: u64 {
|
||||
const WHEEL = 0x0000_0001;
|
||||
const MOVE = 0x0000_0004;
|
||||
const DOWN = 0x0000_0008;
|
||||
|
||||
const REL = 0x0000_0010;
|
||||
const HAVE_REL = 0x0000_0020;
|
||||
const BUTTON1 = 0x0000_1000; /* left */
|
||||
const BUTTON2 = 0x0000_2000; /* right */
|
||||
const BUTTON3 = 0x0000_4000; /* middle */
|
||||
|
||||
const XBUTTON1 = 0x0000_0100;
|
||||
const XBUTTON2 = 0x0000_0200;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct VersionPdu {
|
||||
major_version: u32,
|
||||
minor_version: u32,
|
||||
}
|
||||
|
||||
impl VersionPdu {
|
||||
const NAME: &'static str = "AInputVersionPdu";
|
||||
|
||||
const FIXED_PART_SIZE: usize = 4 /* MajorVersion */ + 4 /* MinorVersion */;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
major_version: VERSION_MAJOR,
|
||||
minor_version: VERSION_MINOR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VersionPdu {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for VersionPdu {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
|
||||
ensure_fixed_part_size!(in: dst);
|
||||
|
||||
dst.write_u32(self.major_version);
|
||||
dst.write_u32(self.minor_version);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
Self::FIXED_PART_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Decode<'de> for VersionPdu {
|
||||
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
|
||||
ensure_fixed_part_size!(in: src);
|
||||
|
||||
let major_version = src.read_u32();
|
||||
let minor_version = src.read_u32();
|
||||
|
||||
Ok(Self {
|
||||
major_version,
|
||||
minor_version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)]
|
||||
#[repr(u16)]
|
||||
pub enum ServerPduType {
|
||||
Version = 0x01,
|
||||
}
|
||||
|
||||
impl ServerPduType {
|
||||
#[expect(
|
||||
clippy::as_conversions,
|
||||
reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
|
||||
)]
|
||||
fn as_u16(&self) -> u16 {
|
||||
*self as u16
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ServerPdu> for ServerPduType {
|
||||
fn from(s: &'a ServerPdu) -> Self {
|
||||
match s {
|
||||
ServerPdu::Version(_) => Self::Version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ServerPdu {
|
||||
Version(VersionPdu),
|
||||
}
|
||||
|
||||
impl ServerPdu {
|
||||
const NAME: &'static str = "AInputServerPdu";
|
||||
|
||||
const FIXED_PART_SIZE: usize = 2 /* PduType */;
|
||||
}
|
||||
|
||||
impl Encode for ServerPdu {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
|
||||
ensure_fixed_part_size!(in: dst);
|
||||
|
||||
dst.write_u16(ServerPduType::from(self).as_u16());
|
||||
match self {
|
||||
ServerPdu::Version(pdu) => pdu.encode(dst),
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
Self::FIXED_PART_SIZE
|
||||
.checked_add(match self {
|
||||
ServerPdu::Version(pdu) => pdu.size(),
|
||||
})
|
||||
.expect("never overflow")
|
||||
}
|
||||
}
|
||||
|
||||
impl DvcEncode for ServerPdu {}
|
||||
|
||||
impl<'de> Decode<'de> for ServerPdu {
|
||||
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
|
||||
ensure_fixed_part_size!(in: src);
|
||||
|
||||
let pdu_type =
|
||||
ServerPduType::from_u16(src.read_u16()).ok_or_else(|| invalid_field_err!("pduType", "invalid pdu type"))?;
|
||||
|
||||
let server_pdu = match pdu_type {
|
||||
ServerPduType::Version => ServerPdu::Version(VersionPdu::decode(src)?),
|
||||
};
|
||||
|
||||
Ok(server_pdu)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MousePdu {
|
||||
pub time: u64,
|
||||
pub flags: MouseEventFlags,
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl MousePdu {
|
||||
const NAME: &'static str = "AInputMousePdu";
|
||||
|
||||
const FIXED_PART_SIZE: usize = 8 /* Time */ + 8 /* Flags */ + 4 /* X */ + 4 /* Y */;
|
||||
}
|
||||
|
||||
impl Encode for MousePdu {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
|
||||
ensure_fixed_part_size!(in: dst);
|
||||
|
||||
dst.write_u64(self.time);
|
||||
dst.write_u64(self.flags.bits());
|
||||
dst.write_i32(self.x);
|
||||
dst.write_i32(self.y);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
Self::FIXED_PART_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Decode<'de> for MousePdu {
|
||||
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
|
||||
ensure_fixed_part_size!(in: src);
|
||||
|
||||
let time = src.read_u64();
|
||||
let flags = MouseEventFlags::from_bits_retain(src.read_u64());
|
||||
let x = src.read_i32();
|
||||
let y = src.read_i32();
|
||||
|
||||
Ok(Self { time, flags, x, y })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ClientPdu {
|
||||
Mouse(MousePdu),
|
||||
}
|
||||
|
||||
impl ClientPdu {
|
||||
const NAME: &'static str = "AInputClientPdu";
|
||||
|
||||
const FIXED_PART_SIZE: usize = 2 /* PduType */;
|
||||
}
|
||||
|
||||
impl Encode for ClientPdu {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
|
||||
ensure_fixed_part_size!(in: dst);
|
||||
|
||||
dst.write_u16(ClientPduType::from(self).as_u16());
|
||||
match self {
|
||||
ClientPdu::Mouse(pdu) => pdu.encode(dst),
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
Self::FIXED_PART_SIZE
|
||||
.checked_add(match self {
|
||||
ClientPdu::Mouse(pdu) => pdu.size(),
|
||||
})
|
||||
.expect("never overflow")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Decode<'de> for ClientPdu {
|
||||
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
|
||||
ensure_fixed_part_size!(in: src);
|
||||
|
||||
let pdu_type =
|
||||
ClientPduType::from_u16(src.read_u16()).ok_or_else(|| invalid_field_err!("pduType", "invalid pdu type"))?;
|
||||
|
||||
let client_pdu = match pdu_type {
|
||||
ClientPduType::Mouse => ClientPdu::Mouse(MousePdu::decode(src)?),
|
||||
};
|
||||
|
||||
Ok(client_pdu)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)]
|
||||
#[repr(u16)]
|
||||
pub enum ClientPduType {
|
||||
Mouse = 0x02,
|
||||
}
|
||||
|
||||
impl ClientPduType {
|
||||
#[expect(
|
||||
clippy::as_conversions,
|
||||
reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
|
||||
)]
|
||||
fn as_u16(self) -> u16 {
|
||||
self as u16
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ClientPdu> for ClientPduType {
|
||||
fn from(s: &'a ClientPdu) -> Self {
|
||||
match s {
|
||||
ClientPdu::Mouse(_) => Self::Mouse,
|
||||
}
|
||||
}
|
||||
}
|
||||
48
crates/ironrdp-async/CHANGELOG.md
Normal file
48
crates/ironrdp-async/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.7.0...ironrdp-async-v0.8.0)] - 2025-12-18
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- [**breaking**] Use static dispatch for NetworkClient trait ([#1043](https://github.com/Devolutions/IronRDP/issues/1043)) ([bca6d190a8](https://github.com/Devolutions/IronRDP/commit/bca6d190a870708468534d224ff225a658767a9a))
|
||||
|
||||
- Rename `AsyncNetworkClient` to `NetworkClient`
|
||||
- Replace dynamic dispatch (`Option<&mut dyn ...>`) with static dispatch
|
||||
using generics (`&mut N where N: NetworkClient`)
|
||||
- Reorder `connect_finalize` parameters for consistency across crates
|
||||
|
||||
## [[0.3.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.3.1...ironrdp-async-v0.3.2)] - 2025-03-12
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump ironrdp-pdu
|
||||
|
||||
## [[0.3.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.3.0...ironrdp-async-v0.3.1)] - 2025-03-12
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa))
|
||||
|
||||
## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.2.1...ironrdp-async-v0.3.0)] - 2025-01-28
|
||||
|
||||
### <!-- 4 -->Changed
|
||||
|
||||
- Remove unmatched parameter from `Framed::read_by_hint` function ([63963182b5](https://github.com/Devolutions/IronRDP/commit/63963182b5af6ad45dc638e93de4b8a0b565c7d3))
|
||||
|
||||
### <!-- 6 -->Documentation
|
||||
|
||||
- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b))
|
||||
|
||||
|
||||
|
||||
## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.2.0...ironrdp-async-v0.2.1)] - 2024-12-14
|
||||
|
||||
### Other
|
||||
|
||||
- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a))
|
||||
26
crates/ironrdp-async/Cargo.toml
Normal file
26
crates/ironrdp-async/Cargo.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "ironrdp-async"
|
||||
version = "0.8.0"
|
||||
readme = "README.md"
|
||||
description = "Provides `Future`s wrapping the IronRDP state machines conveniently"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
ironrdp-connector = { path = "../ironrdp-connector", version = "0.8" } # public
|
||||
ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["alloc"] } # public
|
||||
ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6" } # public
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
bytes = "1" # public
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
1
crates/ironrdp-async/LICENSE-APACHE
Symbolic link
1
crates/ironrdp-async/LICENSE-APACHE
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-APACHE
|
||||
1
crates/ironrdp-async/LICENSE-MIT
Symbolic link
1
crates/ironrdp-async/LICENSE-MIT
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-MIT
|
||||
7
crates/ironrdp-async/README.md
Normal file
7
crates/ironrdp-async/README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# IronRDP Async
|
||||
|
||||
`Future`s built on top of `ironrdp-connector` and `ironrdp-session` crates.
|
||||
|
||||
This crate is part of the [IronRDP] project.
|
||||
|
||||
[IronRDP]: https://github.com/Devolutions/IronRDP
|
||||
189
crates/ironrdp-async/src/connector.rs
Normal file
189
crates/ironrdp-async/src/connector.rs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
use ironrdp_connector::credssp::{CredsspProcessGenerator, CredsspSequence, KerberosConfig};
|
||||
use ironrdp_connector::sspi::credssp::ClientState;
|
||||
use ironrdp_connector::sspi::generator::GeneratorState;
|
||||
use ironrdp_connector::{
|
||||
general_err, ClientConnector, ClientConnectorState, ConnectionResult, ConnectorError, ConnectorResult, ServerName,
|
||||
State as _,
|
||||
};
|
||||
use ironrdp_core::WriteBuf;
|
||||
use tracing::{debug, info, instrument, trace};
|
||||
|
||||
use crate::framed::{Framed, FramedRead, FramedWrite};
|
||||
use crate::{single_sequence_step, NetworkClient};
|
||||
|
||||
#[non_exhaustive]
|
||||
pub struct ShouldUpgrade;
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn connect_begin<S>(framed: &mut Framed<S>, connector: &mut ClientConnector) -> ConnectorResult<ShouldUpgrade>
|
||||
where
|
||||
S: Sync + FramedRead + FramedWrite,
|
||||
{
|
||||
let mut buf = WriteBuf::new();
|
||||
|
||||
info!("Begin connection procedure");
|
||||
|
||||
while !connector.should_perform_security_upgrade() {
|
||||
single_sequence_step(framed, connector, &mut buf).await?;
|
||||
}
|
||||
|
||||
Ok(ShouldUpgrade)
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if connector state is not [ClientConnectorState::EnhancedSecurityUpgrade].
|
||||
pub fn skip_connect_begin(connector: &mut ClientConnector) -> ShouldUpgrade {
|
||||
assert!(connector.should_perform_security_upgrade());
|
||||
ShouldUpgrade
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
pub struct Upgraded;
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub fn mark_as_upgraded(_: ShouldUpgrade, connector: &mut ClientConnector) -> Upgraded {
|
||||
trace!("Marked as upgraded");
|
||||
connector.mark_security_upgrade_as_done();
|
||||
Upgraded
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn connect_finalize<S, N>(
|
||||
_: Upgraded,
|
||||
mut connector: ClientConnector,
|
||||
framed: &mut Framed<S>,
|
||||
network_client: &mut N,
|
||||
server_name: ServerName,
|
||||
server_public_key: Vec<u8>,
|
||||
kerberos_config: Option<KerberosConfig>,
|
||||
) -> ConnectorResult<ConnectionResult>
|
||||
where
|
||||
S: FramedRead + FramedWrite,
|
||||
N: NetworkClient,
|
||||
{
|
||||
let mut buf = WriteBuf::new();
|
||||
|
||||
if connector.should_perform_credssp() {
|
||||
perform_credssp_step(
|
||||
&mut connector,
|
||||
framed,
|
||||
network_client,
|
||||
&mut buf,
|
||||
server_name,
|
||||
server_public_key,
|
||||
kerberos_config,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let result = loop {
|
||||
single_sequence_step(framed, &mut connector, &mut buf).await?;
|
||||
|
||||
if let ClientConnectorState::Connected { result } = connector.state {
|
||||
break result;
|
||||
}
|
||||
};
|
||||
|
||||
info!("Connected with success");
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn resolve_generator(
|
||||
generator: &mut CredsspProcessGenerator<'_>,
|
||||
network_client: &mut impl NetworkClient,
|
||||
) -> ConnectorResult<ClientState> {
|
||||
let mut state = generator.start();
|
||||
|
||||
loop {
|
||||
match state {
|
||||
GeneratorState::Suspended(request) => {
|
||||
let response = network_client.send(&request).await?;
|
||||
state = generator.resume(Ok(response));
|
||||
}
|
||||
GeneratorState::Completed(client_state) => {
|
||||
break client_state
|
||||
.map_err(|e| ConnectorError::new("CredSSP", ironrdp_connector::ConnectorErrorKind::Credssp(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
async fn perform_credssp_step<S, N>(
|
||||
connector: &mut ClientConnector,
|
||||
framed: &mut Framed<S>,
|
||||
network_client: &mut N,
|
||||
buf: &mut WriteBuf,
|
||||
server_name: ServerName,
|
||||
server_public_key: Vec<u8>,
|
||||
kerberos_config: Option<KerberosConfig>,
|
||||
) -> ConnectorResult<()>
|
||||
where
|
||||
S: FramedRead + FramedWrite,
|
||||
N: NetworkClient,
|
||||
{
|
||||
assert!(connector.should_perform_credssp());
|
||||
|
||||
let selected_protocol = match connector.state {
|
||||
ClientConnectorState::Credssp { selected_protocol, .. } => selected_protocol,
|
||||
_ => return Err(general_err!("invalid connector state for CredSSP sequence")),
|
||||
};
|
||||
|
||||
let (mut sequence, mut ts_request) = CredsspSequence::init(
|
||||
connector.config.credentials.clone(),
|
||||
connector.config.domain.as_deref(),
|
||||
selected_protocol,
|
||||
server_name,
|
||||
server_public_key,
|
||||
kerberos_config,
|
||||
)?;
|
||||
|
||||
loop {
|
||||
let client_state = {
|
||||
let mut generator = sequence.process_ts_request(ts_request);
|
||||
trace!("resolving network");
|
||||
resolve_generator(&mut generator, network_client).await?
|
||||
}; // drop generator
|
||||
|
||||
buf.clear();
|
||||
let written = sequence.handle_process_result(client_state, buf)?;
|
||||
|
||||
if let Some(response_len) = written.size() {
|
||||
let response = &buf[..response_len];
|
||||
trace!(response_len, "Send response");
|
||||
framed
|
||||
.write_all(response)
|
||||
.await
|
||||
.map_err(|e| ironrdp_connector::custom_err!("write all", e))?;
|
||||
}
|
||||
|
||||
let Some(next_pdu_hint) = sequence.next_pdu_hint() else {
|
||||
break;
|
||||
};
|
||||
|
||||
debug!(
|
||||
connector.state = connector.state.name(),
|
||||
hint = ?next_pdu_hint,
|
||||
"Wait for PDU"
|
||||
);
|
||||
|
||||
let pdu = framed
|
||||
.read_by_hint(next_pdu_hint)
|
||||
.await
|
||||
.map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?;
|
||||
|
||||
trace!(length = pdu.len(), "PDU received");
|
||||
|
||||
if let Some(next_request) = sequence.decode_server_message(&pdu)? {
|
||||
ts_request = next_request;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
connector.mark_credssp_as_done();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
290
crates/ironrdp-async/src/framed.rs
Normal file
290
crates/ironrdp-async/src/framed.rs
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
use std::io;
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use ironrdp_connector::{ConnectorResult, Sequence, Written};
|
||||
use ironrdp_core::WriteBuf;
|
||||
use ironrdp_pdu::PduHint;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
// TODO: investigate if we could use static async fn / return position impl trait in traits when stabilized:
|
||||
// https://github.com/rust-lang/rust/issues/91611
|
||||
|
||||
pub trait FramedRead {
|
||||
type ReadFut<'read>: core::future::Future<Output = io::Result<usize>> + 'read
|
||||
where
|
||||
Self: 'read;
|
||||
|
||||
/// Reads from stream and fills internal buffer
|
||||
///
|
||||
/// # Cancel safety
|
||||
///
|
||||
/// This method is cancel safe. If you use it as the event in a
|
||||
/// `tokio::select!` statement and some other branch
|
||||
/// completes first, then it is guaranteed that no data was read.
|
||||
fn read<'a>(&'a mut self, buf: &'a mut BytesMut) -> Self::ReadFut<'a>;
|
||||
}
|
||||
|
||||
pub trait FramedWrite {
|
||||
type WriteAllFut<'write>: core::future::Future<Output = io::Result<()>> + 'write
|
||||
where
|
||||
Self: 'write;
|
||||
|
||||
/// Writes an entire buffer into this stream.
|
||||
///
|
||||
/// # Cancel safety
|
||||
///
|
||||
/// This method is not cancellation safe. If it is used as the event
|
||||
/// in a `tokio::select!` statement and some other
|
||||
/// branch completes first, then the provided buffer may have been
|
||||
/// partially written, but future calls to `write_all` will start over
|
||||
/// from the beginning of the buffer.
|
||||
fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteAllFut<'a>;
|
||||
}
|
||||
|
||||
pub trait StreamWrapper: Sized {
|
||||
type InnerStream;
|
||||
|
||||
fn from_inner(stream: Self::InnerStream) -> Self;
|
||||
|
||||
fn into_inner(self) -> Self::InnerStream;
|
||||
|
||||
fn get_inner(&self) -> &Self::InnerStream;
|
||||
|
||||
fn get_inner_mut(&mut self) -> &mut Self::InnerStream;
|
||||
}
|
||||
|
||||
pub struct Framed<S> {
|
||||
stream: S,
|
||||
buf: BytesMut,
|
||||
}
|
||||
|
||||
impl<S> Framed<S> {
|
||||
pub fn peek(&self) -> &[u8] {
|
||||
&self.buf
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Framed<S>
|
||||
where
|
||||
S: StreamWrapper,
|
||||
{
|
||||
pub fn new(stream: S::InnerStream) -> Self {
|
||||
Self::new_with_leftover(stream, BytesMut::new())
|
||||
}
|
||||
|
||||
pub fn new_with_leftover(stream: S::InnerStream, leftover: BytesMut) -> Self {
|
||||
Self {
|
||||
stream: S::from_inner(stream),
|
||||
buf: leftover,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> (S::InnerStream, BytesMut) {
|
||||
(self.stream.into_inner(), self.buf)
|
||||
}
|
||||
|
||||
pub fn into_inner_no_leftover(self) -> S::InnerStream {
|
||||
let (stream, leftover) = self.into_inner();
|
||||
debug_assert_eq!(leftover.len(), 0, "unexpected leftover");
|
||||
stream
|
||||
}
|
||||
|
||||
pub fn get_inner(&self) -> (&S::InnerStream, &BytesMut) {
|
||||
(self.stream.get_inner(), &self.buf)
|
||||
}
|
||||
|
||||
pub fn get_inner_mut(&mut self) -> (&mut S::InnerStream, &mut BytesMut) {
|
||||
(self.stream.get_inner_mut(), &mut self.buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Framed<S>
|
||||
where
|
||||
S: FramedRead,
|
||||
{
|
||||
/// Accumulates at least `length` bytes and returns exactly `length` bytes, keeping the leftover in the internal buffer.
|
||||
///
|
||||
/// # Cancel safety
|
||||
///
|
||||
/// This method is cancel safe. If you use it as the event in a
|
||||
/// `tokio::select!` statement and some other branch
|
||||
/// completes first, then it is safe to drop the future and re-create it later.
|
||||
/// Data may have been read, but it will be stored in the internal buffer.
|
||||
pub async fn read_exact(&mut self, length: usize) -> io::Result<BytesMut> {
|
||||
loop {
|
||||
if self.buf.len() >= length {
|
||||
return Ok(self.buf.split_to(length));
|
||||
} else {
|
||||
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer underflow)")]
|
||||
self.buf
|
||||
.reserve(length.checked_sub(self.buf.len()).expect("length > self.buf.len()"));
|
||||
}
|
||||
|
||||
let len = self.read().await?;
|
||||
|
||||
// Handle EOF
|
||||
if len == 0 {
|
||||
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a standard RDP PDU frame.
|
||||
///
|
||||
/// # Cancel safety
|
||||
///
|
||||
/// This method is cancel safe. If you use it as the event in a
|
||||
/// `tokio::select!` statement and some other branch
|
||||
/// completes first, then it is safe to drop the future and re-create it later.
|
||||
/// Data may have been read, but it will be stored in the internal buffer.
|
||||
pub async fn read_pdu(&mut self) -> io::Result<(ironrdp_pdu::Action, BytesMut)> {
|
||||
loop {
|
||||
// Try decoding and see if a frame has been received already
|
||||
match ironrdp_pdu::find_size(self.peek()) {
|
||||
Ok(Some(pdu_info)) => {
|
||||
let frame = self.read_exact(pdu_info.length).await?;
|
||||
|
||||
return Ok((pdu_info.action, frame));
|
||||
}
|
||||
Ok(None) => {
|
||||
let len = self.read().await?;
|
||||
|
||||
// Handle EOF
|
||||
if len == 0 {
|
||||
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes"));
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(io::Error::other(e)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a frame using the provided PduHint.
|
||||
///
|
||||
/// # Cancel safety
|
||||
///
|
||||
/// This method is cancel safe. If you use it as the event in a
|
||||
/// `tokio::select!` statement and some other branch
|
||||
/// completes first, then it is safe to drop the future and re-create it later.
|
||||
/// Data may have been read, but it will be stored in the internal buffer.
|
||||
pub async fn read_by_hint(&mut self, hint: &dyn PduHint) -> io::Result<Bytes> {
|
||||
loop {
|
||||
match hint.find_size(self.peek()).map_err(io::Error::other)? {
|
||||
Some((matched, length)) => {
|
||||
let bytes = self.read_exact(length).await?.freeze();
|
||||
if matched {
|
||||
return Ok(bytes);
|
||||
} else {
|
||||
debug!("Received and lost an unexpected PDU");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let len = self.read().await?;
|
||||
|
||||
// Handle EOF
|
||||
if len == 0 {
|
||||
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes"));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads from stream and fills internal buffer, returning how many bytes were read.
|
||||
///
|
||||
/// # Cancel safety
|
||||
///
|
||||
/// This method is cancel safe. If you use it as the event in a
|
||||
/// `tokio::select!` statement and some other branch
|
||||
/// completes first, then it is guaranteed that no data was read.
|
||||
async fn read(&mut self) -> io::Result<usize> {
|
||||
self.stream.read(&mut self.buf).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> FramedWrite for Framed<S>
|
||||
where
|
||||
S: FramedWrite,
|
||||
{
|
||||
type WriteAllFut<'write>
|
||||
= S::WriteAllFut<'write>
|
||||
where
|
||||
Self: 'write;
|
||||
|
||||
/// Attempts to write an entire buffer into this `Framed`’s stream.
|
||||
///
|
||||
/// # Cancel safety
|
||||
///
|
||||
/// This method is not cancellation safe. If it is used as the event
|
||||
/// in a `tokio::select!` statement and some other
|
||||
/// branch completes first, then the provided buffer may have been
|
||||
/// partially written, but future calls to `write_all` will start over
|
||||
/// from the beginning of the buffer.
|
||||
fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteAllFut<'a> {
|
||||
self.stream.write_all(buf)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn single_sequence_step<S>(
|
||||
framed: &mut Framed<S>,
|
||||
sequence: &mut dyn Sequence,
|
||||
buf: &mut WriteBuf,
|
||||
) -> ConnectorResult<()>
|
||||
where
|
||||
S: FramedWrite + FramedRead,
|
||||
{
|
||||
buf.clear();
|
||||
let written = single_sequence_step_read(framed, sequence, buf).await?;
|
||||
single_sequence_step_write(framed, buf, written).await
|
||||
}
|
||||
|
||||
pub async fn single_sequence_step_read<S>(
|
||||
framed: &mut Framed<S>,
|
||||
sequence: &mut dyn Sequence,
|
||||
buf: &mut WriteBuf,
|
||||
) -> ConnectorResult<Written>
|
||||
where
|
||||
S: FramedRead,
|
||||
{
|
||||
buf.clear();
|
||||
|
||||
if let Some(next_pdu_hint) = sequence.next_pdu_hint() {
|
||||
debug!(
|
||||
connector.state = sequence.state().name(),
|
||||
hint = ?next_pdu_hint,
|
||||
"Wait for PDU"
|
||||
);
|
||||
|
||||
let pdu = framed
|
||||
.read_by_hint(next_pdu_hint)
|
||||
.await
|
||||
.map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?;
|
||||
|
||||
trace!(length = pdu.len(), "PDU received");
|
||||
|
||||
sequence.step(&pdu, buf)
|
||||
} else {
|
||||
sequence.step_no_input(buf)
|
||||
}
|
||||
}
|
||||
|
||||
async fn single_sequence_step_write<S>(
|
||||
framed: &mut Framed<S>,
|
||||
buf: &mut WriteBuf,
|
||||
written: Written,
|
||||
) -> ConnectorResult<()>
|
||||
where
|
||||
S: FramedWrite,
|
||||
{
|
||||
if let Some(response_len) = written.size() {
|
||||
debug_assert_eq!(buf.filled_len(), response_len);
|
||||
let response = buf.filled();
|
||||
trace!(response_len, "Send response");
|
||||
framed
|
||||
.write_all(response)
|
||||
.await
|
||||
.map_err(|e| ironrdp_connector::custom_err!("write all", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
21
crates/ironrdp-async/src/lib.rs
Normal file
21
crates/ironrdp-async/src/lib.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
|
||||
|
||||
use core::future::Future;
|
||||
|
||||
pub use bytes;
|
||||
|
||||
mod connector;
|
||||
mod framed;
|
||||
mod session;
|
||||
|
||||
use ironrdp_connector::sspi::generator::NetworkRequest;
|
||||
use ironrdp_connector::ConnectorResult;
|
||||
|
||||
pub use self::connector::*;
|
||||
pub use self::framed::*;
|
||||
// pub use self::session::*;
|
||||
|
||||
pub trait NetworkClient {
|
||||
fn send(&mut self, network_request: &NetworkRequest) -> impl Future<Output = ConnectorResult<Vec<u8>>>;
|
||||
}
|
||||
1
crates/ironrdp-async/src/session.rs
Normal file
1
crates/ironrdp-async/src/session.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
// TODO: active session async helpers
|
||||
20
crates/ironrdp-bench/Cargo.toml
Normal file
20
crates/ironrdp-bench/Cargo.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "ironrdp-bench"
|
||||
version = "0.0.0"
|
||||
description = "IronRDP benchmarks"
|
||||
edition.workspace = true
|
||||
publish = false
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.8"
|
||||
ironrdp-graphics.path = "../ironrdp-graphics"
|
||||
ironrdp-pdu.path = "../ironrdp-pdu"
|
||||
ironrdp-server = { path = "../ironrdp-server", features = ["__bench"] }
|
||||
|
||||
[[bench]]
|
||||
name = "bench"
|
||||
path = "benches/bench.rs"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
80
crates/ironrdp-bench/benches/bench.rs
Normal file
80
crates/ironrdp-bench/benches/bench.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#![expect(clippy::missing_panics_doc, reason = "panics in benches are allowed")]
|
||||
|
||||
use core::num::{NonZeroU16, NonZeroUsize};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use ironrdp_graphics::color_conversion::to_64x64_ycbcr_tile;
|
||||
use ironrdp_pdu::codecs::rfx;
|
||||
use ironrdp_server::bench::encoder::rfx::{rfx_enc, rfx_enc_tile};
|
||||
use ironrdp_server::BitmapUpdate;
|
||||
|
||||
pub fn rfx_enc_tile_bench(c: &mut Criterion) {
|
||||
const WIDTH: NonZeroU16 = NonZeroU16::new(64).expect("value is guaranteed to be non-zero");
|
||||
const HEIGHT: NonZeroU16 = NonZeroU16::new(64).expect("value is guaranteed to be non-zero");
|
||||
const STRIDE: NonZeroUsize = NonZeroUsize::new(64 * 4).expect("value is guaranteed to be non-zero");
|
||||
|
||||
let quant = rfx::Quant::default();
|
||||
let algo = rfx::EntropyAlgorithm::Rlgr3;
|
||||
|
||||
let bitmap = BitmapUpdate {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
format: ironrdp_server::PixelFormat::ARgb32,
|
||||
data: vec![0; 64 * 64 * 4].into(),
|
||||
stride: STRIDE,
|
||||
};
|
||||
c.bench_function("rfx_enc_tile", |b| b.iter(|| rfx_enc_tile(&bitmap, &quant, algo, 0, 0)));
|
||||
}
|
||||
|
||||
pub fn rfx_enc_bench(c: &mut Criterion) {
|
||||
const WIDTH: NonZeroU16 = NonZeroU16::new(2048).expect("value is guaranteed to be non-zero");
|
||||
const HEIGHT: NonZeroU16 = NonZeroU16::new(2048).expect("value is guaranteed to be non-zero");
|
||||
// FIXME/QUESTION: It looks like we have a bug here, don't we? The stride value should be 2048 * 4.
|
||||
const STRIDE: NonZeroUsize = NonZeroUsize::new(64 * 4).expect("value is guaranteed to be non-zero");
|
||||
|
||||
let quant = rfx::Quant::default();
|
||||
let algo = rfx::EntropyAlgorithm::Rlgr3;
|
||||
|
||||
let bitmap = BitmapUpdate {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
format: ironrdp_server::PixelFormat::ARgb32,
|
||||
data: vec![0; 2048 * 2048 * 4].into(),
|
||||
stride: STRIDE,
|
||||
};
|
||||
c.bench_function("rfx_enc", |b| b.iter(|| rfx_enc(&bitmap, &quant, algo)));
|
||||
}
|
||||
|
||||
pub fn to_ycbcr_bench(c: &mut Criterion) {
|
||||
const WIDTH: usize = 64;
|
||||
const HEIGHT: usize = 64;
|
||||
|
||||
let input = vec![0; WIDTH * HEIGHT * 4];
|
||||
let stride = WIDTH * 4;
|
||||
let mut y = [0i16; WIDTH * HEIGHT];
|
||||
let mut cb = [0i16; WIDTH * HEIGHT];
|
||||
let mut cr = [0i16; WIDTH * HEIGHT];
|
||||
let format = ironrdp_graphics::image_processing::PixelFormat::ARgb32;
|
||||
|
||||
c.bench_function("to_ycbcr", |b| {
|
||||
b.iter(|| {
|
||||
to_64x64_ycbcr_tile(
|
||||
&input,
|
||||
WIDTH.try_into().expect("can't panic"),
|
||||
HEIGHT.try_into().expect("can't panic"),
|
||||
stride.try_into().expect("can't panic"),
|
||||
format,
|
||||
&mut y,
|
||||
&mut cb,
|
||||
&mut cr,
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, rfx_enc_tile_bench, rfx_enc_bench, to_ycbcr_bench);
|
||||
criterion_main!(benches);
|
||||
48
crates/ironrdp-blocking/CHANGELOG.md
Normal file
48
crates/ironrdp-blocking/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-blocking-v0.7.0...ironrdp-blocking-v0.8.0)] - 2025-12-18
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- [**breaking**] Use static dispatch for NetworkClient trait ([#1043](https://github.com/Devolutions/IronRDP/issues/1043)) ([bca6d190a8](https://github.com/Devolutions/IronRDP/commit/bca6d190a870708468534d224ff225a658767a9a))
|
||||
|
||||
- Rename `AsyncNetworkClient` to `NetworkClient`
|
||||
- Replace dynamic dispatch (`Option<&mut dyn ...>`) with static dispatch
|
||||
using generics (`&mut N where N: NetworkClient`)
|
||||
- Reorder `connect_finalize` parameters for consistency across crates
|
||||
|
||||
## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-blocking-v0.3.1...ironrdp-blocking-v0.4.0)] - 2025-03-12
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump ironrdp-pdu
|
||||
|
||||
|
||||
## [[0.3.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-blocking-v0.3.0...ironrdp-blocking-v0.3.1)] - 2025-03-12
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa))
|
||||
|
||||
## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-blocking-v0.2.1...ironrdp-blocking-v0.3.0)] - 2025-01-28
|
||||
|
||||
### <!-- 4 -->Changed
|
||||
|
||||
- Remove unmatched parameter from `Framed::read_by_hint` function ([63963182b5](https://github.com/Devolutions/IronRDP/commit/63963182b5af6ad45dc638e93de4b8a0b565c7d3))
|
||||
|
||||
### <!-- 6 -->Documentation
|
||||
|
||||
- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b))
|
||||
|
||||
|
||||
## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-blocking-v0.2.0...ironrdp-blocking-v0.2.1)] - 2024-12-14
|
||||
|
||||
### Other
|
||||
|
||||
- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a))
|
||||
27
crates/ironrdp-blocking/Cargo.toml
Normal file
27
crates/ironrdp-blocking/Cargo.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "ironrdp-blocking"
|
||||
version = "0.8.0"
|
||||
readme = "README.md"
|
||||
description = "Blocking I/O abstraction wrapping the IronRDP state machines conveniently"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
ironrdp-connector = { path = "../ironrdp-connector", version = "0.8" } # public
|
||||
ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["alloc"] } # public
|
||||
ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.6" } # public
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
bytes = "1" # public
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
1
crates/ironrdp-blocking/LICENSE-APACHE
Symbolic link
1
crates/ironrdp-blocking/LICENSE-APACHE
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-APACHE
|
||||
1
crates/ironrdp-blocking/LICENSE-MIT
Symbolic link
1
crates/ironrdp-blocking/LICENSE-MIT
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-MIT
|
||||
11
crates/ironrdp-blocking/README.md
Normal file
11
crates/ironrdp-blocking/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# IronRDP Blocking
|
||||
|
||||
Blocking I/O abstraction wrapping the IronRDP state machines conveniently.
|
||||
|
||||
This crate is a higher level abstraction for IronRDP state machines using blocking I/O instead of
|
||||
asynchronous I/O. This results in a simpler API with fewer dependencies that may be used
|
||||
instead of `ironrdp-async` when concurrency is not a requirement.
|
||||
|
||||
This crate is part of the [IronRDP] project.
|
||||
|
||||
[IronRDP]: https://github.com/Devolutions/IronRDP
|
||||
230
crates/ironrdp-blocking/src/connector.rs
Normal file
230
crates/ironrdp-blocking/src/connector.rs
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
use std::io::{Read, Write};
|
||||
|
||||
use ironrdp_connector::credssp::{CredsspProcessGenerator, CredsspSequence, KerberosConfig};
|
||||
use ironrdp_connector::sspi::credssp::ClientState;
|
||||
use ironrdp_connector::sspi::generator::GeneratorState;
|
||||
use ironrdp_connector::sspi::network_client::NetworkClient;
|
||||
use ironrdp_connector::{
|
||||
general_err, ClientConnector, ClientConnectorState, ConnectionResult, ConnectorError, ConnectorResult,
|
||||
Sequence as _, ServerName, State as _,
|
||||
};
|
||||
use ironrdp_core::WriteBuf;
|
||||
use tracing::{debug, info, instrument, trace};
|
||||
|
||||
use crate::framed::Framed;
|
||||
|
||||
#[non_exhaustive]
|
||||
pub struct ShouldUpgrade;
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub fn connect_begin<S>(framed: &mut Framed<S>, connector: &mut ClientConnector) -> ConnectorResult<ShouldUpgrade>
|
||||
where
|
||||
S: Sync + Read + Write,
|
||||
{
|
||||
let mut buf = WriteBuf::new();
|
||||
|
||||
info!("Begin connection procedure");
|
||||
|
||||
while !connector.should_perform_security_upgrade() {
|
||||
single_sequence_step(framed, connector, &mut buf)?;
|
||||
}
|
||||
|
||||
Ok(ShouldUpgrade)
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if connector state is not [ClientConnectorState::EnhancedSecurityUpgrade].
|
||||
pub fn skip_connect_begin(connector: &mut ClientConnector) -> ShouldUpgrade {
|
||||
assert!(connector.should_perform_security_upgrade());
|
||||
ShouldUpgrade
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
pub struct Upgraded;
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub fn mark_as_upgraded(_: ShouldUpgrade, connector: &mut ClientConnector) -> Upgraded {
|
||||
trace!("Marked as upgraded");
|
||||
connector.mark_security_upgrade_as_done();
|
||||
Upgraded
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub fn connect_finalize<S>(
|
||||
_: Upgraded,
|
||||
mut connector: ClientConnector,
|
||||
framed: &mut Framed<S>,
|
||||
network_client: &mut impl NetworkClient,
|
||||
server_name: ServerName,
|
||||
server_public_key: Vec<u8>,
|
||||
kerberos_config: Option<KerberosConfig>,
|
||||
) -> ConnectorResult<ConnectionResult>
|
||||
where
|
||||
S: Read + Write,
|
||||
{
|
||||
let mut buf = WriteBuf::new();
|
||||
|
||||
debug!("CredSSP procedure");
|
||||
|
||||
if connector.should_perform_credssp() {
|
||||
perform_credssp_step(
|
||||
&mut connector,
|
||||
framed,
|
||||
network_client,
|
||||
&mut buf,
|
||||
server_name,
|
||||
server_public_key,
|
||||
kerberos_config,
|
||||
)?;
|
||||
}
|
||||
|
||||
debug!("Remaining of connection sequence");
|
||||
|
||||
let result = loop {
|
||||
single_sequence_step(framed, &mut connector, &mut buf)?;
|
||||
|
||||
if let ClientConnectorState::Connected { result } = connector.state {
|
||||
break result;
|
||||
}
|
||||
};
|
||||
|
||||
info!("Connected with success");
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn resolve_generator(
|
||||
generator: &mut CredsspProcessGenerator<'_>,
|
||||
network_client: &mut impl NetworkClient,
|
||||
) -> ConnectorResult<ClientState> {
|
||||
let mut state = generator.start();
|
||||
|
||||
loop {
|
||||
match state {
|
||||
GeneratorState::Suspended(request) => {
|
||||
let response = network_client.send(&request).map_err(|e| {
|
||||
ConnectorError::new("network client send", ironrdp_connector::ConnectorErrorKind::Credssp(e))
|
||||
})?;
|
||||
state = generator.resume(Ok(response));
|
||||
}
|
||||
GeneratorState::Completed(client_state) => {
|
||||
break client_state
|
||||
.map_err(|e| ConnectorError::new("CredSSP", ironrdp_connector::ConnectorErrorKind::Credssp(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
fn perform_credssp_step<S>(
|
||||
connector: &mut ClientConnector,
|
||||
framed: &mut Framed<S>,
|
||||
network_client: &mut impl NetworkClient,
|
||||
buf: &mut WriteBuf,
|
||||
server_name: ServerName,
|
||||
server_public_key: Vec<u8>,
|
||||
kerberos_config: Option<KerberosConfig>,
|
||||
) -> ConnectorResult<()>
|
||||
where
|
||||
S: Read + Write,
|
||||
{
|
||||
assert!(connector.should_perform_credssp());
|
||||
|
||||
let selected_protocol = match connector.state {
|
||||
ClientConnectorState::Credssp { selected_protocol, .. } => selected_protocol,
|
||||
_ => return Err(general_err!("invalid connector state for CredSSP sequence")),
|
||||
};
|
||||
|
||||
let (mut sequence, mut ts_request) = CredsspSequence::init(
|
||||
connector.config.credentials.clone(),
|
||||
connector.config.domain.as_deref(),
|
||||
selected_protocol,
|
||||
server_name,
|
||||
server_public_key,
|
||||
kerberos_config,
|
||||
)?;
|
||||
|
||||
loop {
|
||||
let client_state = {
|
||||
let mut generator = sequence.process_ts_request(ts_request);
|
||||
resolve_generator(&mut generator, network_client)?
|
||||
}; // drop generator
|
||||
|
||||
buf.clear();
|
||||
let written = sequence.handle_process_result(client_state, buf)?;
|
||||
|
||||
if let Some(response_len) = written.size() {
|
||||
let response = &buf[..response_len];
|
||||
trace!(response_len, "Send response");
|
||||
framed
|
||||
.write_all(response)
|
||||
.map_err(|e| ironrdp_connector::custom_err!("write all", e))?;
|
||||
}
|
||||
|
||||
let Some(next_pdu_hint) = sequence.next_pdu_hint() else {
|
||||
break;
|
||||
};
|
||||
|
||||
debug!(
|
||||
connector.state = connector.state.name(),
|
||||
hint = ?next_pdu_hint,
|
||||
"Wait for PDU"
|
||||
);
|
||||
|
||||
let pdu = framed
|
||||
.read_by_hint(next_pdu_hint)
|
||||
.map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?;
|
||||
|
||||
trace!(length = pdu.len(), "PDU received");
|
||||
|
||||
if let Some(next_request) = sequence.decode_server_message(&pdu)? {
|
||||
ts_request = next_request;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
connector.mark_credssp_as_done();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn single_sequence_step<S>(
|
||||
framed: &mut Framed<S>,
|
||||
connector: &mut ClientConnector,
|
||||
buf: &mut WriteBuf,
|
||||
) -> ConnectorResult<()>
|
||||
where
|
||||
S: Read + Write,
|
||||
{
|
||||
buf.clear();
|
||||
|
||||
let written = if let Some(next_pdu_hint) = connector.next_pdu_hint() {
|
||||
debug!(
|
||||
connector.state = connector.state.name(),
|
||||
hint = ?next_pdu_hint,
|
||||
"Wait for PDU"
|
||||
);
|
||||
|
||||
let pdu = framed
|
||||
.read_by_hint(next_pdu_hint)
|
||||
.map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?;
|
||||
|
||||
trace!(length = pdu.len(), "PDU received");
|
||||
|
||||
connector.step(&pdu, buf)?
|
||||
} else {
|
||||
connector.step_no_input(buf)?
|
||||
};
|
||||
|
||||
if let Some(response_len) = written.size() {
|
||||
let response = &buf[..response_len];
|
||||
trace!(response_len, "Send response");
|
||||
framed
|
||||
.write_all(response)
|
||||
.map_err(|e| ironrdp_connector::custom_err!("write all", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
136
crates/ironrdp-blocking/src/framed.rs
Normal file
136
crates/ironrdp-blocking/src/framed.rs
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
use std::io::{self, Read, Write};
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use ironrdp_pdu::PduHint;
|
||||
use tracing::debug;
|
||||
|
||||
pub struct Framed<S> {
|
||||
stream: S,
|
||||
buf: BytesMut,
|
||||
}
|
||||
|
||||
impl<S> Framed<S> {
|
||||
pub fn new(stream: S) -> Self {
|
||||
Self::new_with_leftover(stream, BytesMut::new())
|
||||
}
|
||||
|
||||
pub fn new_with_leftover(stream: S, leftover: BytesMut) -> Self {
|
||||
Self { stream, buf: leftover }
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> (S, BytesMut) {
|
||||
(self.stream, self.buf)
|
||||
}
|
||||
|
||||
pub fn into_inner_no_leftover(self) -> S {
|
||||
let (stream, leftover) = self.into_inner();
|
||||
debug_assert_eq!(leftover.len(), 0, "unexpected leftover");
|
||||
stream
|
||||
}
|
||||
|
||||
pub fn get_inner(&self) -> (&S, &BytesMut) {
|
||||
(&self.stream, &self.buf)
|
||||
}
|
||||
|
||||
pub fn get_inner_mut(&mut self) -> (&mut S, &mut BytesMut) {
|
||||
(&mut self.stream, &mut self.buf)
|
||||
}
|
||||
|
||||
pub fn peek(&self) -> &[u8] {
|
||||
&self.buf
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Framed<S>
|
||||
where
|
||||
S: Read,
|
||||
{
|
||||
/// Accumulates at least `length` bytes and returns exactly `length` bytes, keeping the leftover in the internal buffer.
|
||||
pub fn read_exact(&mut self, length: usize) -> io::Result<BytesMut> {
|
||||
loop {
|
||||
if self.buf.len() >= length {
|
||||
return Ok(self.buf.split_to(length));
|
||||
} else {
|
||||
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked underflow)")]
|
||||
self.buf
|
||||
.reserve(length.checked_sub(self.buf.len()).expect("length > self.buf.len()"));
|
||||
}
|
||||
|
||||
let len = self.read()?;
|
||||
|
||||
// Handle EOF
|
||||
if len == 0 {
|
||||
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a standard RDP PDU frame.
|
||||
pub fn read_pdu(&mut self) -> io::Result<(ironrdp_pdu::Action, BytesMut)> {
|
||||
loop {
|
||||
// Try decoding and see if a frame has been received already
|
||||
match ironrdp_pdu::find_size(self.peek()) {
|
||||
Ok(Some(pdu_info)) => {
|
||||
let frame = self.read_exact(pdu_info.length)?;
|
||||
|
||||
return Ok((pdu_info.action, frame));
|
||||
}
|
||||
Ok(None) => {
|
||||
let len = self.read()?;
|
||||
|
||||
// Handle EOF
|
||||
if len == 0 {
|
||||
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes"));
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(io::Error::other(e)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a frame using the provided PduHint.
|
||||
pub fn read_by_hint(&mut self, hint: &dyn PduHint) -> io::Result<Bytes> {
|
||||
loop {
|
||||
match hint.find_size(self.peek()).map_err(io::Error::other)? {
|
||||
Some((matched, length)) => {
|
||||
let bytes = self.read_exact(length)?.freeze();
|
||||
if matched {
|
||||
return Ok(bytes);
|
||||
} else {
|
||||
debug!("Received and lost an unexpected PDU");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let len = self.read()?;
|
||||
|
||||
// Handle EOF
|
||||
if len == 0 {
|
||||
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes"));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads from stream and fills internal buffer, returning how many bytes were read.
|
||||
fn read(&mut self) -> io::Result<usize> {
|
||||
// FIXME(perf): use read_buf (https://doc.rust-lang.org/std/io/trait.Read.html#method.read_buf)
|
||||
// once its stabilized. See tracking issue for RFC 2930: https://github.com/rust-lang/rust/issues/78485
|
||||
|
||||
let mut read_bytes = [0u8; 1024];
|
||||
let len = self.stream.read(&mut read_bytes)?;
|
||||
self.buf.extend_from_slice(&read_bytes[..len]);
|
||||
|
||||
Ok(len)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Framed<S>
|
||||
where
|
||||
S: Write,
|
||||
{
|
||||
/// Attempts to write an entire buffer into this `Framed`’s stream.
|
||||
pub fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
|
||||
self.stream.write_all(buf)
|
||||
}
|
||||
}
|
||||
9
crates/ironrdp-blocking/src/lib.rs
Normal file
9
crates/ironrdp-blocking/src/lib.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
|
||||
|
||||
mod connector;
|
||||
mod framed;
|
||||
mod session;
|
||||
|
||||
pub use self::connector::*;
|
||||
pub use self::framed::*;
|
||||
1
crates/ironrdp-blocking/src/session.rs
Normal file
1
crates/ironrdp-blocking/src/session.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
// TODO: active session I/O helpers? I’m not yet sure we need that
|
||||
23
crates/ironrdp-cfg/Cargo.toml
Normal file
23
crates/ironrdp-cfg/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "ironrdp-cfg"
|
||||
version = "0.1.0"
|
||||
readme = "README.md"
|
||||
description = "IronRDP utilities for ironrdp-cfgstore"
|
||||
publish = false # TODO: publish
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" } # public
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
7
crates/ironrdp-cfg/README.md
Normal file
7
crates/ironrdp-cfg/README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# IronRDP Configuration
|
||||
|
||||
IronRDP-related utilities for ironrdp-propertyset.
|
||||
|
||||
This crate is part of the [IronRDP] project.
|
||||
|
||||
[IronRDP]: https://github.com/Devolutions/IronRDP
|
||||
63
crates/ironrdp-cfg/src/lib.rs
Normal file
63
crates/ironrdp-cfg/src/lib.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// QUESTION: consider auto-generating this file based on a reference file?
|
||||
// https://gist.github.com/awakecoding/838c7fe2ed3a6208e3ca5d8af25363f6
|
||||
|
||||
use ironrdp_propertyset::PropertySet;
|
||||
|
||||
pub trait PropertySetExt {
|
||||
fn full_address(&self) -> Option<&str>;
|
||||
|
||||
fn server_port(&self) -> Option<i64>;
|
||||
|
||||
fn alternate_full_address(&self) -> Option<&str>;
|
||||
|
||||
fn gateway_hostname(&self) -> Option<&str>;
|
||||
|
||||
fn remote_application_name(&self) -> Option<&str>;
|
||||
|
||||
fn remote_application_program(&self) -> Option<&str>;
|
||||
|
||||
fn kdc_proxy_url(&self) -> Option<&str>;
|
||||
|
||||
fn username(&self) -> Option<&str>;
|
||||
|
||||
/// Target RDP server password - use for testing only
|
||||
fn clear_text_password(&self) -> Option<&str>;
|
||||
}
|
||||
|
||||
impl PropertySetExt for PropertySet {
|
||||
fn full_address(&self) -> Option<&str> {
|
||||
self.get::<&str>("full address")
|
||||
}
|
||||
|
||||
fn server_port(&self) -> Option<i64> {
|
||||
self.get::<i64>("server port")
|
||||
}
|
||||
|
||||
fn alternate_full_address(&self) -> Option<&str> {
|
||||
self.get::<&str>("alternate full address")
|
||||
}
|
||||
|
||||
fn gateway_hostname(&self) -> Option<&str> {
|
||||
self.get::<&str>("gatewayhostname")
|
||||
}
|
||||
|
||||
fn remote_application_name(&self) -> Option<&str> {
|
||||
self.get::<&str>("remoteapplicationname")
|
||||
}
|
||||
|
||||
fn remote_application_program(&self) -> Option<&str> {
|
||||
self.get::<&str>("remoteapplicationprogram")
|
||||
}
|
||||
|
||||
fn kdc_proxy_url(&self) -> Option<&str> {
|
||||
self.get::<&str>("kdcproxyurl")
|
||||
}
|
||||
|
||||
fn username(&self) -> Option<&str> {
|
||||
self.get::<&str>("username")
|
||||
}
|
||||
|
||||
fn clear_text_password(&self) -> Option<&str> {
|
||||
self.get::<&str>("ClearTextPassword")
|
||||
}
|
||||
}
|
||||
49
crates/ironrdp-client-glutin/Cargo.toml
Normal file
49
crates/ironrdp-client-glutin/Cargo.toml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
[package]
|
||||
name = "ironrdp-client-glutin"
|
||||
version = "0.1.0"
|
||||
readme = "README.md"
|
||||
description = "GPU-accelerated RDP client using glutin"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["rustls"]
|
||||
rustls = ["ironrdp-tls/rustls"]
|
||||
native-tls = ["ironrdp-tls/native-tls"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
# Protocols
|
||||
ironrdp.workspace = true
|
||||
ironrdp-tls.workspace = true
|
||||
sspi = { workspace = true, features = ["network_client"] }
|
||||
|
||||
# CLI
|
||||
clap = { version = "4.2", features = ["derive", "cargo"] }
|
||||
exitcode = "1.1"
|
||||
|
||||
# logging
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# async, futures
|
||||
tokio = { version = "1", features = ["full"]}
|
||||
tokio-util = { version = "0.7", features = ["compat"] }
|
||||
futures-util = "0.3"
|
||||
|
||||
# Utils
|
||||
chrono = "0.4"
|
||||
anyhow = "1.0"
|
||||
|
||||
# GUI
|
||||
glutin = "0.29"
|
||||
ironrdp-glutin-renderer = { path = "../glutin-renderer"}
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
13
crates/ironrdp-client-glutin/README.md
Normal file
13
crates/ironrdp-client-glutin/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# GUI client
|
||||
|
||||
1. An experimental GUI based of glutin and glow library.
|
||||
2. Sample command to run the ui client:
|
||||
```
|
||||
cargo run --bin ironrdp-gui-client -- -u SimpleUsername -p SimplePassword! --avc444 --thin-client --small-cache --capabilities 0xf 192.168.1.100:3389
|
||||
```
|
||||
3. If the GUI has artifacts it can be dumped to a file using the gfx_dump_file parameter. Later the ironrdp-replay-client binary can be used to debug and fix any issues
|
||||
in the renderer.
|
||||
|
||||
This crate is part of the [IronRDP] project.
|
||||
|
||||
[IronRDP]: https://github.com/Devolutions/IronRDP
|
||||
186
crates/ironrdp-client-glutin/src/config.rs
Normal file
186
crates/ironrdp-client-glutin/src/config.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
use std::num::ParseIntError;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::clap_derive::ValueEnum;
|
||||
use clap::{crate_name, Parser};
|
||||
use ironrdp::session::{GraphicsConfig, InputConfig};
|
||||
use sspi::AuthIdentity;
|
||||
|
||||
const DEFAULT_WIDTH: u16 = 1920;
|
||||
const DEFAULT_HEIGHT: u16 = 1080;
|
||||
const GLOBAL_CHANNEL_NAME: &str = "GLOBAL";
|
||||
const USER_CHANNEL_NAME: &str = "USER";
|
||||
|
||||
pub struct Config {
|
||||
pub log_file: String,
|
||||
pub addr: String,
|
||||
pub input: InputConfig,
|
||||
pub gfx_dump_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum SecurityProtocol {
|
||||
Ssl,
|
||||
Hybrid,
|
||||
HybridEx,
|
||||
}
|
||||
|
||||
impl SecurityProtocol {
|
||||
fn parse(security_protocol: SecurityProtocol) -> ironrdp::pdu::SecurityProtocol {
|
||||
match security_protocol {
|
||||
SecurityProtocol::Ssl => ironrdp::pdu::SecurityProtocol::SSL,
|
||||
SecurityProtocol::Hybrid => ironrdp::pdu::SecurityProtocol::HYBRID,
|
||||
SecurityProtocol::HybridEx => ironrdp::pdu::SecurityProtocol::HYBRID_EX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum KeyboardType {
|
||||
IbmPcXt,
|
||||
OlivettiIco,
|
||||
IbmPcAt,
|
||||
IbmEnhanced,
|
||||
Nokia1050,
|
||||
Nokia9140,
|
||||
Japanese,
|
||||
}
|
||||
|
||||
impl KeyboardType {
|
||||
fn parse(keyboard_type: KeyboardType) -> ironrdp::pdu::gcc::KeyboardType {
|
||||
match keyboard_type {
|
||||
KeyboardType::IbmEnhanced => ironrdp::pdu::gcc::KeyboardType::IbmEnhanced,
|
||||
KeyboardType::IbmPcAt => ironrdp::pdu::gcc::KeyboardType::IbmPcAt,
|
||||
KeyboardType::IbmPcXt => ironrdp::pdu::gcc::KeyboardType::IbmPcXt,
|
||||
KeyboardType::OlivettiIco => ironrdp::pdu::gcc::KeyboardType::OlivettiIco,
|
||||
KeyboardType::Nokia1050 => ironrdp::pdu::gcc::KeyboardType::Nokia1050,
|
||||
KeyboardType::Nokia9140 => ironrdp::pdu::gcc::KeyboardType::Nokia9140,
|
||||
KeyboardType::Japanese => ironrdp::pdu::gcc::KeyboardType::Japanese,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hex(input: &str) -> Result<u32, ParseIntError> {
|
||||
if input.starts_with("0x") {
|
||||
u32::from_str_radix(input.get(2..).unwrap_or(""), 16)
|
||||
} else {
|
||||
input.parse::<u32>()
|
||||
}
|
||||
}
|
||||
/// Devolutions IronRDP client
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author = "Devolutions", about = "Devolutions-IronRDP client")]
|
||||
#[clap(version, long_about = None)]
|
||||
struct Args {
|
||||
/// A file with IronRDP client logs
|
||||
#[clap(short, long, value_parser, default_value_t = format!("{}.log", crate_name!()))]
|
||||
log_file: String,
|
||||
|
||||
/// An address on which the client will connect.
|
||||
addr: String,
|
||||
|
||||
/// A target RDP server user name
|
||||
#[clap(short, long, value_parser)]
|
||||
username: String,
|
||||
|
||||
/// An optional target RDP server domain name
|
||||
#[clap(short, long, value_parser)]
|
||||
domain: Option<String>,
|
||||
|
||||
/// A target RDP server user password
|
||||
#[clap(short, long, value_parser)]
|
||||
password: String,
|
||||
|
||||
/// Specify the security protocols to use
|
||||
#[clap(long, value_enum, value_parser, default_value_t = SecurityProtocol::HybridEx)]
|
||||
security_protocol: SecurityProtocol,
|
||||
|
||||
/// The keyboard type
|
||||
#[clap(long, value_enum, value_parser, default_value_t = KeyboardType::IbmEnhanced)]
|
||||
keyboard_type: KeyboardType,
|
||||
|
||||
/// The keyboard subtype (an original equipment manufacturer-dependent value)
|
||||
#[clap(long, value_parser, default_value_t = 0)]
|
||||
keyboard_subtype: u32,
|
||||
|
||||
/// The number of function keys on the keyboard
|
||||
#[clap(long, value_parser, default_value_t = 12)]
|
||||
keyboard_functional_keys_count: u32,
|
||||
|
||||
/// The input method editor (IME) file name associated with the active input locale
|
||||
#[clap(long, value_parser, default_value_t = String::from(""))]
|
||||
ime_file_name: String,
|
||||
|
||||
/// Contains a value that uniquely identifies the client
|
||||
#[clap(long, value_parser, default_value_t = String::from(""))]
|
||||
dig_product_id: String,
|
||||
|
||||
/// Enable AVC444
|
||||
#[clap(long, group = "avc")]
|
||||
avc444: bool,
|
||||
|
||||
/// Enable H264
|
||||
#[clap(long, group = "avc")]
|
||||
h264: bool,
|
||||
|
||||
/// Enable thin client
|
||||
#[clap(long)]
|
||||
thin_client: bool,
|
||||
|
||||
/// Enable small cache
|
||||
#[clap(long)]
|
||||
small_cache: bool,
|
||||
|
||||
/// Enabled capability versions. Each bit represents enabling a capability version
|
||||
/// starting from V8 to V10_7
|
||||
#[clap(long, value_parser = parse_hex, default_value_t = 0)]
|
||||
capabilities: u32,
|
||||
|
||||
/// Enables dumping the gfx stream to a file location
|
||||
#[clap(long, value_parser)]
|
||||
gfx_dump_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn parse_args() -> Self {
|
||||
let args = Args::parse();
|
||||
|
||||
let graphics_config = if args.avc444 || args.h264 {
|
||||
Some(GraphicsConfig {
|
||||
avc444: args.avc444,
|
||||
h264: args.h264,
|
||||
thin_client: args.thin_client,
|
||||
small_cache: args.small_cache,
|
||||
capabilities: args.capabilities,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let input = InputConfig {
|
||||
credentials: AuthIdentity {
|
||||
username: args.username,
|
||||
password: args.password.into(),
|
||||
domain: args.domain,
|
||||
},
|
||||
security_protocol: SecurityProtocol::parse(args.security_protocol),
|
||||
keyboard_type: KeyboardType::parse(args.keyboard_type),
|
||||
keyboard_subtype: args.keyboard_subtype,
|
||||
keyboard_functional_keys_count: args.keyboard_functional_keys_count,
|
||||
ime_file_name: args.ime_file_name,
|
||||
dig_product_id: args.dig_product_id,
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
global_channel_name: GLOBAL_CHANNEL_NAME.to_string(),
|
||||
user_channel_name: USER_CHANNEL_NAME.to_string(),
|
||||
graphics_config,
|
||||
};
|
||||
|
||||
Self {
|
||||
log_file: args.log_file,
|
||||
addr: args.addr,
|
||||
input,
|
||||
gfx_dump_file: args.gfx_dump_file,
|
||||
}
|
||||
}
|
||||
}
|
||||
147
crates/ironrdp-client-glutin/src/gui.rs
Normal file
147
crates/ironrdp-client-glutin/src/gui.rs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
use std::fmt::Debug;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::{Receiver, SyncSender};
|
||||
use std::sync::{self, Arc};
|
||||
|
||||
use glutin::dpi::PhysicalPosition;
|
||||
use glutin::event::{Event, WindowEvent};
|
||||
use glutin::event_loop::ControlFlow;
|
||||
use ironrdp::pdu::dvc::gfx::ServerPdu;
|
||||
use ironrdp::session::{ErasedWriter, GfxHandler};
|
||||
use ironrdp_glutin_renderer::renderer::Renderer;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use self::input::{handle_input_events, translate_input_event};
|
||||
use crate::RdpError;
|
||||
|
||||
mod input;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MessagePassingGfxHandler {
|
||||
channel: SyncSender<ServerPdu>,
|
||||
}
|
||||
|
||||
impl MessagePassingGfxHandler {
|
||||
pub fn new(channel: SyncSender<ServerPdu>) -> Self {
|
||||
Self { channel }
|
||||
}
|
||||
}
|
||||
|
||||
impl GfxHandler for MessagePassingGfxHandler {
|
||||
fn on_message(&self, message: ServerPdu) -> Result<Option<ironrdp::pdu::dvc::gfx::ClientPdu>, RdpError> {
|
||||
self.channel.send(message).map_err(|e| RdpError::Send(e.to_string()))?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UiContext {
|
||||
window: glutin::ContextWrapper<glutin::NotCurrent, glutin::window::Window>,
|
||||
event_loop: glutin::event_loop::EventLoop<UserEvent>,
|
||||
}
|
||||
|
||||
impl UiContext {
|
||||
fn create_ui_context(
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> (
|
||||
glutin::ContextWrapper<glutin::NotCurrent, glutin::window::Window>,
|
||||
glutin::event_loop::EventLoop<UserEvent>,
|
||||
) {
|
||||
let event_loop = glutin::event_loop::EventLoopBuilder::with_user_event().build();
|
||||
let window_builder = glutin::window::WindowBuilder::new()
|
||||
.with_title("IronRDP Client")
|
||||
.with_resizable(false)
|
||||
.with_inner_size(glutin::dpi::PhysicalSize::new(width, height));
|
||||
let window = glutin::ContextBuilder::new()
|
||||
.with_vsync(true)
|
||||
.build_windowed(window_builder, &event_loop)
|
||||
.unwrap();
|
||||
(window, event_loop)
|
||||
}
|
||||
|
||||
pub fn new(width: u16, height: u16) -> Self {
|
||||
let (window, event_loop) = UiContext::create_ui_context(width as i32, height as i32);
|
||||
UiContext { window, event_loop }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UserEvent {}
|
||||
|
||||
/// Launches the GUI. Because of the way UI programming works the event loop has to be run from main thread
|
||||
pub fn launch_gui(
|
||||
context: UiContext,
|
||||
gfx_dump_file: Option<PathBuf>,
|
||||
graphic_receiver: Receiver<ServerPdu>,
|
||||
stream: Arc<Mutex<ErasedWriter>>,
|
||||
) -> Result<(), RdpError> {
|
||||
let (sender, receiver) = sync::mpsc::channel();
|
||||
|
||||
tokio::spawn(async move { handle_input_events(receiver, stream).await });
|
||||
|
||||
let renderer = Renderer::new(context.window, graphic_receiver, gfx_dump_file);
|
||||
// We handle events differently between targets
|
||||
|
||||
let mut last_position: Option<PhysicalPosition<f64>> = None;
|
||||
context.event_loop.run(move |main_event, _, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match &main_event {
|
||||
Event::LoopDestroyed => {}
|
||||
Event::RedrawRequested(_) => {
|
||||
let res = renderer.repaint();
|
||||
if res.is_err() {
|
||||
error!("Repaint send error: {:?}", res);
|
||||
}
|
||||
}
|
||||
Event::WindowEvent { ref event, .. } => match event {
|
||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||
WindowEvent::Resized(..) => {
|
||||
// let width = new_size.width;
|
||||
// let height = new_size.height;
|
||||
// let scale_factor = window.window().scale_factor();
|
||||
// info!("Scale factor: {} Window size: {:?}x {:?}", scale_factor, width, height);
|
||||
// let layout_pdu = display::ClientPdu::DisplayControlMonitorLayout(MonitorLayoutPdu {
|
||||
// monitors: vec![Monitor {
|
||||
// left: 0,
|
||||
// top: 0,
|
||||
// width: width,
|
||||
// height: height,
|
||||
// flags: MonitorFlags::PRIMARY,
|
||||
// physical_width: 0,
|
||||
// physical_height: 0,
|
||||
// orientation: Orientation::Landscape,
|
||||
// desktop_scale_factor: 0,
|
||||
// device_scale_factor: 0,
|
||||
// }],
|
||||
// });
|
||||
// let mut data_buffer = Vec::new();
|
||||
// layout_pdu.to_buffer(&mut data_buffer)?;
|
||||
// if let (Some(x224_processor), Some(stream)) = (x224_processor.as_ref(), stream.as_mut()) {
|
||||
// let mut x224_processor = x224_processor.lock()?;
|
||||
// // Ignorable error in case of display channel is not connected
|
||||
// let result =
|
||||
// x224_processor.send_dynamic(&mut *stream, x224::RDP8_DISPLAY_PIPELINE_NAME, data_buffer);
|
||||
// if result.is_err() {
|
||||
// error!("Monitor layout {:?}", result);
|
||||
// } else {
|
||||
// error!("Monitor layout success");
|
||||
// }
|
||||
// }
|
||||
}
|
||||
WindowEvent::KeyboardInput { .. }
|
||||
| WindowEvent::MouseInput { .. }
|
||||
| WindowEvent::CursorMoved { .. } => {
|
||||
if let Some(event) = translate_input_event(main_event, &mut last_position) {
|
||||
let result = sender.send(event);
|
||||
if result.is_err() {
|
||||
error!("Send of event failed: {:?}", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
}
|
||||
92
crates/ironrdp-client-glutin/src/gui/input.rs
Normal file
92
crates/ironrdp-client-glutin/src/gui/input.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::AsyncWriteExt;
|
||||
use glutin::dpi::PhysicalPosition;
|
||||
use glutin::event::{ElementState, Event, WindowEvent};
|
||||
use ironrdp::pdu::input::fast_path::{FastPathInput, FastPathInputEvent, KeyboardFlags};
|
||||
use ironrdp::pdu::input::mouse::PointerFlags;
|
||||
use ironrdp::pdu::input::MousePdu;
|
||||
use ironrdp::session::ErasedWriter;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::UserEvent;
|
||||
|
||||
pub async fn handle_input_events(receiver: Receiver<FastPathInputEvent>, event_stream: Arc<Mutex<ErasedWriter>>) {
|
||||
loop {
|
||||
let mut fastpath_events = Vec::new();
|
||||
let event = receiver.recv().unwrap();
|
||||
fastpath_events.push(event);
|
||||
while let Ok(event) = receiver.try_recv() {
|
||||
fastpath_events.push(event);
|
||||
}
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
let input_pdu = FastPathInput(fastpath_events);
|
||||
input_pdu.to_buffer(&mut data).unwrap();
|
||||
let mut event_stream = event_stream.lock().await;
|
||||
let _result = event_stream.write_all(data.as_slice()).await;
|
||||
let _result = event_stream.flush().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn translate_input_event(
|
||||
event: Event<UserEvent>,
|
||||
last_position: &mut Option<PhysicalPosition<f64>>,
|
||||
) -> Option<FastPathInputEvent> {
|
||||
match event {
|
||||
Event::WindowEvent { ref event, .. } => match event {
|
||||
WindowEvent::KeyboardInput {
|
||||
device_id: _,
|
||||
input,
|
||||
is_synthetic: _,
|
||||
} => {
|
||||
let scan_code = input.scancode & 0xff;
|
||||
|
||||
let flags = match input.state {
|
||||
ElementState::Pressed => KeyboardFlags::empty(),
|
||||
ElementState::Released => KeyboardFlags::RELEASE,
|
||||
};
|
||||
Some(FastPathInputEvent::KeyboardEvent(flags, scan_code as u8))
|
||||
}
|
||||
WindowEvent::MouseInput { state, button, .. } => {
|
||||
if let Some(position) = last_position.as_ref() {
|
||||
let button = match button {
|
||||
glutin::event::MouseButton::Left => PointerFlags::LEFT_BUTTON,
|
||||
glutin::event::MouseButton::Right => PointerFlags::RIGHT_BUTTON,
|
||||
glutin::event::MouseButton::Middle => PointerFlags::MIDDLE_BUTTON_OR_WHEEL,
|
||||
glutin::event::MouseButton::Other(_) => PointerFlags::empty(),
|
||||
};
|
||||
let button_events = button
|
||||
| match state {
|
||||
ElementState::Pressed => PointerFlags::DOWN,
|
||||
ElementState::Released => PointerFlags::empty(),
|
||||
};
|
||||
let pdu = MousePdu {
|
||||
x_position: position.x as u16,
|
||||
y_position: position.y as u16,
|
||||
flags: button_events,
|
||||
number_of_wheel_rotation_units: 0,
|
||||
};
|
||||
|
||||
Some(FastPathInputEvent::MouseEvent(pdu))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
*last_position = Some(*position);
|
||||
|
||||
let pdu = MousePdu {
|
||||
x_position: position.x as u16,
|
||||
y_position: position.y as u16,
|
||||
flags: PointerFlags::MOVE,
|
||||
number_of_wheel_rotation_units: 0,
|
||||
};
|
||||
|
||||
Some(FastPathInputEvent::MouseEvent(pdu))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
258
crates/ironrdp-client-glutin/src/main.rs
Normal file
258
crates/ironrdp-client-glutin/src/main.rs
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
mod config;
|
||||
|
||||
use std::sync::mpsc::sync_channel;
|
||||
use std::sync::Arc;
|
||||
use std::{io, process};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use futures_util::io::AsyncWriteExt as _;
|
||||
use gui::MessagePassingGfxHandler;
|
||||
use ironrdp::graphics::image_processing::PixelFormat;
|
||||
use ironrdp::pdu::dvc::gfx::ServerPdu;
|
||||
use ironrdp::session::connection_sequence::{process_connection_sequence, UpgradedStream};
|
||||
use ironrdp::session::image::DecodedImage;
|
||||
use ironrdp::session::{ActiveStageOutput, ActiveStageProcessor, ErasedWriter, RdpError};
|
||||
use sspi::network_client::reqwest_network_client::RequestClientFactory;
|
||||
use tokio::io::AsyncWriteExt as _;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt as _;
|
||||
use x509_parser::prelude::{FromDer as _, X509Certificate};
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
type TlsStream = tokio_util::compat::Compat<tokio_rustls::client::TlsStream<TcpStream>>;
|
||||
|
||||
#[cfg(all(feature = "native-tls", not(feature = "rustls")))]
|
||||
type TlsStream = tokio_util::compat::Compat<async_native_tls::TlsStream<TcpStream>>;
|
||||
|
||||
mod gui;
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
mod danger {
|
||||
use std::time::SystemTime;
|
||||
|
||||
use tokio_rustls::rustls::client::ServerCertVerified;
|
||||
use tokio_rustls::rustls::{Certificate, Error, ServerName};
|
||||
|
||||
pub struct NoCertificateVerification;
|
||||
|
||||
impl tokio_rustls::rustls::client::ServerCertVerifier for NoCertificateVerification {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &Certificate,
|
||||
_intermediates: &[Certificate],
|
||||
_server_name: &ServerName,
|
||||
_scts: &mut dyn Iterator<Item = &[u8]>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: SystemTime,
|
||||
) -> Result<ServerCertVerified, Error> {
|
||||
Ok(tokio_rustls::rustls::client::ServerCertVerified::assertion())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let config = Config::parse_args();
|
||||
setup_logging(config.log_file.as_str()).expect("failed to initialize logging");
|
||||
|
||||
let exit_code = match run(config).await {
|
||||
Ok(_) => {
|
||||
println!("RDP successfully finished");
|
||||
exitcode::OK
|
||||
}
|
||||
Err(RdpError::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => {
|
||||
error!("{}", e);
|
||||
println!("The server has terminated the RDP session");
|
||||
exitcode::NOHOST
|
||||
}
|
||||
Err(ref e) => {
|
||||
error!("{}", e);
|
||||
println!("RDP failed because of {e}");
|
||||
|
||||
match e {
|
||||
RdpError::Io(_) => exitcode::IOERR,
|
||||
RdpError::Connection(_) => exitcode::NOHOST,
|
||||
_ => exitcode::PROTOCOL,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
fn setup_logging(log_file: &str) -> anyhow::Result<()> {
|
||||
use std::fs::OpenOptions;
|
||||
|
||||
use tracing::metadata::LevelFilter;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(log_file)
|
||||
.with_context(|| format!("Couldn’t open {log_file}"))?;
|
||||
|
||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||
.compact()
|
||||
.with_ansi(false)
|
||||
.with_writer(file);
|
||||
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::WARN.into())
|
||||
.with_env_var("IRONRDP_LOG_LEVEL")
|
||||
.from_env_lossy();
|
||||
|
||||
let reg = tracing_subscriber::registry().with(fmt_layer).with(env_filter);
|
||||
|
||||
tracing::subscriber::set_global_default(reg).context("Failed to set tracing global subscriber")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run(config: Config) -> Result<(), RdpError> {
|
||||
let addr = ironrdp::session::connection_sequence::Address::lookup_addr(&config.addr)?;
|
||||
|
||||
let stream = TcpStream::connect(addr.sock).await.map_err(RdpError::Connection)?;
|
||||
|
||||
let (connection_sequence_result, reader, writer) = process_connection_sequence(
|
||||
stream.compat(),
|
||||
&addr,
|
||||
&config.input,
|
||||
establish_tls,
|
||||
Box::new(RequestClientFactory),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let writer = Arc::new(Mutex::new(writer));
|
||||
let image = DecodedImage::new(
|
||||
PixelFormat::RgbA32,
|
||||
connection_sequence_result.desktop_size.width,
|
||||
connection_sequence_result.desktop_size.height,
|
||||
);
|
||||
|
||||
launch_client(config, connection_sequence_result, image, reader, writer).await
|
||||
}
|
||||
|
||||
async fn launch_client(
|
||||
config: Config,
|
||||
connection_sequence_result: ironrdp::session::connection_sequence::ConnectionSequenceResult,
|
||||
image: DecodedImage,
|
||||
reader: ironrdp::session::FramedReader,
|
||||
writer: Arc<Mutex<ErasedWriter>>,
|
||||
) -> Result<(), RdpError> {
|
||||
let (sender, receiver) = sync_channel::<ServerPdu>(1);
|
||||
let handler = MessagePassingGfxHandler::new(sender);
|
||||
let active_stage = ActiveStageProcessor::new(
|
||||
config.input.clone(),
|
||||
Some(Box::new(handler)),
|
||||
connection_sequence_result,
|
||||
);
|
||||
let gui = gui::UiContext::new(config.input.width, config.input.height);
|
||||
|
||||
let active_stage_writer = writer.clone();
|
||||
let active_stage_handle = tokio::spawn(async move {
|
||||
match process_active_stage(reader, active_stage, image, active_stage_writer).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) => {
|
||||
error!(?error, "Active stage failed");
|
||||
process::exit(-1);
|
||||
}
|
||||
}
|
||||
});
|
||||
gui::launch_gui(gui, config.gfx_dump_file, receiver, writer.clone())?;
|
||||
active_stage_handle.await.map_err(|e| RdpError::Io(e.into()))?
|
||||
}
|
||||
|
||||
async fn process_active_stage(
|
||||
mut reader: ironrdp::session::FramedReader,
|
||||
mut active_stage: ActiveStageProcessor,
|
||||
mut image: DecodedImage,
|
||||
writer: Arc<Mutex<ErasedWriter>>,
|
||||
) -> Result<(), RdpError> {
|
||||
'outer: loop {
|
||||
let frame = reader.read_frame().await?.ok_or(RdpError::AccessDenied)?.freeze();
|
||||
let outputs = active_stage.process(&mut image, frame)?;
|
||||
for out in outputs {
|
||||
match out {
|
||||
ActiveStageOutput::ResponseFrame(frame) => {
|
||||
let mut writer = writer.lock().await;
|
||||
writer.write_all(&frame).await?
|
||||
}
|
||||
ActiveStageOutput::GraphicsUpdate(_region) => {}
|
||||
ActiveStageOutput::Terminate => break 'outer,
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: this can be refactored into a separate `ironrdp-tls` crate (all native clients will do the same TLS dance)
|
||||
async fn establish_tls(stream: tokio_util::compat::Compat<TcpStream>) -> Result<UpgradedStream<TlsStream>, RdpError> {
|
||||
let stream = stream.into_inner();
|
||||
|
||||
#[cfg(all(feature = "native-tls", not(feature = "rustls")))]
|
||||
let mut tls_stream = {
|
||||
let connector = async_native_tls::TlsConnector::new()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.use_sni(false);
|
||||
|
||||
// domain is an empty string because client accepts IP address in the cli
|
||||
match connector.connect("", stream).await {
|
||||
Ok(tls) => tls,
|
||||
Err(err) => return Err(RdpError::TlsHandshake(err)),
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
let mut tls_stream = {
|
||||
let mut client_config = tokio_rustls::rustls::client::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_custom_certificate_verifier(std::sync::Arc::new(danger::NoCertificateVerification))
|
||||
.with_no_client_auth();
|
||||
// This adds support for the SSLKEYLOGFILE env variable (https://wiki.wireshark.org/TLS#using-the-pre-master-secret)
|
||||
client_config.key_log = std::sync::Arc::new(tokio_rustls::rustls::KeyLogFile::new());
|
||||
let rc_config = std::sync::Arc::new(client_config);
|
||||
let example_com = "stub_string".try_into().unwrap();
|
||||
let connector = tokio_rustls::TlsConnector::from(rc_config);
|
||||
connector.connect(example_com, stream).await?
|
||||
};
|
||||
|
||||
tls_stream.flush().await?;
|
||||
|
||||
#[cfg(all(feature = "native-tls", not(feature = "rustls")))]
|
||||
let server_public_key = {
|
||||
let cert = tls_stream
|
||||
.peer_certificate()
|
||||
.map_err(RdpError::TlsConnector)?
|
||||
.ok_or(RdpError::MissingPeerCertificate)?;
|
||||
get_tls_peer_pubkey(cert.to_der().map_err(RdpError::DerEncode)?)?
|
||||
};
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
let server_public_key = {
|
||||
let cert = tls_stream
|
||||
.get_ref()
|
||||
.1
|
||||
.peer_certificates()
|
||||
.ok_or(RdpError::MissingPeerCertificate)?[0]
|
||||
.as_ref();
|
||||
get_tls_peer_pubkey(cert.to_vec())?
|
||||
};
|
||||
|
||||
Ok(UpgradedStream {
|
||||
stream: tls_stream.compat(),
|
||||
server_public_key,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_tls_peer_pubkey(cert: Vec<u8>) -> io::Result<Vec<u8>> {
|
||||
let res = X509Certificate::from_der(&cert[..])
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid der certificate."))?;
|
||||
let public_key = res.1.tbs_certificate.subject_pki.subject_public_key;
|
||||
|
||||
Ok(public_key.data.to_vec())
|
||||
}
|
||||
94
crates/ironrdp-client/Cargo.toml
Normal file
94
crates/ironrdp-client/Cargo.toml
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
[package]
|
||||
name = "ironrdp-client"
|
||||
version = "0.1.0"
|
||||
readme = "README.md"
|
||||
description = "Portable RDP client without GPU acceleration"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
default-run = "ironrdp-client"
|
||||
|
||||
# Not publishing for now.
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[[bin]]
|
||||
name = "ironrdp-client"
|
||||
test = false
|
||||
|
||||
[features]
|
||||
default = ["rustls"]
|
||||
rustls = ["ironrdp-tls/rustls", "tokio-tungstenite/rustls-tls-native-roots", "ironrdp-mstsgu/rustls"]
|
||||
native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls", "ironrdp-mstsgu/native-tls"]
|
||||
qoi = ["ironrdp/qoi"]
|
||||
qoiz = ["ironrdp/qoiz"]
|
||||
|
||||
[dependencies]
|
||||
# Protocols
|
||||
ironrdp = { path = "../ironrdp", version = "0.14", features = [
|
||||
"session",
|
||||
"input",
|
||||
"graphics",
|
||||
"dvc",
|
||||
"svc",
|
||||
"rdpdr",
|
||||
"rdpsnd",
|
||||
"cliprdr",
|
||||
"displaycontrol",
|
||||
"connector",
|
||||
] }
|
||||
ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["alloc"] }
|
||||
ironrdp-cliprdr-native = { path = "../ironrdp-cliprdr-native", version = "0.5" }
|
||||
ironrdp-rdpsnd-native = { path = "../ironrdp-rdpsnd-native", version = "0.4" }
|
||||
ironrdp-tls = { path = "../ironrdp-tls", version = "0.2" }
|
||||
ironrdp-mstsgu = { path = "../ironrdp-mstsgu" }
|
||||
ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.8", features = ["reqwest"] }
|
||||
ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath"
|
||||
ironrdp-dvc-pipe-proxy.path = "../ironrdp-dvc-pipe-proxy"
|
||||
ironrdp-propertyset.path = "../ironrdp-propertyset"
|
||||
ironrdp-rdpfile.path = "../ironrdp-rdpfile"
|
||||
ironrdp-cfg.path = "../ironrdp-cfg"
|
||||
|
||||
# Windowing and rendering
|
||||
winit = { version = "0.30", features = ["rwh_06"] }
|
||||
softbuffer = "0.4"
|
||||
|
||||
# CLI
|
||||
clap = { version = "4.5", features = ["derive", "cargo"] }
|
||||
proc-exit = "2"
|
||||
inquire = "0.9"
|
||||
|
||||
# Logging
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# Async, futures
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7" }
|
||||
tokio-tungstenite = "0.28"
|
||||
transport = { git = "https://github.com/Devolutions/devolutions-gateway", rev = "06e91dfe82751a6502eaf74b6a99663f06f0236d" }
|
||||
futures-util = { version = "0.3", features = ["sink"] }
|
||||
|
||||
# Utils
|
||||
whoami = "1.6"
|
||||
anyhow = "1"
|
||||
smallvec = "1.15"
|
||||
tap = "1"
|
||||
semver = "1"
|
||||
raw-window-handle = "0.6"
|
||||
uuid = { version = "1.19" }
|
||||
x509-cert = { version = "0.2", default-features = false, features = ["std"] }
|
||||
url = "2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.62", features = ["Win32_Foundation"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
48
crates/ironrdp-client/README.md
Normal file
48
crates/ironrdp-client/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# IronRDP client
|
||||
|
||||
Portable RDP client without GPU acceleration.
|
||||
|
||||
This is a a full-fledged RDP client based on IronRDP crates suite, and implemented using
|
||||
non-blocking, asynchronous I/O. Portability is achieved by using softbuffer for rendering
|
||||
and winit for windowing.
|
||||
|
||||
## Sample usage
|
||||
|
||||
```shell
|
||||
ironrdp-client <HOSTNAME> --username <USERNAME> --password <PASSWORD>
|
||||
```
|
||||
|
||||
## Configuring log filter directives
|
||||
|
||||
The `IRONRDP_LOG` environment variable is used to set the log filter directives.
|
||||
|
||||
```shell
|
||||
IRONRDP_LOG="info,ironrdp_connector=trace" ironrdp-client <HOSTNAME> --username <USERNAME> --password <PASSWORD>
|
||||
```
|
||||
|
||||
See [`tracing-subscriber`’s documentation][tracing-doc] for more details.
|
||||
|
||||
[tracing-doc]: https://docs.rs/tracing-subscriber/0.3.17/tracing_subscriber/filter/struct.EnvFilter.html#directives
|
||||
|
||||
## Support for `SSLKEYLOGFILE`
|
||||
|
||||
This client supports reading the `SSLKEYLOGFILE` environment variable.
|
||||
When set, the TLS encryption secrets for the session will be dumped to the file specified
|
||||
by the environment variable.
|
||||
This file can be read by Wireshark so that in can decrypt the packets.
|
||||
|
||||
### Example
|
||||
|
||||
```shell
|
||||
SSLKEYLOGFILE=/tmp/tls-secrets ironrdp-client <HOSTNAME> --username <USERNAME> --password <PASSWORD>
|
||||
```
|
||||
|
||||
### Usage in Wireshark
|
||||
|
||||
See this [awakecoding's repository][awakecoding-repository] explaining how to use the file in wireshark.
|
||||
|
||||
This crate is part of the [IronRDP] project.
|
||||
|
||||
[IronRDP]: https://github.com/Devolutions/IronRDP
|
||||
[awakecoding-repository]: https://github.com/awakecoding/wireshark-rdp#sslkeylogfile
|
||||
|
||||
414
crates/ironrdp-client/src/app.rs
Normal file
414
crates/ironrdp-client/src/app.rs
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
#![allow(clippy::print_stderr, clippy::print_stdout)] // allowed in this module only
|
||||
|
||||
use core::num::NonZeroU32;
|
||||
use core::time::Duration;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use raw_window_handle::{DisplayHandle, HasDisplayHandle as _};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::{LogicalPosition, PhysicalSize};
|
||||
use winit::event::{self, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::platform::scancode::PhysicalKeyExtScancode as _;
|
||||
use winit::window::{CursorIcon, CustomCursor, Window, WindowAttributes};
|
||||
|
||||
use crate::rdp::{RdpInputEvent, RdpOutputEvent};
|
||||
|
||||
type WindowSurface = (Arc<Window>, softbuffer::Surface<DisplayHandle<'static>, Arc<Window>>);
|
||||
|
||||
pub struct App {
|
||||
input_event_sender: mpsc::UnboundedSender<RdpInputEvent>,
|
||||
context: softbuffer::Context<DisplayHandle<'static>>,
|
||||
window: Option<WindowSurface>,
|
||||
buffer: Vec<u32>,
|
||||
buffer_size: (u16, u16),
|
||||
input_database: ironrdp::input::Database,
|
||||
last_size: Option<PhysicalSize<u32>>,
|
||||
resize_timeout: Option<Instant>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(
|
||||
event_loop: &EventLoop<RdpOutputEvent>,
|
||||
input_event_sender: &mpsc::UnboundedSender<RdpInputEvent>,
|
||||
) -> anyhow::Result<Self> {
|
||||
// SAFETY: We drop the softbuffer context right before the event loop is stopped, thus making this safe.
|
||||
// FIXME: This is not a sufficient proof and the API is actually unsound as-is.
|
||||
let display_handle = unsafe {
|
||||
core::mem::transmute::<DisplayHandle<'_>, DisplayHandle<'static>>(
|
||||
event_loop.display_handle().context("get display handle")?,
|
||||
)
|
||||
};
|
||||
let context = softbuffer::Context::new(display_handle)
|
||||
.map_err(|e| anyhow::anyhow!("unable to initialize softbuffer context: {e}"))?;
|
||||
|
||||
let input_database = ironrdp::input::Database::new();
|
||||
Ok(Self {
|
||||
input_event_sender: input_event_sender.clone(),
|
||||
context,
|
||||
window: None,
|
||||
buffer: Vec::new(),
|
||||
buffer_size: (0, 0),
|
||||
input_database,
|
||||
last_size: None,
|
||||
resize_timeout: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn send_resize_event(&mut self) {
|
||||
let Some(size) = self.last_size.take() else {
|
||||
return;
|
||||
};
|
||||
let Some((window, _)) = self.window.as_mut() else {
|
||||
return;
|
||||
};
|
||||
#[expect(clippy::as_conversions, reason = "casting f64 to u32")]
|
||||
let scale_factor = (window.scale_factor() * 100.0) as u32;
|
||||
|
||||
let width = u16::try_from(size.width).expect("reasonable width");
|
||||
let height = u16::try_from(size.height).expect("reasonable height");
|
||||
|
||||
let _ = self.input_event_sender.send(RdpInputEvent::Resize {
|
||||
width,
|
||||
height,
|
||||
scale_factor,
|
||||
// TODO: it should be possible to get the physical size here, however winit doesn't make it straightforward.
|
||||
// FreeRDP does it based on DPI reading grabbed via [`SDL_GetDisplayDPI`](https://wiki.libsdl.org/SDL2/SDL_GetDisplayDPI):
|
||||
// https://github.com/FreeRDP/FreeRDP/blob/ba8cf8cf2158018fb7abbedb51ab245f369be813/client/SDL/sdl_monitor.cpp#L250-L262
|
||||
// See also: https://github.com/rust-windowing/winit/issues/826
|
||||
physical_size: None,
|
||||
});
|
||||
}
|
||||
|
||||
fn draw(&mut self) {
|
||||
if self.buffer.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some((_, surface)) = self.window.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let mut sb_buffer = surface.buffer_mut().expect("surface buffer");
|
||||
sb_buffer.copy_from_slice(self.buffer.as_slice());
|
||||
sb_buffer.present().expect("buffer present");
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler<RdpOutputEvent> for App {
|
||||
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if let Some(timeout) = self.resize_timeout {
|
||||
if let Some(timeout) = timeout.checked_duration_since(Instant::now()) {
|
||||
event_loop.set_control_flow(ControlFlow::wait_duration(timeout));
|
||||
} else {
|
||||
self.send_resize_event();
|
||||
self.resize_timeout = None;
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let window_attributes = WindowAttributes::default().with_title("IronRDP");
|
||||
match event_loop.create_window(window_attributes) {
|
||||
Ok(window) => {
|
||||
let window = Arc::new(window);
|
||||
let surface = softbuffer::Surface::new(&self.context, Arc::clone(&window)).expect("surface");
|
||||
self.window = Some((window, surface));
|
||||
}
|
||||
Err(error) => {
|
||||
error!(%error, "Failed to create window");
|
||||
event_loop.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: winit::window::WindowId, event: WindowEvent) {
|
||||
let Some((window, _)) = self.window.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if window_id != window.id() {
|
||||
return;
|
||||
}
|
||||
|
||||
match event {
|
||||
WindowEvent::Resized(size) => {
|
||||
self.last_size = Some(size);
|
||||
self.resize_timeout = Some(Instant::now() + Duration::from_secs(1));
|
||||
}
|
||||
WindowEvent::CloseRequested => {
|
||||
if self.input_event_sender.send(RdpInputEvent::Close).is_err() {
|
||||
error!("Failed to send graceful shutdown event, closing the window");
|
||||
event_loop.exit();
|
||||
}
|
||||
}
|
||||
WindowEvent::DroppedFile(_) => {
|
||||
// TODO(#110): File upload
|
||||
}
|
||||
// WindowEvent::ReceivedCharacter(_) => {
|
||||
// Sadly, we can't use this winit event to send RDP unicode events because
|
||||
// of the several reasons:
|
||||
// 1. `ReceivedCharacter` event doesn't provide a way to distinguish between
|
||||
// key press and key release, therefore the only way to use it is to send
|
||||
// a key press + release events sequentially, which will not allow to
|
||||
// handle long press and key repeat events.
|
||||
// 2. This event do not fire for non-printable keys (e.g. Control, Alt, etc.)
|
||||
// 3. This event fies BEFORE `KeyboardInput` event, so we can't make a
|
||||
// reasonable workaround for `1` and `2` by collecting physical key press
|
||||
// information first via `KeyboardInput` before processing `ReceivedCharacter`.
|
||||
//
|
||||
// However, all of these issues can be solved by updating `winit` to the
|
||||
// newer version.
|
||||
//
|
||||
// TODO(#376): Update winit
|
||||
// TODO(#376): Implement unicode input in native client
|
||||
// }
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
if let Some(scancode) = event.physical_key.to_scancode() {
|
||||
let scancode = match u16::try_from(scancode) {
|
||||
Ok(scancode) => scancode,
|
||||
Err(_) => {
|
||||
warn!("Unsupported scancode: `{scancode:#X}`; ignored");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let scancode = ironrdp::input::Scancode::from_u16(scancode);
|
||||
|
||||
let operation = match event.state {
|
||||
event::ElementState::Pressed => ironrdp::input::Operation::KeyPressed(scancode),
|
||||
event::ElementState::Released => ironrdp::input::Operation::KeyReleased(scancode),
|
||||
};
|
||||
|
||||
let input_events = self.input_database.apply(core::iter::once(operation));
|
||||
|
||||
send_fast_path_events(&self.input_event_sender, input_events);
|
||||
}
|
||||
}
|
||||
WindowEvent::ModifiersChanged(modifiers) => {
|
||||
const SHIFT_LEFT: ironrdp::input::Scancode = ironrdp::input::Scancode::from_u8(false, 0x2A);
|
||||
const CONTROL_LEFT: ironrdp::input::Scancode = ironrdp::input::Scancode::from_u8(false, 0x1D);
|
||||
const ALT_LEFT: ironrdp::input::Scancode = ironrdp::input::Scancode::from_u8(false, 0x38);
|
||||
const LOGO_LEFT: ironrdp::input::Scancode = ironrdp::input::Scancode::from_u8(true, 0x5B);
|
||||
|
||||
let mut operations = smallvec::SmallVec::<[ironrdp::input::Operation; 4]>::new();
|
||||
|
||||
let mut add_operation = |pressed: bool, scancode: ironrdp::input::Scancode| {
|
||||
let operation = if pressed {
|
||||
ironrdp::input::Operation::KeyPressed(scancode)
|
||||
} else {
|
||||
ironrdp::input::Operation::KeyReleased(scancode)
|
||||
};
|
||||
operations.push(operation);
|
||||
};
|
||||
|
||||
// NOTE: https://docs.rs/winit/0.30.12/src/winit/keyboard.rs.html#1737-1744
|
||||
//
|
||||
// We can’t use state.lshift_state(), state.lcontrol_state(), etc, because on some platforms such as
|
||||
// Linux, the modifiers change is hidden.
|
||||
//
|
||||
// > The exact modifier key is not used to represent modifiers state in the
|
||||
// > first place due to a fact that modifiers state could be changed without any
|
||||
// > key being pressed and on some platforms like Wayland/X11 which key resulted
|
||||
// > in modifiers change is hidden, also, not that it really matters.
|
||||
add_operation(modifiers.state().shift_key(), SHIFT_LEFT);
|
||||
add_operation(modifiers.state().control_key(), CONTROL_LEFT);
|
||||
add_operation(modifiers.state().alt_key(), ALT_LEFT);
|
||||
add_operation(modifiers.state().super_key(), LOGO_LEFT);
|
||||
|
||||
let input_events = self.input_database.apply(operations);
|
||||
|
||||
send_fast_path_events(&self.input_event_sender, input_events);
|
||||
}
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
let win_size = window.inner_size();
|
||||
#[expect(clippy::as_conversions, reason = "casting f64 to u16")]
|
||||
let x = (position.x / f64::from(win_size.width) * f64::from(self.buffer_size.0)) as u16;
|
||||
#[expect(clippy::as_conversions, reason = "casting f64 to u16")]
|
||||
let y = (position.y / f64::from(win_size.height) * f64::from(self.buffer_size.1)) as u16;
|
||||
let operation = ironrdp::input::Operation::MouseMove(ironrdp::input::MousePosition { x, y });
|
||||
|
||||
let input_events = self.input_database.apply(core::iter::once(operation));
|
||||
|
||||
send_fast_path_events(&self.input_event_sender, input_events);
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } => {
|
||||
let mut operations = smallvec::SmallVec::<[ironrdp::input::Operation; 2]>::new();
|
||||
|
||||
match delta {
|
||||
event::MouseScrollDelta::LineDelta(delta_x, delta_y) => {
|
||||
if delta_x.abs() > 0.001 {
|
||||
operations.push(ironrdp::input::Operation::WheelRotations(
|
||||
ironrdp::input::WheelRotations {
|
||||
is_vertical: false,
|
||||
#[expect(clippy::as_conversions, reason = "casting f32 to i16")]
|
||||
rotation_units: (delta_x * 100.) as i16,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if delta_y.abs() > 0.001 {
|
||||
operations.push(ironrdp::input::Operation::WheelRotations(
|
||||
ironrdp::input::WheelRotations {
|
||||
is_vertical: true,
|
||||
#[expect(clippy::as_conversions, reason = "casting f32 to i16")]
|
||||
rotation_units: (delta_y * 100.) as i16,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
event::MouseScrollDelta::PixelDelta(delta) => {
|
||||
if delta.x.abs() > 0.001 {
|
||||
operations.push(ironrdp::input::Operation::WheelRotations(
|
||||
ironrdp::input::WheelRotations {
|
||||
is_vertical: false,
|
||||
#[expect(clippy::as_conversions, reason = "casting f64 to i16")]
|
||||
rotation_units: delta.x as i16,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if delta.y.abs() > 0.001 {
|
||||
operations.push(ironrdp::input::Operation::WheelRotations(
|
||||
ironrdp::input::WheelRotations {
|
||||
is_vertical: true,
|
||||
#[expect(clippy::as_conversions, reason = "casting f64 to i16")]
|
||||
rotation_units: delta.y as i16,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let input_events = self.input_database.apply(operations);
|
||||
|
||||
send_fast_path_events(&self.input_event_sender, input_events);
|
||||
}
|
||||
WindowEvent::MouseInput { state, button, .. } => {
|
||||
let mouse_button = match button {
|
||||
event::MouseButton::Left => ironrdp::input::MouseButton::Left,
|
||||
event::MouseButton::Right => ironrdp::input::MouseButton::Right,
|
||||
event::MouseButton::Middle => ironrdp::input::MouseButton::Middle,
|
||||
event::MouseButton::Back => ironrdp::input::MouseButton::X1,
|
||||
event::MouseButton::Forward => ironrdp::input::MouseButton::X2,
|
||||
event::MouseButton::Other(native_button) => {
|
||||
if let Some(button) = ironrdp::input::MouseButton::from_native_button(native_button) {
|
||||
button
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let operation = match state {
|
||||
event::ElementState::Pressed => ironrdp::input::Operation::MouseButtonPressed(mouse_button),
|
||||
event::ElementState::Released => ironrdp::input::Operation::MouseButtonReleased(mouse_button),
|
||||
};
|
||||
|
||||
let input_events = self.input_database.apply(core::iter::once(operation));
|
||||
|
||||
send_fast_path_events(&self.input_event_sender, input_events);
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
self.draw();
|
||||
}
|
||||
WindowEvent::ActivationTokenDone { .. }
|
||||
| WindowEvent::Moved(_)
|
||||
| WindowEvent::Destroyed
|
||||
| WindowEvent::HoveredFile(_)
|
||||
| WindowEvent::HoveredFileCancelled
|
||||
| WindowEvent::Focused(_)
|
||||
| WindowEvent::Ime(_)
|
||||
| WindowEvent::CursorEntered { .. }
|
||||
| WindowEvent::CursorLeft { .. }
|
||||
| WindowEvent::PinchGesture { .. }
|
||||
| WindowEvent::PanGesture { .. }
|
||||
| WindowEvent::DoubleTapGesture { .. }
|
||||
| WindowEvent::RotationGesture { .. }
|
||||
| WindowEvent::TouchpadPressure { .. }
|
||||
| WindowEvent::AxisMotion { .. }
|
||||
| WindowEvent::Touch(_)
|
||||
| WindowEvent::ScaleFactorChanged { .. }
|
||||
| WindowEvent::ThemeChanged(_)
|
||||
| WindowEvent::Occluded(_) => {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: RdpOutputEvent) {
|
||||
let Some((window, surface)) = self.window.as_mut() else {
|
||||
return;
|
||||
};
|
||||
match event {
|
||||
RdpOutputEvent::Image { buffer, width, height } => {
|
||||
trace!(width = ?width, height = ?height, "Received image with size");
|
||||
trace!(window_physical_size = ?window.inner_size(), "Drawing image to the window with size");
|
||||
self.buffer_size = (width.get(), height.get());
|
||||
self.buffer = buffer;
|
||||
surface
|
||||
.resize(NonZeroU32::from(width), NonZeroU32::from(height))
|
||||
.expect("surface resize");
|
||||
|
||||
window.request_redraw();
|
||||
}
|
||||
RdpOutputEvent::ConnectionFailure(error) => {
|
||||
error!(?error);
|
||||
eprintln!("Connection error: {}", error.report());
|
||||
// TODO set proc_exit::sysexits::PROTOCOL_ERR.as_raw());
|
||||
event_loop.exit();
|
||||
}
|
||||
RdpOutputEvent::Terminated(result) => {
|
||||
let _exit_code = match result {
|
||||
Ok(reason) => {
|
||||
println!("Terminated gracefully: {reason}");
|
||||
proc_exit::sysexits::OK
|
||||
}
|
||||
Err(error) => {
|
||||
error!(?error);
|
||||
eprintln!("Active session error: {}", error.report());
|
||||
proc_exit::sysexits::PROTOCOL_ERR
|
||||
}
|
||||
};
|
||||
// TODO set exit_code.as_raw());
|
||||
event_loop.exit();
|
||||
}
|
||||
RdpOutputEvent::PointerHidden => {
|
||||
window.set_cursor_visible(false);
|
||||
}
|
||||
RdpOutputEvent::PointerDefault => {
|
||||
window.set_cursor(CursorIcon::default());
|
||||
window.set_cursor_visible(true);
|
||||
}
|
||||
RdpOutputEvent::PointerPosition { x, y } => {
|
||||
if let Err(error) = window.set_cursor_position(LogicalPosition::new(x, y)) {
|
||||
error!(?error, "Failed to set cursor position");
|
||||
}
|
||||
}
|
||||
RdpOutputEvent::PointerBitmap(pointer) => {
|
||||
debug!(width = ?pointer.width, height = ?pointer.height, "Received pointer bitmap");
|
||||
match CustomCursor::from_rgba(
|
||||
pointer.bitmap_data.clone(),
|
||||
pointer.width,
|
||||
pointer.height,
|
||||
pointer.hotspot_x,
|
||||
pointer.hotspot_y,
|
||||
) {
|
||||
Ok(cursor) => window.set_cursor(event_loop.create_custom_cursor(cursor)),
|
||||
Err(error) => error!(?error, "Failed to set cursor bitmap"),
|
||||
}
|
||||
window.set_cursor_visible(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_fast_path_events(
|
||||
input_event_sender: &mpsc::UnboundedSender<RdpInputEvent>,
|
||||
input_events: smallvec::SmallVec<[ironrdp::pdu::input::fast_path::FastPathInputEvent; 2]>,
|
||||
) {
|
||||
if !input_events.is_empty() {
|
||||
let _ = input_event_sender.send(RdpInputEvent::FastPath(input_events));
|
||||
}
|
||||
}
|
||||
25
crates/ironrdp-client/src/clipboard.rs
Normal file
25
crates/ironrdp-client/src/clipboard.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use ironrdp::cliprdr::backend::{ClipboardMessage, ClipboardMessageProxy};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::error;
|
||||
|
||||
use crate::rdp::RdpInputEvent;
|
||||
|
||||
/// Shim for sending and receiving CLIPRDR events as `RdpInputEvent`
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ClientClipboardMessageProxy {
|
||||
tx: mpsc::UnboundedSender<RdpInputEvent>,
|
||||
}
|
||||
|
||||
impl ClientClipboardMessageProxy {
|
||||
pub fn new(tx: mpsc::UnboundedSender<RdpInputEvent>) -> Self {
|
||||
Self { tx }
|
||||
}
|
||||
}
|
||||
|
||||
impl ClipboardMessageProxy for ClientClipboardMessageProxy {
|
||||
fn send_clipboard_message(&self, message: ClipboardMessage) {
|
||||
if self.tx.send(RdpInputEvent::Clipboard(message)).is_err() {
|
||||
error!("Failed to send os clipboard message, receiver is closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
483
crates/ironrdp-client/src/config.rs
Normal file
483
crates/ironrdp-client/src/config.rs
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
#![allow(clippy::print_stdout)]
|
||||
|
||||
use core::num::ParseIntError;
|
||||
use core::str::FromStr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use clap::clap_derive::ValueEnum;
|
||||
use clap::Parser;
|
||||
use ironrdp::connector::{self, Credentials};
|
||||
use ironrdp::pdu::rdp::capability_sets::{client_codecs_capabilities, MajorPlatformType};
|
||||
use ironrdp::pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo};
|
||||
use ironrdp_mstsgu::GwConnectTarget;
|
||||
use tap::prelude::*;
|
||||
use url::Url;
|
||||
|
||||
const DEFAULT_WIDTH: u16 = 1920;
|
||||
const DEFAULT_HEIGHT: u16 = 1080;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
pub log_file: Option<String>,
|
||||
pub gw: Option<GwConnectTarget>,
|
||||
pub destination: Destination,
|
||||
pub connector: connector::Config,
|
||||
pub clipboard_type: ClipboardType,
|
||||
pub rdcleanpath: Option<RDCleanPathConfig>,
|
||||
|
||||
/// DVC channel <-> named pipe proxy configuration.
|
||||
///
|
||||
/// Each configured proxy enables IronRDP to connect to DVC channel and create a named pipe
|
||||
/// server, which will be used for proxying DVC messages to/from user-defined DVC logic
|
||||
/// implemented as named pipe clients (either in the same process or in a different process).
|
||||
pub dvc_pipe_proxies: Vec<DvcProxyInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
pub enum ClipboardType {
|
||||
Default,
|
||||
Stub,
|
||||
#[cfg(windows)]
|
||||
Windows,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
enum KeyboardType {
|
||||
IbmPcXt,
|
||||
OlivettiIco,
|
||||
IbmPcAt,
|
||||
IbmEnhanced,
|
||||
Nokia1050,
|
||||
Nokia9140,
|
||||
Japanese,
|
||||
}
|
||||
|
||||
impl KeyboardType {
|
||||
fn parse(keyboard_type: KeyboardType) -> ironrdp::pdu::gcc::KeyboardType {
|
||||
match keyboard_type {
|
||||
KeyboardType::IbmEnhanced => ironrdp::pdu::gcc::KeyboardType::IbmEnhanced,
|
||||
KeyboardType::IbmPcAt => ironrdp::pdu::gcc::KeyboardType::IbmPcAt,
|
||||
KeyboardType::IbmPcXt => ironrdp::pdu::gcc::KeyboardType::IbmPcXt,
|
||||
KeyboardType::OlivettiIco => ironrdp::pdu::gcc::KeyboardType::OlivettiIco,
|
||||
KeyboardType::Nokia1050 => ironrdp::pdu::gcc::KeyboardType::Nokia1050,
|
||||
KeyboardType::Nokia9140 => ironrdp::pdu::gcc::KeyboardType::Nokia9140,
|
||||
KeyboardType::Japanese => ironrdp::pdu::gcc::KeyboardType::Japanese,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hex(input: &str) -> Result<u32, ParseIntError> {
|
||||
if input.starts_with("0x") {
|
||||
u32::from_str_radix(input.get(2..).unwrap_or(""), 16)
|
||||
} else {
|
||||
input.parse::<u32>()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Destination {
|
||||
name: String,
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl Destination {
|
||||
pub fn new(addr: impl Into<String>) -> anyhow::Result<Self> {
|
||||
const RDP_DEFAULT_PORT: u16 = 3389;
|
||||
|
||||
let addr = addr.into();
|
||||
|
||||
if let Some(addr_split) = addr.rsplit_once(':') {
|
||||
if let Ok(sock_addr) = addr.parse::<core::net::SocketAddr>() {
|
||||
Ok(Self {
|
||||
name: sock_addr.ip().to_string(),
|
||||
port: sock_addr.port(),
|
||||
})
|
||||
} else if addr.parse::<core::net::Ipv6Addr>().is_ok() {
|
||||
Ok(Self {
|
||||
name: addr,
|
||||
port: RDP_DEFAULT_PORT,
|
||||
})
|
||||
} else {
|
||||
Ok(Self {
|
||||
name: addr_split.0.to_owned(),
|
||||
port: addr_split.1.parse().context("invalid port")?,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Ok(Self {
|
||||
name: addr,
|
||||
port: RDP_DEFAULT_PORT,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn port(&self) -> u16 {
|
||||
self.port
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Destination {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Destination> for connector::ServerName {
|
||||
fn from(value: Destination) -> Self {
|
||||
Self::new(value.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Destination> for connector::ServerName {
|
||||
fn from(value: &Destination) -> Self {
|
||||
Self::new(&value.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RDCleanPathConfig {
|
||||
pub url: Url,
|
||||
pub auth_token: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DvcProxyInfo {
|
||||
pub channel_name: String,
|
||||
pub pipe_name: String,
|
||||
}
|
||||
|
||||
impl FromStr for DvcProxyInfo {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut parts = s.split('=');
|
||||
let channel_name = parts
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing DVC channel name"))?
|
||||
.to_owned();
|
||||
let pipe_name = parts
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing DVC proxy pipe name"))?
|
||||
.to_owned();
|
||||
|
||||
Ok(Self {
|
||||
channel_name,
|
||||
pipe_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Devolutions IronRDP client
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author = "Devolutions", about = "Devolutions-IronRDP client")]
|
||||
#[clap(version, long_about = None)]
|
||||
struct Args {
|
||||
/// A file with IronRDP client logs
|
||||
#[clap(short, long, value_parser)]
|
||||
log_file: Option<String>,
|
||||
|
||||
#[clap(long, value_parser)]
|
||||
gw_endpoint: Option<String>,
|
||||
#[clap(long, value_parser)]
|
||||
gw_user: Option<String>,
|
||||
#[clap(long, value_parser)]
|
||||
gw_pass: Option<String>,
|
||||
|
||||
/// An address on which the client will connect.
|
||||
destination: Option<Destination>,
|
||||
|
||||
/// Path to a .rdp file to read the configuration from.
|
||||
#[clap(long)]
|
||||
rdp_file: Option<PathBuf>,
|
||||
|
||||
/// A target RDP server user name
|
||||
#[clap(short, long)]
|
||||
username: Option<String>,
|
||||
|
||||
/// An optional target RDP server domain name
|
||||
#[clap(short, long)]
|
||||
domain: Option<String>,
|
||||
|
||||
/// A target RDP server user password
|
||||
#[clap(short, long)]
|
||||
password: Option<String>,
|
||||
|
||||
/// Proxy URL to connect to for the RDCleanPath
|
||||
#[clap(long, requires("rdcleanpath_token"))]
|
||||
rdcleanpath_url: Option<Url>,
|
||||
|
||||
/// Authentication token to insert in the RDCleanPath packet
|
||||
#[clap(long, requires("rdcleanpath_url"))]
|
||||
rdcleanpath_token: Option<String>,
|
||||
|
||||
/// The keyboard type
|
||||
#[clap(long, value_enum, default_value_t = KeyboardType::IbmEnhanced)]
|
||||
keyboard_type: KeyboardType,
|
||||
|
||||
/// The keyboard subtype (an original equipment manufacturer-dependent value)
|
||||
#[clap(long, default_value_t = 0)]
|
||||
keyboard_subtype: u32,
|
||||
|
||||
/// The number of function keys on the keyboard
|
||||
#[clap(long, default_value_t = 12)]
|
||||
keyboard_functional_keys_count: u32,
|
||||
|
||||
/// The input method editor (IME) file name associated with the active input locale
|
||||
#[clap(long, default_value_t = String::from(""))]
|
||||
ime_file_name: String,
|
||||
|
||||
/// Contains a value that uniquely identifies the client
|
||||
#[clap(long, default_value_t = String::from(""))]
|
||||
dig_product_id: String,
|
||||
|
||||
/// Enable thin client
|
||||
#[clap(long)]
|
||||
thin_client: bool,
|
||||
|
||||
/// Enable small cache
|
||||
#[clap(long)]
|
||||
small_cache: bool,
|
||||
|
||||
/// Set required color depth. Currently only 32 and 16 bit color depths are supported
|
||||
#[clap(long)]
|
||||
color_depth: Option<u32>,
|
||||
|
||||
/// Ignore mouse pointer messages sent by the server. Increases performance when enabled, as the
|
||||
/// client could skip costly software rendering of the pointer with alpha blending
|
||||
#[clap(long)]
|
||||
no_server_pointer: bool,
|
||||
|
||||
/// Enabled capability versions. Each bit represents enabling a capability version
|
||||
/// starting from V8 to V10_7
|
||||
#[clap(long, value_parser = parse_hex, default_value_t = 0)]
|
||||
capabilities: u32,
|
||||
|
||||
/// Automatically logon to the server by passing the INFO_AUTOLOGON flag
|
||||
///
|
||||
/// This flag is ignored if CredSSP authentication is used.
|
||||
/// You can use `--no-credssp` to ensure it’s not.
|
||||
#[clap(long)]
|
||||
autologon: bool,
|
||||
|
||||
/// Disable TLS + Graphical login (legacy authentication method)
|
||||
///
|
||||
/// Disabling this in order to enforce usage of CredSSP (NLA) is recommended.
|
||||
#[clap(long)]
|
||||
no_tls: bool,
|
||||
|
||||
/// Disable TLS + Network Level Authentication (NLA) using CredSSP
|
||||
///
|
||||
/// NLA is used to authenticates RDP clients and servers before sending credentials over the network.
|
||||
/// It’s not recommended to disable this.
|
||||
#[clap(long, alias = "no-nla")]
|
||||
no_credssp: bool,
|
||||
|
||||
/// The clipboard type
|
||||
#[clap(long, value_enum, default_value_t = ClipboardType::Default)]
|
||||
clipboard_type: ClipboardType,
|
||||
|
||||
/// The bitmap codecs to use (remotefx:on, ...)
|
||||
#[clap(long, num_args = 1.., value_delimiter = ',')]
|
||||
codecs: Vec<String>,
|
||||
|
||||
/// Add DVC channel named pipe proxy
|
||||
///
|
||||
/// The format is `<name>=<pipe>`, e.g., `ChannelName=PipeName` where `ChannelName` is the name of the channel,
|
||||
/// and `PipeName` is the name of the named pipe to connect to (without OS-specific prefix).
|
||||
/// `<pipe>` will automatically be prefixed with `\\.\pipe\` on Windows.
|
||||
#[clap(long)]
|
||||
dvc_proxy: Vec<DvcProxyInfo>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn parse_args() -> anyhow::Result<Self> {
|
||||
use ironrdp_cfg::PropertySetExt as _;
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
let mut properties = ironrdp_propertyset::PropertySet::new();
|
||||
|
||||
if let Some(rdp_file) = args.rdp_file {
|
||||
let input =
|
||||
std::fs::read_to_string(&rdp_file).with_context(|| format!("failed to read {}", rdp_file.display()))?;
|
||||
|
||||
if let Err(errors) = ironrdp_rdpfile::load(&mut properties, &input) {
|
||||
for e in errors {
|
||||
#[expect(clippy::print_stderr)]
|
||||
{
|
||||
eprintln!("Error when reading {}: {e}", rdp_file.display())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut gw: Option<GwConnectTarget> = None;
|
||||
if let Some(gw_addr) = args.gw_endpoint {
|
||||
gw = Some(GwConnectTarget {
|
||||
gw_endpoint: gw_addr,
|
||||
gw_user: String::new(),
|
||||
gw_pass: String::new(),
|
||||
server: String::new(), // TODO: non-standard port? also dont use here?
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(ref mut gw) = gw {
|
||||
gw.gw_user = if let Some(gw_user) = args.gw_user {
|
||||
gw_user
|
||||
} else {
|
||||
inquire::Text::new("Gateway username:")
|
||||
.prompt()
|
||||
.context("Username prompt")?
|
||||
};
|
||||
|
||||
gw.gw_pass = if let Some(gw_pass) = args.gw_pass {
|
||||
gw_pass
|
||||
} else {
|
||||
inquire::Password::new("Gateway password:")
|
||||
.without_confirmation()
|
||||
.prompt()
|
||||
.context("Password prompt")?
|
||||
};
|
||||
};
|
||||
|
||||
let destination = if let Some(destination) = args.destination {
|
||||
destination
|
||||
} else if let Some(destination) = properties.full_address() {
|
||||
if let Some(port) = properties.server_port() {
|
||||
format!("{destination}:{port}").parse()
|
||||
} else {
|
||||
destination.parse()
|
||||
}
|
||||
.context("invalid destination")?
|
||||
} else {
|
||||
inquire::Text::new("Server address:")
|
||||
.prompt()
|
||||
.context("Address prompt")?
|
||||
.pipe(Destination::new)?
|
||||
};
|
||||
|
||||
if let Some(ref mut gw) = gw {
|
||||
gw.server = destination.name.clone(); // TODO
|
||||
}
|
||||
|
||||
let username = if let Some(username) = args.username {
|
||||
username
|
||||
} else if let Some(username) = properties.username() {
|
||||
username.to_owned()
|
||||
} else {
|
||||
inquire::Text::new("Username:").prompt().context("Username prompt")?
|
||||
};
|
||||
|
||||
let password = if let Some(password) = args.password {
|
||||
password
|
||||
} else if let Some(password) = properties.clear_text_password() {
|
||||
password.to_owned()
|
||||
} else {
|
||||
inquire::Password::new("Password:")
|
||||
.without_confirmation()
|
||||
.prompt()
|
||||
.context("Password prompt")?
|
||||
};
|
||||
|
||||
let codecs: Vec<_> = args.codecs.iter().map(|s| s.as_str()).collect();
|
||||
let codecs = match client_codecs_capabilities(&codecs) {
|
||||
Ok(codecs) => codecs,
|
||||
Err(help) => {
|
||||
print!("{help}");
|
||||
std::process::exit(0);
|
||||
}
|
||||
};
|
||||
let mut bitmap = connector::BitmapConfig {
|
||||
color_depth: 32,
|
||||
lossy_compression: true,
|
||||
codecs,
|
||||
};
|
||||
|
||||
if let Some(color_depth) = args.color_depth {
|
||||
if color_depth != 16 && color_depth != 32 {
|
||||
anyhow::bail!("Invalid color depth. Only 16 and 32 bit color depths are supported.");
|
||||
}
|
||||
bitmap.color_depth = color_depth;
|
||||
};
|
||||
|
||||
let clipboard_type = if args.clipboard_type == ClipboardType::Default {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
ClipboardType::Windows
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
ClipboardType::None
|
||||
}
|
||||
} else {
|
||||
args.clipboard_type
|
||||
};
|
||||
|
||||
let connector = connector::Config {
|
||||
credentials: Credentials::UsernamePassword { username, password },
|
||||
domain: args.domain,
|
||||
enable_tls: !args.no_tls,
|
||||
enable_credssp: !args.no_credssp,
|
||||
keyboard_type: KeyboardType::parse(args.keyboard_type),
|
||||
keyboard_subtype: args.keyboard_subtype,
|
||||
keyboard_layout: 0, // the server SHOULD use the default active input locale identifier
|
||||
keyboard_functional_keys_count: args.keyboard_functional_keys_count,
|
||||
ime_file_name: args.ime_file_name,
|
||||
dig_product_id: args.dig_product_id,
|
||||
desktop_size: connector::DesktopSize {
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
},
|
||||
desktop_scale_factor: 0, // Default to 0 per FreeRDP
|
||||
bitmap: Some(bitmap),
|
||||
client_build: semver::Version::parse(env!("CARGO_PKG_VERSION"))
|
||||
.map_or(0, |version| version.major * 100 + version.minor * 10 + version.patch)
|
||||
.pipe(u32::try_from)
|
||||
.context("cargo package version")?,
|
||||
client_name: whoami::fallible::hostname().unwrap_or_else(|_| "ironrdp".to_owned()),
|
||||
// NOTE: hardcode this value like in freerdp
|
||||
// https://github.com/FreeRDP/FreeRDP/blob/4e24b966c86fdf494a782f0dfcfc43a057a2ea60/libfreerdp/core/settings.c#LL49C34-L49C70
|
||||
client_dir: "C:\\Windows\\System32\\mstscax.dll".to_owned(),
|
||||
platform: match whoami::platform() {
|
||||
whoami::Platform::Windows => MajorPlatformType::WINDOWS,
|
||||
whoami::Platform::Linux => MajorPlatformType::UNIX,
|
||||
whoami::Platform::MacOS => MajorPlatformType::MACINTOSH,
|
||||
whoami::Platform::Ios => MajorPlatformType::IOS,
|
||||
whoami::Platform::Android => MajorPlatformType::ANDROID,
|
||||
_ => MajorPlatformType::UNSPECIFIED,
|
||||
},
|
||||
hardware_id: None,
|
||||
license_cache: None,
|
||||
enable_server_pointer: !args.no_server_pointer,
|
||||
autologon: args.autologon,
|
||||
enable_audio_playback: true,
|
||||
request_data: None,
|
||||
pointer_software_rendering: false,
|
||||
performance_flags: PerformanceFlags::default(),
|
||||
timezone_info: TimezoneInfo::default(),
|
||||
};
|
||||
|
||||
let rdcleanpath = args
|
||||
.rdcleanpath_url
|
||||
.zip(args.rdcleanpath_token)
|
||||
.map(|(url, auth_token)| RDCleanPathConfig { url, auth_token });
|
||||
|
||||
Ok(Self {
|
||||
log_file: args.log_file,
|
||||
gw,
|
||||
destination,
|
||||
connector,
|
||||
clipboard_type,
|
||||
rdcleanpath,
|
||||
dvc_pipe_proxies: args.dvc_proxy,
|
||||
})
|
||||
}
|
||||
}
|
||||
17
crates/ironrdp-client/src/lib.rs
Normal file
17
crates/ironrdp-client/src/lib.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
|
||||
#![allow(unused_crate_dependencies)] // false positives because there is both a library and a binary
|
||||
|
||||
// No need to be as strict as in production libraries
|
||||
#![allow(clippy::arithmetic_side_effects)]
|
||||
#![allow(clippy::cast_lossless)]
|
||||
#![allow(clippy::cast_possible_truncation)]
|
||||
#![allow(clippy::cast_possible_wrap)]
|
||||
#![allow(clippy::cast_sign_loss)]
|
||||
|
||||
pub mod app;
|
||||
pub mod clipboard;
|
||||
pub mod config;
|
||||
pub mod rdp;
|
||||
|
||||
mod ws;
|
||||
122
crates/ironrdp-client/src/main.rs
Normal file
122
crates/ironrdp-client/src/main.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
#![allow(unused_crate_dependencies)] // false positives because there is both a library and a binary
|
||||
|
||||
use anyhow::Context as _;
|
||||
use ironrdp_client::app::App;
|
||||
use ironrdp_client::config::{ClipboardType, Config};
|
||||
use ironrdp_client::rdp::{DvcPipeProxyFactory, RdpClient, RdpInputEvent, RdpOutputEvent};
|
||||
use tokio::runtime;
|
||||
use tracing::debug;
|
||||
use winit::event_loop::EventLoop;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let mut config = Config::parse_args().context("CLI arguments parsing")?;
|
||||
|
||||
setup_logging(config.log_file.as_deref()).context("unable to initialize logging")?;
|
||||
|
||||
debug!("Initialize App");
|
||||
let event_loop = EventLoop::<RdpOutputEvent>::with_user_event().build()?;
|
||||
let event_loop_proxy = event_loop.create_proxy();
|
||||
let (input_event_sender, input_event_receiver) = RdpInputEvent::create_channel();
|
||||
let mut app = App::new(&event_loop, &input_event_sender).context("unable to initialize App")?;
|
||||
|
||||
// TODO: get window size & scale factor from GUI/App
|
||||
let window_size = (1024, 768);
|
||||
config.connector.desktop_scale_factor = 0;
|
||||
config.connector.desktop_size.width = window_size.0;
|
||||
config.connector.desktop_size.height = window_size.1;
|
||||
|
||||
let rt = runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.context("unable to create tokio runtime")?;
|
||||
|
||||
// NOTE: we need to keep `win_clipboard` alive, otherwise it will be dropped before IronRDP
|
||||
// starts and clipboard functionality will not be available.
|
||||
#[cfg(windows)]
|
||||
let _win_clipboard;
|
||||
|
||||
let cliprdr_factory = match config.clipboard_type {
|
||||
ClipboardType::Stub => {
|
||||
use ironrdp_cliprdr_native::StubClipboard;
|
||||
|
||||
let cliprdr = StubClipboard::new();
|
||||
let factory = cliprdr.backend_factory();
|
||||
Some(factory)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
ClipboardType::Windows => {
|
||||
use ironrdp_client::clipboard::ClientClipboardMessageProxy;
|
||||
use ironrdp_cliprdr_native::WinClipboard;
|
||||
|
||||
let cliprdr = WinClipboard::new(ClientClipboardMessageProxy::new(input_event_sender.clone()))?;
|
||||
|
||||
let factory = cliprdr.backend_factory();
|
||||
_win_clipboard = cliprdr;
|
||||
Some(factory)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let dvc_pipe_proxy_factory = DvcPipeProxyFactory::new(input_event_sender);
|
||||
|
||||
let client = RdpClient {
|
||||
config,
|
||||
event_loop_proxy,
|
||||
input_event_receiver,
|
||||
cliprdr_factory,
|
||||
dvc_pipe_proxy_factory,
|
||||
};
|
||||
|
||||
debug!("Start RDP thread");
|
||||
std::thread::spawn(move || {
|
||||
rt.block_on(client.run());
|
||||
});
|
||||
|
||||
debug!("Run App");
|
||||
event_loop.run_app(&mut app)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_logging(log_file: Option<&str>) -> anyhow::Result<()> {
|
||||
use std::fs::OpenOptions;
|
||||
|
||||
use tracing::metadata::LevelFilter;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::WARN.into())
|
||||
.with_env_var("IRONRDP_LOG")
|
||||
.from_env_lossy();
|
||||
|
||||
if let Some(log_file) = log_file {
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(log_file)
|
||||
.with_context(|| format!("couldn't open {log_file}"))?;
|
||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||
.with_ansi(false)
|
||||
.with_writer(file)
|
||||
.compact();
|
||||
tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(fmt_layer)
|
||||
.try_init()
|
||||
.context("failed to set tracing global subscriber")?;
|
||||
} else {
|
||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||
.compact()
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_thread_ids(true)
|
||||
.with_target(false);
|
||||
tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(fmt_layer)
|
||||
.try_init()
|
||||
.context("failed to set tracing global subscriber")?;
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
691
crates/ironrdp-client/src/rdp.rs
Normal file
691
crates/ironrdp-client/src/rdp.rs
Normal file
|
|
@ -0,0 +1,691 @@
|
|||
use core::num::NonZeroU16;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ironrdp::cliprdr::backend::{ClipboardMessage, CliprdrBackendFactory};
|
||||
use ironrdp::connector::connection_activation::ConnectionActivationState;
|
||||
use ironrdp::connector::{ConnectionResult, ConnectorResult};
|
||||
use ironrdp::displaycontrol::client::DisplayControlClient;
|
||||
use ironrdp::displaycontrol::pdu::MonitorLayoutEntry;
|
||||
use ironrdp::graphics::image_processing::PixelFormat;
|
||||
use ironrdp::graphics::pointer::DecodedPointer;
|
||||
use ironrdp::pdu::input::fast_path::FastPathInputEvent;
|
||||
use ironrdp::pdu::{pdu_other_err, PduResult};
|
||||
use ironrdp::session::image::DecodedImage;
|
||||
use ironrdp::session::{fast_path, ActiveStage, ActiveStageOutput, GracefulDisconnectReason, SessionResult};
|
||||
use ironrdp::svc::SvcMessage;
|
||||
use ironrdp::{cliprdr, connector, rdpdr, rdpsnd, session};
|
||||
use ironrdp_core::WriteBuf;
|
||||
use ironrdp_dvc_pipe_proxy::DvcNamedPipeProxy;
|
||||
use ironrdp_rdpsnd_native::cpal;
|
||||
use ironrdp_tokio::reqwest::ReqwestNetworkClient;
|
||||
use ironrdp_tokio::{single_sequence_step_read, split_tokio_framed, FramedWrite};
|
||||
use rdpdr::NoopRdpdrBackend;
|
||||
use smallvec::SmallVec;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
use winit::event_loop::EventLoopProxy;
|
||||
|
||||
use crate::config::{Config, RDCleanPathConfig};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RdpOutputEvent {
|
||||
Image {
|
||||
buffer: Vec<u32>,
|
||||
width: NonZeroU16,
|
||||
height: NonZeroU16,
|
||||
},
|
||||
ConnectionFailure(connector::ConnectorError),
|
||||
PointerDefault,
|
||||
PointerHidden,
|
||||
PointerPosition {
|
||||
x: u16,
|
||||
y: u16,
|
||||
},
|
||||
PointerBitmap(Arc<DecodedPointer>),
|
||||
Terminated(SessionResult<GracefulDisconnectReason>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RdpInputEvent {
|
||||
Resize {
|
||||
width: u16,
|
||||
height: u16,
|
||||
scale_factor: u32,
|
||||
/// The physical size of the display in millimeters (width, height).
|
||||
physical_size: Option<(u32, u32)>,
|
||||
},
|
||||
FastPath(SmallVec<[FastPathInputEvent; 2]>),
|
||||
Close,
|
||||
Clipboard(ClipboardMessage),
|
||||
SendDvcMessages {
|
||||
channel_id: u32,
|
||||
messages: Vec<SvcMessage>,
|
||||
},
|
||||
}
|
||||
|
||||
impl RdpInputEvent {
|
||||
pub fn create_channel() -> (mpsc::UnboundedSender<Self>, mpsc::UnboundedReceiver<Self>) {
|
||||
mpsc::unbounded_channel()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DvcPipeProxyFactory {
|
||||
rdp_input_sender: mpsc::UnboundedSender<RdpInputEvent>,
|
||||
}
|
||||
|
||||
impl DvcPipeProxyFactory {
|
||||
pub fn new(rdp_input_sender: mpsc::UnboundedSender<RdpInputEvent>) -> Self {
|
||||
Self { rdp_input_sender }
|
||||
}
|
||||
|
||||
pub fn create(&self, channel_name: String, pipe_name: String) -> DvcNamedPipeProxy {
|
||||
let rdp_input_sender = self.rdp_input_sender.clone();
|
||||
|
||||
DvcNamedPipeProxy::new(&channel_name, &pipe_name, move |channel_id, messages| {
|
||||
rdp_input_sender
|
||||
.send(RdpInputEvent::SendDvcMessages { channel_id, messages })
|
||||
.map_err(|_error| pdu_other_err!("send DVC messages to the event loop",))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub type WriteDvcMessageFn = Box<dyn Fn(u32, SvcMessage) -> PduResult<()> + Send + 'static>;
|
||||
|
||||
pub struct RdpClient {
|
||||
pub config: Config,
|
||||
pub event_loop_proxy: EventLoopProxy<RdpOutputEvent>,
|
||||
pub input_event_receiver: mpsc::UnboundedReceiver<RdpInputEvent>,
|
||||
pub cliprdr_factory: Option<Box<dyn CliprdrBackendFactory + Send>>,
|
||||
pub dvc_pipe_proxy_factory: DvcPipeProxyFactory,
|
||||
}
|
||||
|
||||
impl RdpClient {
|
||||
pub async fn run(mut self) {
|
||||
loop {
|
||||
let (connection_result, framed) = if let Some(rdcleanpath) = self.config.rdcleanpath.as_ref() {
|
||||
match connect_ws(
|
||||
&self.config,
|
||||
rdcleanpath,
|
||||
self.cliprdr_factory.as_deref(),
|
||||
&self.dvc_pipe_proxy_factory,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
let _ = self.event_loop_proxy.send_event(RdpOutputEvent::ConnectionFailure(e));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match connect(
|
||||
&self.config,
|
||||
self.cliprdr_factory.as_deref(),
|
||||
&self.dvc_pipe_proxy_factory,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
let _ = self.event_loop_proxy.send_event(RdpOutputEvent::ConnectionFailure(e));
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match active_session(
|
||||
framed,
|
||||
connection_result,
|
||||
&self.event_loop_proxy,
|
||||
&mut self.input_event_receiver,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(RdpControlFlow::ReconnectWithNewSize { width, height }) => {
|
||||
self.config.connector.desktop_size.width = width;
|
||||
self.config.connector.desktop_size.height = height;
|
||||
}
|
||||
Ok(RdpControlFlow::TerminatedGracefully(reason)) => {
|
||||
let _ = self.event_loop_proxy.send_event(RdpOutputEvent::Terminated(Ok(reason)));
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = self.event_loop_proxy.send_event(RdpOutputEvent::Terminated(Err(e)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RdpControlFlow {
|
||||
ReconnectWithNewSize { width: u16, height: u16 },
|
||||
TerminatedGracefully(GracefulDisconnectReason),
|
||||
}
|
||||
|
||||
trait AsyncReadWrite: AsyncRead + AsyncWrite {}
|
||||
|
||||
impl<T> AsyncReadWrite for T where T: AsyncRead + AsyncWrite {}
|
||||
|
||||
type UpgradedFramed = ironrdp_tokio::TokioFramed<Box<dyn AsyncReadWrite + Unpin + Send + Sync>>;
|
||||
|
||||
async fn connect(
|
||||
config: &Config,
|
||||
cliprdr_factory: Option<&(dyn CliprdrBackendFactory + Send)>,
|
||||
dvc_pipe_proxy_factory: &DvcPipeProxyFactory,
|
||||
) -> ConnectorResult<(ConnectionResult, UpgradedFramed)> {
|
||||
let dest = format!("{}:{}", config.destination.name(), config.destination.port());
|
||||
|
||||
let (client_addr, stream) = if let Some(ref gw_config) = config.gw {
|
||||
let (gw, client_addr) = ironrdp_mstsgu::GwClient::connect(gw_config, &config.connector.client_name)
|
||||
.await
|
||||
.map_err(|e| connector::custom_err!("GW Connect", e))?;
|
||||
(client_addr, tokio_util::either::Either::Left(gw))
|
||||
} else {
|
||||
let stream = TcpStream::connect(dest)
|
||||
.await
|
||||
.map_err(|e| connector::custom_err!("TCP connect", e))?;
|
||||
let client_addr = stream
|
||||
.local_addr()
|
||||
.map_err(|e| connector::custom_err!("get socket local address", e))?;
|
||||
(client_addr, tokio_util::either::Either::Right(stream))
|
||||
};
|
||||
let mut framed = ironrdp_tokio::TokioFramed::new(stream);
|
||||
|
||||
let mut drdynvc =
|
||||
ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new())));
|
||||
|
||||
// Instantiate all DVC proxies
|
||||
for proxy in config.dvc_pipe_proxies.iter() {
|
||||
let channel_name = proxy.channel_name.clone();
|
||||
let pipe_name = proxy.pipe_name.clone();
|
||||
|
||||
trace!(%channel_name, %pipe_name, "Creating DVC proxy");
|
||||
|
||||
drdynvc = drdynvc.with_dynamic_channel(dvc_pipe_proxy_factory.create(channel_name, pipe_name));
|
||||
}
|
||||
|
||||
let mut connector = connector::ClientConnector::new(config.connector.clone(), client_addr)
|
||||
.with_static_channel(drdynvc)
|
||||
.with_static_channel(rdpsnd::client::Rdpsnd::new(Box::new(cpal::RdpsndBackend::new())))
|
||||
.with_static_channel(rdpdr::Rdpdr::new(Box::new(NoopRdpdrBackend {}), "IronRDP".to_owned()).with_smartcard(0));
|
||||
|
||||
if let Some(builder) = cliprdr_factory {
|
||||
let backend = builder.build_cliprdr_backend();
|
||||
|
||||
let cliprdr = cliprdr::Cliprdr::new(backend);
|
||||
|
||||
connector.attach_static_channel(cliprdr);
|
||||
}
|
||||
|
||||
let should_upgrade = ironrdp_tokio::connect_begin(&mut framed, &mut connector).await?;
|
||||
|
||||
debug!("TLS upgrade");
|
||||
|
||||
// Ensure there is no leftover
|
||||
let (initial_stream, leftover_bytes) = framed.into_inner();
|
||||
|
||||
let (upgraded_stream, tls_cert) = ironrdp_tls::upgrade(initial_stream, config.destination.name())
|
||||
.await
|
||||
.map_err(|e| connector::custom_err!("TLS upgrade", e))?;
|
||||
|
||||
let upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, &mut connector);
|
||||
|
||||
let erased_stream: Box<dyn AsyncReadWrite + Unpin + Send + Sync> = Box::new(upgraded_stream);
|
||||
let mut upgraded_framed = ironrdp_tokio::TokioFramed::new_with_leftover(erased_stream, leftover_bytes);
|
||||
|
||||
let server_public_key = ironrdp_tls::extract_tls_server_public_key(&tls_cert)
|
||||
.ok_or_else(|| connector::general_err!("unable to extract tls server public key"))?;
|
||||
let connection_result = ironrdp_tokio::connect_finalize(
|
||||
upgraded,
|
||||
connector,
|
||||
&mut upgraded_framed,
|
||||
&mut ReqwestNetworkClient::new(),
|
||||
(&config.destination).into(),
|
||||
server_public_key.to_owned(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
debug!(?connection_result);
|
||||
|
||||
Ok((connection_result, upgraded_framed))
|
||||
}
|
||||
|
||||
async fn connect_ws(
|
||||
config: &Config,
|
||||
rdcleanpath: &RDCleanPathConfig,
|
||||
cliprdr_factory: Option<&(dyn CliprdrBackendFactory + Send)>,
|
||||
dvc_pipe_proxy_factory: &DvcPipeProxyFactory,
|
||||
) -> ConnectorResult<(ConnectionResult, UpgradedFramed)> {
|
||||
let hostname = rdcleanpath
|
||||
.url
|
||||
.host_str()
|
||||
.ok_or_else(|| connector::general_err!("host missing from the URL"))?;
|
||||
|
||||
let port = rdcleanpath.url.port_or_known_default().unwrap_or(443);
|
||||
|
||||
let socket = TcpStream::connect((hostname, port))
|
||||
.await
|
||||
.map_err(|e| connector::custom_err!("TCP connect", e))?;
|
||||
|
||||
socket
|
||||
.set_nodelay(true)
|
||||
.map_err(|e| connector::custom_err!("set TCP_NODELAY", e))?;
|
||||
|
||||
let client_addr = socket
|
||||
.local_addr()
|
||||
.map_err(|e| connector::custom_err!("get socket local address", e))?;
|
||||
|
||||
let (ws, _) = tokio_tungstenite::client_async_tls(rdcleanpath.url.as_str(), socket)
|
||||
.await
|
||||
.map_err(|e| connector::custom_err!("WS connect", e))?;
|
||||
|
||||
let ws = crate::ws::websocket_compat(ws);
|
||||
|
||||
let mut framed = ironrdp_tokio::TokioFramed::new(ws);
|
||||
|
||||
let mut drdynvc =
|
||||
ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new())));
|
||||
|
||||
// Instantiate all DVC proxies
|
||||
for proxy in config.dvc_pipe_proxies.iter() {
|
||||
let channel_name = proxy.channel_name.clone();
|
||||
let pipe_name = proxy.pipe_name.clone();
|
||||
|
||||
trace!(%channel_name, %pipe_name, "Creating DVC proxy");
|
||||
|
||||
drdynvc = drdynvc.with_dynamic_channel(dvc_pipe_proxy_factory.create(channel_name, pipe_name));
|
||||
}
|
||||
|
||||
let mut connector = connector::ClientConnector::new(config.connector.clone(), client_addr)
|
||||
.with_static_channel(drdynvc)
|
||||
.with_static_channel(rdpsnd::client::Rdpsnd::new(Box::new(cpal::RdpsndBackend::new())))
|
||||
.with_static_channel(rdpdr::Rdpdr::new(Box::new(NoopRdpdrBackend {}), "IronRDP".to_owned()).with_smartcard(0));
|
||||
|
||||
if let Some(builder) = cliprdr_factory {
|
||||
let backend = builder.build_cliprdr_backend();
|
||||
|
||||
let cliprdr = cliprdr::Cliprdr::new(backend);
|
||||
|
||||
connector.attach_static_channel(cliprdr);
|
||||
}
|
||||
|
||||
let destination = format!("{}:{}", config.destination.name(), config.destination.port());
|
||||
|
||||
let (upgraded, server_public_key) = connect_rdcleanpath(
|
||||
&mut framed,
|
||||
&mut connector,
|
||||
destination,
|
||||
rdcleanpath.auth_token.clone(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let connection_result = ironrdp_tokio::connect_finalize(
|
||||
upgraded,
|
||||
connector,
|
||||
&mut framed,
|
||||
&mut ReqwestNetworkClient::new(),
|
||||
(&config.destination).into(),
|
||||
server_public_key,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (ws, leftover_bytes) = framed.into_inner();
|
||||
let erased_stream: Box<dyn AsyncReadWrite + Unpin + Send + Sync> = Box::new(ws);
|
||||
let upgraded_framed = ironrdp_tokio::TokioFramed::new_with_leftover(erased_stream, leftover_bytes);
|
||||
|
||||
Ok((connection_result, upgraded_framed))
|
||||
}
|
||||
|
||||
async fn connect_rdcleanpath<S>(
|
||||
framed: &mut ironrdp_tokio::Framed<S>,
|
||||
connector: &mut connector::ClientConnector,
|
||||
destination: String,
|
||||
proxy_auth_token: String,
|
||||
pcb: Option<String>,
|
||||
) -> ConnectorResult<(ironrdp_tokio::Upgraded, Vec<u8>)>
|
||||
where
|
||||
S: ironrdp_tokio::FramedRead + FramedWrite,
|
||||
{
|
||||
use ironrdp::connector::Sequence as _;
|
||||
use x509_cert::der::Decode as _;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct RDCleanPathHint;
|
||||
|
||||
const RDCLEANPATH_HINT: RDCleanPathHint = RDCleanPathHint;
|
||||
|
||||
impl ironrdp::pdu::PduHint for RDCleanPathHint {
|
||||
fn find_size(&self, bytes: &[u8]) -> ironrdp::core::DecodeResult<Option<(bool, usize)>> {
|
||||
match ironrdp_rdcleanpath::RDCleanPathPdu::detect(bytes) {
|
||||
ironrdp_rdcleanpath::DetectionResult::Detected { total_length, .. } => Ok(Some((true, total_length))),
|
||||
ironrdp_rdcleanpath::DetectionResult::NotEnoughBytes => Ok(None),
|
||||
ironrdp_rdcleanpath::DetectionResult::Failed => Err(ironrdp::core::other_err!(
|
||||
"RDCleanPathHint",
|
||||
"detection failed (invalid PDU)"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut buf = WriteBuf::new();
|
||||
|
||||
info!("Begin connection procedure");
|
||||
|
||||
{
|
||||
// RDCleanPath request
|
||||
|
||||
let connector::ClientConnectorState::ConnectionInitiationSendRequest = connector.state else {
|
||||
return Err(connector::general_err!("invalid connector state (send request)"));
|
||||
};
|
||||
|
||||
debug_assert!(connector.next_pdu_hint().is_none());
|
||||
|
||||
let written = connector.step_no_input(&mut buf)?;
|
||||
let x224_pdu_len = written.size().expect("written size");
|
||||
debug_assert_eq!(x224_pdu_len, buf.filled_len());
|
||||
let x224_pdu = buf.filled().to_vec();
|
||||
|
||||
let rdcleanpath_req =
|
||||
ironrdp_rdcleanpath::RDCleanPathPdu::new_request(x224_pdu, destination, proxy_auth_token, pcb)
|
||||
.map_err(|e| connector::custom_err!("new RDCleanPath request", e))?;
|
||||
debug!(message = ?rdcleanpath_req, "Send RDCleanPath request");
|
||||
let rdcleanpath_req = rdcleanpath_req
|
||||
.to_der()
|
||||
.map_err(|e| connector::custom_err!("RDCleanPath request encode", e))?;
|
||||
|
||||
framed
|
||||
.write_all(&rdcleanpath_req)
|
||||
.await
|
||||
.map_err(|e| connector::custom_err!("couldn't write RDCleanPath request", e))?;
|
||||
}
|
||||
|
||||
{
|
||||
// RDCleanPath response
|
||||
|
||||
let rdcleanpath_res = framed
|
||||
.read_by_hint(&RDCLEANPATH_HINT)
|
||||
.await
|
||||
.map_err(|e| connector::custom_err!("read RDCleanPath request", e))?;
|
||||
|
||||
let rdcleanpath_res = ironrdp_rdcleanpath::RDCleanPathPdu::from_der(&rdcleanpath_res)
|
||||
.map_err(|e| connector::custom_err!("RDCleanPath response decode", e))?;
|
||||
|
||||
debug!(message = ?rdcleanpath_res, "Received RDCleanPath PDU");
|
||||
|
||||
let (x224_connection_response, server_cert_chain) = match rdcleanpath_res
|
||||
.into_enum()
|
||||
.map_err(|e| connector::custom_err!("invalid RDCleanPath PDU", e))?
|
||||
{
|
||||
ironrdp_rdcleanpath::RDCleanPath::Request { .. } => {
|
||||
return Err(connector::general_err!(
|
||||
"received an unexpected RDCleanPath type (request)",
|
||||
));
|
||||
}
|
||||
ironrdp_rdcleanpath::RDCleanPath::Response {
|
||||
x224_connection_response,
|
||||
server_cert_chain,
|
||||
server_addr: _,
|
||||
} => (x224_connection_response, server_cert_chain),
|
||||
ironrdp_rdcleanpath::RDCleanPath::GeneralErr(error) => {
|
||||
return Err(connector::custom_err!("received an RDCleanPath error", error));
|
||||
}
|
||||
ironrdp_rdcleanpath::RDCleanPath::NegotiationErr {
|
||||
x224_connection_response,
|
||||
} => {
|
||||
// Try to decode as X.224 Connection Confirm to extract negotiation failure details.
|
||||
if let Ok(x224_confirm) = ironrdp_core::decode::<
|
||||
ironrdp::pdu::x224::X224<ironrdp::pdu::nego::ConnectionConfirm>,
|
||||
>(&x224_connection_response)
|
||||
{
|
||||
if let ironrdp::pdu::nego::ConnectionConfirm::Failure { code } = x224_confirm.0 {
|
||||
// Convert to negotiation failure instead of generic RDCleanPath error.
|
||||
let negotiation_failure = connector::NegotiationFailure::from(code);
|
||||
return Err(connector::ConnectorError::new(
|
||||
"RDP negotiation failed",
|
||||
connector::ConnectorErrorKind::Negotiation(negotiation_failure),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to generic error if we can't decode the negotiation failure.
|
||||
return Err(connector::general_err!("received an RDCleanPath negotiation error"));
|
||||
}
|
||||
};
|
||||
|
||||
let connector::ClientConnectorState::ConnectionInitiationWaitConfirm { .. } = connector.state else {
|
||||
return Err(connector::general_err!("invalid connector state (wait confirm)"));
|
||||
};
|
||||
|
||||
debug_assert!(connector.next_pdu_hint().is_some());
|
||||
|
||||
buf.clear();
|
||||
let written = connector.step(x224_connection_response.as_bytes(), &mut buf)?;
|
||||
|
||||
debug_assert!(written.is_nothing());
|
||||
|
||||
let server_cert = server_cert_chain
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| connector::general_err!("server cert chain missing from rdcleanpath response"))?;
|
||||
|
||||
let cert = x509_cert::Certificate::from_der(server_cert.as_bytes())
|
||||
.map_err(|e| connector::custom_err!("server cert chain missing from rdcleanpath response", e))?;
|
||||
|
||||
let server_public_key = cert
|
||||
.tbs_certificate
|
||||
.subject_public_key_info
|
||||
.subject_public_key
|
||||
.as_bytes()
|
||||
.ok_or_else(|| connector::general_err!("subject public key BIT STRING is not aligned"))?
|
||||
.to_owned();
|
||||
|
||||
let should_upgrade = ironrdp_tokio::skip_connect_begin(connector);
|
||||
|
||||
// At this point, proxy established the TLS session.
|
||||
|
||||
let upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, connector);
|
||||
|
||||
Ok((upgraded, server_public_key))
|
||||
}
|
||||
}
|
||||
|
||||
async fn active_session(
|
||||
framed: UpgradedFramed,
|
||||
connection_result: ConnectionResult,
|
||||
event_loop_proxy: &EventLoopProxy<RdpOutputEvent>,
|
||||
input_event_receiver: &mut mpsc::UnboundedReceiver<RdpInputEvent>,
|
||||
) -> SessionResult<RdpControlFlow> {
|
||||
let (mut reader, mut writer) = split_tokio_framed(framed);
|
||||
let mut image = DecodedImage::new(
|
||||
PixelFormat::RgbA32,
|
||||
connection_result.desktop_size.width,
|
||||
connection_result.desktop_size.height,
|
||||
);
|
||||
|
||||
let mut active_stage = ActiveStage::new(connection_result);
|
||||
|
||||
let disconnect_reason = 'outer: loop {
|
||||
let outputs = tokio::select! {
|
||||
frame = reader.read_pdu() => {
|
||||
let (action, payload) = frame.map_err(|e| session::custom_err!("read frame", e))?;
|
||||
trace!(?action, frame_length = payload.len(), "Frame received");
|
||||
|
||||
active_stage.process(&mut image, action, &payload)?
|
||||
}
|
||||
input_event = input_event_receiver.recv() => {
|
||||
let input_event = input_event.ok_or_else(|| session::general_err!("GUI is stopped"))?;
|
||||
|
||||
match input_event {
|
||||
RdpInputEvent::Resize { width, height, scale_factor, physical_size } => {
|
||||
trace!(width, height, "Resize event");
|
||||
let width = u32::from(width);
|
||||
let height = u32::from(height);
|
||||
// TODO: Make adjust_display_size take and return width and height as u16.
|
||||
// From the function's doc comment, the width and height values must be less than or equal to 8192 pixels.
|
||||
// Therefore, we can remove unnecessary casts from u16 to u32 and back.
|
||||
let (width, height) = MonitorLayoutEntry::adjust_display_size(width, height);
|
||||
debug!(width, height, "Adjusted display size");
|
||||
if let Some(response_frame) = active_stage.encode_resize(width, height, Some(scale_factor), physical_size) {
|
||||
vec![ActiveStageOutput::ResponseFrame(response_frame?)]
|
||||
} else {
|
||||
// TODO(#271): use the "auto-reconnect cookie": https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/15b0d1c9-2891-4adb-a45e-deb4aeeeab7c
|
||||
debug!("Reconnecting with new size");
|
||||
let width = u16::try_from(width).expect("always in the range");
|
||||
let height = u16::try_from(height).expect("always in the range");
|
||||
return Ok(RdpControlFlow::ReconnectWithNewSize { width, height })
|
||||
}
|
||||
},
|
||||
RdpInputEvent::FastPath(events) => {
|
||||
trace!(?events);
|
||||
active_stage.process_fastpath_input(&mut image, &events)?
|
||||
}
|
||||
RdpInputEvent::Close => {
|
||||
active_stage.graceful_shutdown()?
|
||||
}
|
||||
RdpInputEvent::Clipboard(event) => {
|
||||
if let Some(cliprdr) = active_stage.get_svc_processor::<cliprdr::CliprdrClient>() {
|
||||
if let Some(svc_messages) = match event {
|
||||
ClipboardMessage::SendInitiateCopy(formats) => {
|
||||
Some(cliprdr.initiate_copy(&formats)
|
||||
.map_err(|e| session::custom_err!("CLIPRDR", e))?)
|
||||
}
|
||||
ClipboardMessage::SendFormatData(response) => {
|
||||
Some(cliprdr.submit_format_data(response)
|
||||
.map_err(|e| session::custom_err!("CLIPRDR", e))?)
|
||||
}
|
||||
ClipboardMessage::SendInitiatePaste(format) => {
|
||||
Some(cliprdr.initiate_paste(format)
|
||||
.map_err(|e| session::custom_err!("CLIPRDR", e))?)
|
||||
}
|
||||
ClipboardMessage::Error(e) => {
|
||||
error!("Clipboard backend error: {}", e);
|
||||
None
|
||||
}
|
||||
} {
|
||||
let frame = active_stage.process_svc_processor_messages(svc_messages)?;
|
||||
// Send the messages to the server
|
||||
vec![ActiveStageOutput::ResponseFrame(frame)]
|
||||
} else {
|
||||
// No messages to send to the server
|
||||
Vec::new()
|
||||
}
|
||||
} else {
|
||||
warn!("Clipboard event received, but Cliprdr is not available");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
RdpInputEvent::SendDvcMessages { channel_id, messages } => {
|
||||
trace!(channel_id, ?messages, "Send DVC messages");
|
||||
|
||||
let frame = active_stage.encode_dvc_messages(messages)?;
|
||||
vec![ActiveStageOutput::ResponseFrame(frame)]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for out in outputs {
|
||||
match out {
|
||||
ActiveStageOutput::ResponseFrame(frame) => writer
|
||||
.write_all(&frame)
|
||||
.await
|
||||
.map_err(|e| session::custom_err!("write response", e))?,
|
||||
ActiveStageOutput::GraphicsUpdate(_region) => {
|
||||
let buffer: Vec<u32> = image
|
||||
.data()
|
||||
.chunks_exact(4)
|
||||
.map(|pixel| {
|
||||
let r = pixel[0];
|
||||
let g = pixel[1];
|
||||
let b = pixel[2];
|
||||
u32::from_be_bytes([0, r, g, b])
|
||||
})
|
||||
.collect();
|
||||
|
||||
event_loop_proxy
|
||||
.send_event(RdpOutputEvent::Image {
|
||||
buffer,
|
||||
width: NonZeroU16::new(image.width())
|
||||
.ok_or_else(|| session::general_err!("width is zero"))?,
|
||||
height: NonZeroU16::new(image.height())
|
||||
.ok_or_else(|| session::general_err!("height is zero"))?,
|
||||
})
|
||||
.map_err(|e| session::custom_err!("event_loop_proxy", e))?;
|
||||
}
|
||||
ActiveStageOutput::PointerDefault => {
|
||||
event_loop_proxy
|
||||
.send_event(RdpOutputEvent::PointerDefault)
|
||||
.map_err(|e| session::custom_err!("event_loop_proxy", e))?;
|
||||
}
|
||||
ActiveStageOutput::PointerHidden => {
|
||||
event_loop_proxy
|
||||
.send_event(RdpOutputEvent::PointerHidden)
|
||||
.map_err(|e| session::custom_err!("event_loop_proxy", e))?;
|
||||
}
|
||||
ActiveStageOutput::PointerPosition { x, y } => {
|
||||
event_loop_proxy
|
||||
.send_event(RdpOutputEvent::PointerPosition { x, y })
|
||||
.map_err(|e| session::custom_err!("event_loop_proxy", e))?;
|
||||
}
|
||||
ActiveStageOutput::PointerBitmap(pointer) => {
|
||||
event_loop_proxy
|
||||
.send_event(RdpOutputEvent::PointerBitmap(pointer))
|
||||
.map_err(|e| session::custom_err!("event_loop_proxy", e))?;
|
||||
}
|
||||
ActiveStageOutput::DeactivateAll(mut connection_activation) => {
|
||||
// Execute the Deactivation-Reactivation Sequence:
|
||||
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432
|
||||
debug!("Received Server Deactivate All PDU, executing Deactivation-Reactivation Sequence");
|
||||
let mut buf = WriteBuf::new();
|
||||
'activation_seq: loop {
|
||||
let written = single_sequence_step_read(&mut reader, &mut *connection_activation, &mut buf)
|
||||
.await
|
||||
.map_err(|e| session::custom_err!("read deactivation-reactivation sequence step", e))?;
|
||||
|
||||
if written.size().is_some() {
|
||||
writer.write_all(buf.filled()).await.map_err(|e| {
|
||||
session::custom_err!("write deactivation-reactivation sequence step", e)
|
||||
})?;
|
||||
}
|
||||
|
||||
if let ConnectionActivationState::Finalized {
|
||||
io_channel_id,
|
||||
user_channel_id,
|
||||
desktop_size,
|
||||
enable_server_pointer,
|
||||
pointer_software_rendering,
|
||||
} = connection_activation.connection_activation_state()
|
||||
{
|
||||
debug!(?desktop_size, "Deactivation-Reactivation Sequence completed");
|
||||
// Update image size with the new desktop size.
|
||||
image = DecodedImage::new(PixelFormat::RgbA32, desktop_size.width, desktop_size.height);
|
||||
// Update the active stage with the new channel IDs and pointer settings.
|
||||
active_stage.set_fastpath_processor(
|
||||
fast_path::ProcessorBuilder {
|
||||
io_channel_id,
|
||||
user_channel_id,
|
||||
enable_server_pointer,
|
||||
pointer_software_rendering,
|
||||
}
|
||||
.build(),
|
||||
);
|
||||
active_stage.set_enable_server_pointer(enable_server_pointer);
|
||||
break 'activation_seq;
|
||||
}
|
||||
}
|
||||
}
|
||||
ActiveStageOutput::Terminate(reason) => break 'outer reason,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(RdpControlFlow::TerminatedGracefully(disconnect_reason))
|
||||
}
|
||||
34
crates/ironrdp-client/src/ws.rs
Normal file
34
crates/ironrdp-client/src/ws.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use futures_util::{Sink, SinkExt as _, Stream, StreamExt as _};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_tungstenite::tungstenite;
|
||||
|
||||
pub(crate) fn websocket_compat<S>(stream: S) -> impl AsyncRead + AsyncWrite + Unpin + Send + 'static
|
||||
where
|
||||
S: Stream<Item = Result<tungstenite::Message, tungstenite::Error>>
|
||||
+ Sink<tungstenite::Message, Error = tungstenite::Error>
|
||||
+ Unpin
|
||||
+ Send
|
||||
+ 'static,
|
||||
{
|
||||
let compat = stream
|
||||
.filter_map(|item| {
|
||||
let mapped = item
|
||||
.map(|msg| match msg {
|
||||
tungstenite::Message::Text(s) => Some(transport::WsReadMsg::Payload(tungstenite::Bytes::from(s))),
|
||||
tungstenite::Message::Binary(data) => Some(transport::WsReadMsg::Payload(data)),
|
||||
tungstenite::Message::Ping(_) | tungstenite::Message::Pong(_) => None,
|
||||
tungstenite::Message::Close(_) => Some(transport::WsReadMsg::Close),
|
||||
tungstenite::Message::Frame(_) => unreachable!("raw frames are never returned when reading"),
|
||||
})
|
||||
.transpose();
|
||||
|
||||
core::future::ready(mapped)
|
||||
})
|
||||
.with(|item| {
|
||||
core::future::ready(Ok::<_, tungstenite::Error>(tungstenite::Message::Binary(
|
||||
tungstenite::Bytes::from(item),
|
||||
)))
|
||||
});
|
||||
|
||||
transport::WsStream::new(compat)
|
||||
}
|
||||
26
crates/ironrdp-cliprdr-format/CHANGELOG.md
Normal file
26
crates/ironrdp-cliprdr-format/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [[0.1.4](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-format-v0.1.3...ironrdp-cliprdr-format-v0.1.4)] - 2025-09-04
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump png from 0.17.16 to 0.18.0 (#961) ([21fa028dff](https://github.com/Devolutions/IronRDP/commit/21fa028dffa5f9bb1498b4d48d063ea42929faf5))
|
||||
|
||||
## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-format-v0.1.2...ironrdp-cliprdr-format-v0.1.3)] - 2025-03-12
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa))
|
||||
|
||||
## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-format-v0.1.1...ironrdp-cliprdr-format-v0.1.2)] - 2025-01-28
|
||||
|
||||
### <!-- 6 -->Documentation
|
||||
|
||||
- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b))
|
||||
|
||||
23
crates/ironrdp-cliprdr-format/Cargo.toml
Normal file
23
crates/ironrdp-cliprdr-format/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "ironrdp-cliprdr-format"
|
||||
version = "0.1.4"
|
||||
readme = "README.md"
|
||||
description = "CLIPRDR format conversion library"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["std"] } # public
|
||||
png = "0.18"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
1
crates/ironrdp-cliprdr-format/LICENSE-APACHE
Symbolic link
1
crates/ironrdp-cliprdr-format/LICENSE-APACHE
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-APACHE
|
||||
1
crates/ironrdp-cliprdr-format/LICENSE-MIT
Symbolic link
1
crates/ironrdp-cliprdr-format/LICENSE-MIT
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-MIT
|
||||
16
crates/ironrdp-cliprdr-format/README.md
Normal file
16
crates/ironrdp-cliprdr-format/README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# IronRDP CLIPRDR formats decoding/encoding library
|
||||
|
||||
This Library provides the conversion logic between RDP-specific clipboard formats and
|
||||
widely used formats like PNG for images, plain string for HTML etc.
|
||||
|
||||
### Overflows
|
||||
|
||||
This crate has been audited by us and is guaranteed overflow-free on 32 and 64 bits architectures.
|
||||
It would be easy to cause an overflow on a 16-bit architecture.
|
||||
However, it’s hard to imagine an RDP client running on such machines.
|
||||
Size of pointers on such architectures greatly limits the maximum size of the bitmap buffers.
|
||||
It’s likely the RDP client will choke on a big payload before overflowing because of this crate.
|
||||
|
||||
This crate is part of the [IronRDP] project.
|
||||
|
||||
[IronRDP]: https://github.com/Devolutions/IronRDP
|
||||
839
crates/ironrdp-cliprdr-format/src/bitmap.rs
Normal file
839
crates/ironrdp-cliprdr-format/src/bitmap.rs
Normal file
|
|
@ -0,0 +1,839 @@
|
|||
use std::io::Cursor;
|
||||
|
||||
use ironrdp_core::{
|
||||
cast_int, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor,
|
||||
WriteCursor,
|
||||
};
|
||||
|
||||
/// Maximum size of PNG image that could be placed on the clipboard.
|
||||
const MAX_BUFFER_SIZE: usize = 64 * 1024 * 1024; // 64 MB
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BitmapError {
|
||||
Decode(ironrdp_core::DecodeError),
|
||||
Encode(ironrdp_core::EncodeError),
|
||||
Unsupported(&'static str),
|
||||
InvalidSize,
|
||||
BufferTooBig,
|
||||
WidthTooBig,
|
||||
HeightTooBig,
|
||||
PngEncode(png::EncodingError),
|
||||
PngDecode(png::DecodingError),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for BitmapError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
BitmapError::Decode(_error) => write!(f, "decoding error"),
|
||||
BitmapError::Encode(_error) => write!(f, "encoding error"),
|
||||
BitmapError::Unsupported(s) => write!(f, "unsupported bitmap: {s}"),
|
||||
BitmapError::InvalidSize => write!(f, "one of bitmap's dimensions is invalid"),
|
||||
BitmapError::BufferTooBig => write!(f, "buffer size required for allocation is too big"),
|
||||
BitmapError::WidthTooBig => write!(f, "image width is too big"),
|
||||
BitmapError::HeightTooBig => write!(f, "image height is too big"),
|
||||
BitmapError::PngEncode(_error) => write!(f, "PNG encoding error"),
|
||||
BitmapError::PngDecode(_error) => write!(f, "PNG decoding error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for BitmapError {
|
||||
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
|
||||
match self {
|
||||
BitmapError::Decode(error) => Some(error),
|
||||
BitmapError::Encode(error) => Some(error),
|
||||
BitmapError::Unsupported(_) => None,
|
||||
BitmapError::InvalidSize => None,
|
||||
BitmapError::BufferTooBig => None,
|
||||
BitmapError::WidthTooBig => None,
|
||||
BitmapError::HeightTooBig => None,
|
||||
BitmapError::PngEncode(encoding_error) => Some(encoding_error),
|
||||
BitmapError::PngDecode(decoding_error) => Some(decoding_error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<png::EncodingError> for BitmapError {
|
||||
fn from(error: png::EncodingError) -> Self {
|
||||
BitmapError::PngEncode(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<png::DecodingError> for BitmapError {
|
||||
fn from(error: png::DecodingError) -> Self {
|
||||
BitmapError::PngDecode(error)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
struct BitmapCompression(u32);
|
||||
|
||||
#[expect(dead_code)]
|
||||
impl BitmapCompression {
|
||||
const RGB: Self = Self(0x0000);
|
||||
const RLE8: Self = Self(0x0001);
|
||||
const RLE4: Self = Self(0x0002);
|
||||
const BITFIELDS: Self = Self(0x0003);
|
||||
const JPEG: Self = Self(0x0004);
|
||||
const PNG: Self = Self(0x0005);
|
||||
const CMYK: Self = Self(0x000B);
|
||||
const CMYKRLE8: Self = Self(0x000C);
|
||||
const CMYKRLE4: Self = Self(0x000D);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
struct ColorSpace(u32);
|
||||
|
||||
#[expect(dead_code)]
|
||||
impl ColorSpace {
|
||||
const CALIBRATED_RGB: Self = Self(0x00000000);
|
||||
const SRGB: Self = Self(0x73524742);
|
||||
const WINDOWS: Self = Self(0x57696E20);
|
||||
const PROFILE_LINKED: Self = Self(0x4C494E4B);
|
||||
const PROFILE_EMBEDDED: Self = Self(0x4D424544);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
struct BitmapIntent(u32);
|
||||
|
||||
#[expect(dead_code)]
|
||||
impl BitmapIntent {
|
||||
const LCS_GM_ABS_COLORIMETRIC: Self = Self(0x00000008);
|
||||
const LCS_GM_BUSINESS: Self = Self(0x00000001);
|
||||
const LCS_GM_GRAPHICS: Self = Self(0x00000002);
|
||||
const LCS_GM_IMAGES: Self = Self(0x00000004);
|
||||
}
|
||||
|
||||
type Fxpt2Dot30 = u32; // (LONG)
|
||||
|
||||
#[derive(Default)]
|
||||
struct Ciexyz {
|
||||
x: Fxpt2Dot30,
|
||||
y: Fxpt2Dot30,
|
||||
z: Fxpt2Dot30,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CiexyzTriple {
|
||||
red: Ciexyz,
|
||||
green: Ciexyz,
|
||||
blue: Ciexyz,
|
||||
}
|
||||
|
||||
impl CiexyzTriple {
|
||||
const NAME: &'static str = "CIEXYZTRIPLE";
|
||||
const FIXED_PART_SIZE: usize = 4 * 3 * 3; // 4(LONG) * 3(xyz) * 3(red, green, blue)
|
||||
}
|
||||
|
||||
impl<'a> Decode<'a> for CiexyzTriple {
|
||||
fn decode(src: &mut ReadCursor<'a>) -> DecodeResult<Self> {
|
||||
ensure_fixed_part_size!(in: src);
|
||||
|
||||
let red = Ciexyz {
|
||||
x: src.read_u32(),
|
||||
y: src.read_u32(),
|
||||
z: src.read_u32(),
|
||||
};
|
||||
|
||||
let green = Ciexyz {
|
||||
x: src.read_u32(),
|
||||
y: src.read_u32(),
|
||||
z: src.read_u32(),
|
||||
};
|
||||
|
||||
let blue = Ciexyz {
|
||||
x: src.read_u32(),
|
||||
y: src.read_u32(),
|
||||
z: src.read_u32(),
|
||||
};
|
||||
|
||||
Ok(Self { red, green, blue })
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for CiexyzTriple {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
|
||||
ensure_fixed_part_size!(in: dst);
|
||||
|
||||
dst.write_u32(self.red.x);
|
||||
dst.write_u32(self.red.y);
|
||||
dst.write_u32(self.red.z);
|
||||
|
||||
dst.write_u32(self.green.x);
|
||||
dst.write_u32(self.green.y);
|
||||
dst.write_u32(self.green.z);
|
||||
|
||||
dst.write_u32(self.blue.x);
|
||||
dst.write_u32(self.blue.y);
|
||||
dst.write_u32(self.blue.z);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
Self::FIXED_PART_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
/// Header used in `CF_DIB` formats, part of [BITMAPINFO]
|
||||
///
|
||||
/// We don't use the optional `bmiColors` field, because it is only relevant for bitmaps with
|
||||
/// bpp < 24, which are not supported yet, therefore only fixed part of the header is implemented.
|
||||
///
|
||||
/// [BITMAPINFO]: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct BitmapInfoHeader {
|
||||
/// INVARIANT: `width.abs() <= 10_000`
|
||||
width: i32,
|
||||
/// INVARIANT: `height.abs() <= 10_000`
|
||||
height: i32,
|
||||
/// INVARIANT: `bit_count <= 32`
|
||||
bit_count: u16,
|
||||
compression: BitmapCompression,
|
||||
size_image: u32,
|
||||
x_pels_per_meter: i32,
|
||||
y_pels_per_meter: i32,
|
||||
clr_used: u32,
|
||||
clr_important: u32,
|
||||
}
|
||||
|
||||
impl BitmapInfoHeader {
|
||||
const FIXED_PART_SIZE: usize = 4 // biSize (DWORD)
|
||||
+ 4 // biWidth (LONG)
|
||||
+ 4 // biHeight (LONG)
|
||||
+ 2 // biPlanes (WORD)
|
||||
+ 2 // biBitCount (WORD)
|
||||
+ 4 // biCompression (DWORD)
|
||||
+ 4 // biSizeImage (DWORD)
|
||||
+ 4 // biXPelsPerMeter (LONG)
|
||||
+ 4 // biYPelsPerMeter (LONG)
|
||||
+ 4 // biClrUsed (DWORD)
|
||||
+ 4; // biClrImportant (DWORD)
|
||||
|
||||
const NAME: &'static str = "BITMAPINFOHEADER";
|
||||
|
||||
fn encode_with_size(&self, dst: &mut WriteCursor<'_>, size: u32) -> EncodeResult<()> {
|
||||
ensure_fixed_part_size!(in: dst);
|
||||
|
||||
dst.write_u32(size);
|
||||
dst.write_i32(self.width);
|
||||
dst.write_i32(self.height);
|
||||
dst.write_u16(1); // biPlanes
|
||||
dst.write_u16(self.bit_count);
|
||||
dst.write_u32(self.compression.0);
|
||||
dst.write_u32(self.size_image);
|
||||
dst.write_i32(self.x_pels_per_meter);
|
||||
dst.write_i32(self.y_pels_per_meter);
|
||||
dst.write_u32(self.clr_used);
|
||||
dst.write_u32(self.clr_important);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decode_with_size(src: &mut ReadCursor<'_>) -> DecodeResult<(Self, u32)> {
|
||||
ensure_fixed_part_size!(in: src);
|
||||
|
||||
let size = src.read_u32();
|
||||
|
||||
// NOTE: .abs() could panic on i32::MIN, therefore we have a check for it first.
|
||||
|
||||
let width = src.read_i32();
|
||||
check_invariant(width != i32::MIN && width.abs() <= 10_000)
|
||||
.ok_or_else(|| invalid_field_err!("biWidth", "width is too big"))?;
|
||||
|
||||
let height = src.read_i32();
|
||||
check_invariant(height != i32::MIN && height.abs() <= 10_000)
|
||||
.ok_or_else(|| invalid_field_err!("biHeight", "height is too big"))?;
|
||||
|
||||
let planes = src.read_u16();
|
||||
if planes != 1 {
|
||||
return Err(invalid_field_err!("biPlanes", "invalid planes count"));
|
||||
}
|
||||
|
||||
let bit_count = src.read_u16();
|
||||
check_invariant(bit_count <= 32).ok_or_else(|| invalid_field_err!("biBitCount", "invalid bit count"))?;
|
||||
|
||||
let compression = BitmapCompression(src.read_u32());
|
||||
let size_image = src.read_u32();
|
||||
let x_pels_per_meter = src.read_i32();
|
||||
let y_pels_per_meter = src.read_i32();
|
||||
let clr_used = src.read_u32();
|
||||
let clr_important = src.read_u32();
|
||||
|
||||
let header = Self {
|
||||
width,
|
||||
height,
|
||||
bit_count,
|
||||
compression,
|
||||
size_image,
|
||||
x_pels_per_meter,
|
||||
y_pels_per_meter,
|
||||
clr_used,
|
||||
clr_important,
|
||||
};
|
||||
|
||||
Ok((header, size))
|
||||
}
|
||||
|
||||
// INVARIANT: output (width) <= 10_000
|
||||
fn width(&self) -> u16 {
|
||||
let abs = self.width.abs();
|
||||
debug_assert!(abs <= 10_000);
|
||||
u16::try_from(abs).expect("per the invariant on self.width, this cast is infallible")
|
||||
}
|
||||
|
||||
// INVARIANT: output (height) <= 10_000
|
||||
fn height(&self) -> u16 {
|
||||
let abs = self.height.abs();
|
||||
debug_assert!(abs <= 10_000);
|
||||
u16::try_from(abs).expect("per the invariant on self.height, this cast is infallible")
|
||||
}
|
||||
|
||||
fn is_bottom_up(&self) -> bool {
|
||||
// When self.height is positive, the bitmap is defined as bottom-up.
|
||||
self.height >= 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for BitmapInfoHeader {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
|
||||
let size = cast_int!("biSize", Self::FIXED_PART_SIZE)?;
|
||||
self.encode_with_size(dst, size)
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
Self::FIXED_PART_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Decode<'a> for BitmapInfoHeader {
|
||||
fn decode(src: &mut ReadCursor<'a>) -> DecodeResult<Self> {
|
||||
let (header, size) = Self::decode_with_size(src)?;
|
||||
let size: usize = cast_int!("biSize", size)?;
|
||||
|
||||
if size != Self::FIXED_PART_SIZE {
|
||||
return Err(invalid_field_err!("biSize", "invalid V1 bitmap info header size"));
|
||||
}
|
||||
|
||||
Ok(header)
|
||||
}
|
||||
}
|
||||
|
||||
/// Header used in `CF_DIBV5` formats, defined as [BITMAPV5HEADER]
|
||||
///
|
||||
/// [BITMAPV5HEADER]: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header
|
||||
struct BitmapV5Header {
|
||||
v1: BitmapInfoHeader,
|
||||
red_mask: u32,
|
||||
green_mask: u32,
|
||||
blue_mask: u32,
|
||||
alpha_mask: u32,
|
||||
color_space: ColorSpace,
|
||||
endpoints: CiexyzTriple,
|
||||
gamma_red: u32,
|
||||
gamma_green: u32,
|
||||
gamma_blue: u32,
|
||||
intent: BitmapIntent,
|
||||
profile_data: u32,
|
||||
profile_size: u32,
|
||||
}
|
||||
|
||||
impl BitmapV5Header {
|
||||
const FIXED_PART_SIZE: usize = BitmapInfoHeader::FIXED_PART_SIZE // BITMAPV5HEADER
|
||||
+ 4 // bV5RedMask (DWORD)
|
||||
+ 4 // bV5GreenMask (DWORD)
|
||||
+ 4 // bV5BlueMask (DWORD)
|
||||
+ 4 // bV5AlphaMask (DWORD)
|
||||
+ 4 // bV5CSType (DWORD)
|
||||
+ CiexyzTriple::FIXED_PART_SIZE // bV5Endpoints (CIEXYZTRIPLE)
|
||||
+ 4 // bV5GammaRed (DWORD)
|
||||
+ 4 // bV5GammaGreen (DWORD)
|
||||
+ 4 // bV5GammaBlue (DWORD)
|
||||
+ 4 // bV5Intent (DWORD)
|
||||
+ 4 // bV5ProfileData (DWORD)
|
||||
+ 4 // bV5ProfileSize (DWORD)
|
||||
+ 4; // bV5Reserved (DWORD)
|
||||
|
||||
const NAME: &'static str = "BITMAPV5HEADER";
|
||||
}
|
||||
|
||||
impl Encode for BitmapV5Header {
|
||||
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
|
||||
ensure_fixed_part_size!(in: dst);
|
||||
|
||||
let size = cast_int!("biSize", Self::FIXED_PART_SIZE)?;
|
||||
self.v1.encode_with_size(dst, size)?;
|
||||
|
||||
dst.write_u32(self.red_mask);
|
||||
dst.write_u32(self.green_mask);
|
||||
dst.write_u32(self.blue_mask);
|
||||
dst.write_u32(self.alpha_mask);
|
||||
dst.write_u32(self.color_space.0);
|
||||
self.endpoints.encode(dst)?;
|
||||
dst.write_u32(self.gamma_red);
|
||||
dst.write_u32(self.gamma_green);
|
||||
dst.write_u32(self.gamma_blue);
|
||||
dst.write_u32(self.intent.0);
|
||||
dst.write_u32(self.profile_data);
|
||||
dst.write_u32(self.profile_size);
|
||||
dst.write_u32(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn size(&self) -> usize {
|
||||
Self::FIXED_PART_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Decode<'a> for BitmapV5Header {
|
||||
fn decode(src: &mut ReadCursor<'a>) -> DecodeResult<Self> {
|
||||
ensure_fixed_part_size!(in: src);
|
||||
|
||||
let (header_v1, size) = BitmapInfoHeader::decode_with_size(src)?;
|
||||
let size: usize = cast_int!("biSize", size)?;
|
||||
|
||||
if size != Self::FIXED_PART_SIZE {
|
||||
return Err(invalid_field_err!("biSize", "invalid V5 bitmap info header size"));
|
||||
}
|
||||
|
||||
let red_mask = src.read_u32();
|
||||
let green_mask = src.read_u32();
|
||||
let blue_mask = src.read_u32();
|
||||
let alpha_mask = src.read_u32();
|
||||
let color_space_type = ColorSpace(src.read_u32());
|
||||
let endpoints = CiexyzTriple::decode(src)?;
|
||||
let gamma_red = src.read_u32();
|
||||
let gamma_green = src.read_u32();
|
||||
let gamma_blue = src.read_u32();
|
||||
let intent = BitmapIntent(src.read_u32());
|
||||
let profile_data = src.read_u32();
|
||||
let profile_size = src.read_u32();
|
||||
let _reserved = src.read_u32();
|
||||
|
||||
Ok(Self {
|
||||
v1: header_v1,
|
||||
red_mask,
|
||||
green_mask,
|
||||
blue_mask,
|
||||
alpha_mask,
|
||||
color_space: color_space_type,
|
||||
endpoints,
|
||||
gamma_red,
|
||||
gamma_green,
|
||||
gamma_blue,
|
||||
intent,
|
||||
profile_data,
|
||||
profile_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_v1_header(header: &BitmapInfoHeader) -> Result<(), BitmapError> {
|
||||
if header.width < 0 {
|
||||
return Err(BitmapError::Unsupported("negative width"));
|
||||
}
|
||||
|
||||
if header.width == 0 || header.height == 0 {
|
||||
return Err(BitmapError::InvalidSize);
|
||||
}
|
||||
|
||||
// In the modern world bitmaps with bpp < 24 are rare, and it is even more rare for the bitmaps
|
||||
// which are placed on the clipboard as DIBs, therefore we could safely skip the support for
|
||||
// such bitmaps.
|
||||
const SUPPORTED_BIT_COUNT: &[u16] = &[24, 32];
|
||||
|
||||
if !SUPPORTED_BIT_COUNT.contains(&header.bit_count) {
|
||||
return Err(BitmapError::Unsupported("unsupported bit count"));
|
||||
}
|
||||
|
||||
// This is only relevant for bitmaps with bpp < 24, which are not supported.
|
||||
if header.clr_used != 0 {
|
||||
return Err(BitmapError::Unsupported("color table is not supported"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_v5_header(header: &BitmapV5Header) -> Result<(), BitmapError> {
|
||||
validate_v1_header(&header.v1)?;
|
||||
|
||||
// We support only uncompressed DIB bitmaps as it is the most common case for clipboard-copied bitmaps.
|
||||
const DIBV5_SUPPORTED_COMPRESSION: &[BitmapCompression] = &[BitmapCompression::RGB, BitmapCompression::BITFIELDS];
|
||||
|
||||
if !DIBV5_SUPPORTED_COMPRESSION.contains(&header.v1.compression) {
|
||||
return Err(BitmapError::Unsupported("unsupported compression"));
|
||||
}
|
||||
|
||||
if header.v1.compression == BitmapCompression::BITFIELDS {
|
||||
// Currently, we only support the standard order, BGRA, for the bitfields compression.
|
||||
let is_bgr = header.red_mask == 0x00FF0000 && header.green_mask == 0x0000FF00 && header.blue_mask == 0x000000FF;
|
||||
|
||||
// Note: when there is no alpha channel, the mask is 0x00000000 and we support this too.
|
||||
let is_supported_alpha = header.alpha_mask == 0 || header.alpha_mask == 0xFF000000;
|
||||
|
||||
if !is_bgr || !is_supported_alpha {
|
||||
return Err(BitmapError::Unsupported(
|
||||
"non-standard color masks for `BITFIELDS` compression are not supported",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const SUPPORTED_COLOR_SPACE: &[ColorSpace] = &[
|
||||
ColorSpace::SRGB,
|
||||
// Assume that Windows color space is sRGB, either way we don't have enough information on
|
||||
// the clipboard to convert it to other color spaces.
|
||||
ColorSpace::WINDOWS,
|
||||
];
|
||||
|
||||
if !SUPPORTED_COLOR_SPACE.contains(&header.color_space) {
|
||||
return Err(BitmapError::Unsupported("not supported color space"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct PngEncoderContext {
|
||||
bitmap: Vec<u8>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
color_type: png::ColorType,
|
||||
}
|
||||
|
||||
/// Computes the stride of an uncompressed RGB bitmap.
|
||||
///
|
||||
/// INVARIANT: `width <= output (stride) <= width * 4`
|
||||
///
|
||||
/// In an uncompressed bitmap, the stride is the number of bytes needed to go from the start of one
|
||||
/// row of pixels to the start of the next row. The image format defines a minimum stride for an
|
||||
/// image. In addition, the graphics hardware might require a larger stride for the surface that
|
||||
/// contains the image.
|
||||
///
|
||||
/// For uncompressed RGB formats, the minimum stride is always the image width in bytes, rounded up
|
||||
/// to the nearest DWORD (4 bytes). The following formula is used to calculate the stride:
|
||||
///
|
||||
/// ```
|
||||
/// stride = ((((width * bit_count) + 31) & ~31) >> 3)
|
||||
/// ```
|
||||
///
|
||||
/// From Microsoft doc: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
|
||||
fn rgb_bmp_stride(width: u16, bit_count: u16) -> usize {
|
||||
debug_assert!(bit_count <= 32);
|
||||
|
||||
// No side effects, because u16::MAX * 32 + 31 < u16::MAX * u16::MAX < u32::MAX
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
{
|
||||
(((usize::from(width) * usize::from(bit_count)) + 31) & !31) >> 3
|
||||
}
|
||||
}
|
||||
|
||||
fn bgra_to_top_down_rgba(
|
||||
header: &BitmapInfoHeader,
|
||||
src_bitmap: &[u8],
|
||||
preserve_alpha: bool,
|
||||
) -> Result<PngEncoderContext, BitmapError> {
|
||||
// DIB may be encoded bottom-up, but the format we target, PNG, is top-down.
|
||||
let should_flip_vertically = header.is_bottom_up();
|
||||
|
||||
let width = header.width();
|
||||
let height = header.height();
|
||||
|
||||
let src_n_samples = usize::from(header.bit_count / 8);
|
||||
|
||||
let src_stride = rgb_bmp_stride(width, header.bit_count);
|
||||
|
||||
let (dst_color_type, dst_n_samples) = if preserve_alpha {
|
||||
(png::ColorType::Rgba, 4)
|
||||
} else {
|
||||
(png::ColorType::Rgb, 3)
|
||||
};
|
||||
|
||||
// Per invariants: height * width * dst_n_samples <= 10_000 * 10_000 * 4 < u32::MAX
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
let dst_bitmap_len = usize::from(height) * usize::from(width) * dst_n_samples;
|
||||
|
||||
// Prevent allocation of huge buffers.
|
||||
ensure(dst_bitmap_len <= MAX_BUFFER_SIZE).ok_or(BitmapError::BufferTooBig)?;
|
||||
|
||||
let mut rows_normal;
|
||||
let mut rows_reversed;
|
||||
|
||||
let rows: &mut dyn Iterator<Item = &[u8]> = if should_flip_vertically {
|
||||
rows_reversed = src_bitmap.chunks_exact(src_stride).rev();
|
||||
&mut rows_reversed
|
||||
} else {
|
||||
rows_normal = src_bitmap.chunks_exact(src_stride);
|
||||
&mut rows_normal
|
||||
};
|
||||
|
||||
// DIB stores BGRA colors while PNG uses RGBA.
|
||||
// DIBv1 (CF_DIB) does not have alpha channel, and the fourth byte is always set to 0xFF.
|
||||
// DIBv5 (CF_DIBV5) supports alpha channel, so we should preserve it if it is present.
|
||||
let transform: fn((&mut [u8], &[u8])) = match (header.bit_count, dst_color_type) {
|
||||
(24 | 32, png::ColorType::Rgb) => |(pixel_out, pixel_in)| {
|
||||
pixel_out[0] = pixel_in[2];
|
||||
pixel_out[1] = pixel_in[1];
|
||||
pixel_out[2] = pixel_in[0];
|
||||
},
|
||||
(24, png::ColorType::Rgba) => |(pixel_out, pixel_in)| {
|
||||
pixel_out[0] = pixel_in[2];
|
||||
pixel_out[1] = pixel_in[1];
|
||||
pixel_out[2] = pixel_in[0];
|
||||
pixel_out[3] = 0xFF;
|
||||
},
|
||||
(32, png::ColorType::Rgba) => |(pixel_out, pixel_in)| {
|
||||
pixel_out[0] = pixel_in[2];
|
||||
pixel_out[1] = pixel_in[1];
|
||||
pixel_out[2] = pixel_in[0];
|
||||
pixel_out[3] = pixel_in[3];
|
||||
},
|
||||
_ => unreachable!("possible values are restricted by header validation and logic above"),
|
||||
};
|
||||
|
||||
// Per invariants: width * dst_n_samples <= 10_000 * 4 < u32::MAX
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
let dst_stride = usize::from(width) * dst_n_samples;
|
||||
|
||||
let mut dst_bitmap = vec![0u8; dst_bitmap_len];
|
||||
|
||||
dst_bitmap
|
||||
.chunks_exact_mut(dst_stride)
|
||||
.zip(rows)
|
||||
.for_each(|(dst_row, src_row)| {
|
||||
let dst_pixels = dst_row.chunks_exact_mut(dst_n_samples);
|
||||
let src_pixels = src_row.chunks_exact(src_n_samples);
|
||||
dst_pixels.zip(src_pixels).for_each(transform);
|
||||
});
|
||||
|
||||
Ok(PngEncoderContext {
|
||||
bitmap: dst_bitmap,
|
||||
width,
|
||||
height,
|
||||
color_type: dst_color_type,
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_png(ctx: &PngEncoderContext) -> Result<Vec<u8>, BitmapError> {
|
||||
let mut output: Vec<u8> = Vec::new();
|
||||
|
||||
let width = u32::from(ctx.width);
|
||||
let height = u32::from(ctx.height);
|
||||
|
||||
let mut encoder = png::Encoder::new(&mut output, width, height);
|
||||
encoder.set_color(ctx.color_type);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
|
||||
let mut writer = encoder.write_header()?;
|
||||
writer.write_image_data(&ctx.bitmap)?;
|
||||
writer.finish()?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Converts `CF_DIB` to PNG.
|
||||
pub fn dib_to_png(input: &[u8]) -> Result<Vec<u8>, BitmapError> {
|
||||
let mut src = ReadCursor::new(input);
|
||||
let header = BitmapInfoHeader::decode(&mut src).map_err(BitmapError::Decode)?;
|
||||
|
||||
validate_v1_header(&header)?;
|
||||
|
||||
// We support only uncompressed DIB bitmaps as it is the most common case for clipboard-copied bitmaps.
|
||||
// However, for DIBv1 specifically, BitmapCompression::BITFIELDS is not supported even when the order is BGRA,
|
||||
// because there is an additional variable-sized header holding the color masks that we don’t support yet.
|
||||
const DIBV1_SUPPORTED_COMPRESSION: &[BitmapCompression] = &[BitmapCompression::RGB];
|
||||
|
||||
if !DIBV1_SUPPORTED_COMPRESSION.contains(&header.compression) {
|
||||
return Err(BitmapError::Unsupported("unsupported compression"));
|
||||
}
|
||||
|
||||
let png_ctx = bgra_to_top_down_rgba(&header, src.remaining(), false)?;
|
||||
encode_png(&png_ctx)
|
||||
}
|
||||
|
||||
/// Converts `CF_DIB` to PNG.
|
||||
pub fn dibv5_to_png(input: &[u8]) -> Result<Vec<u8>, BitmapError> {
|
||||
let mut src = ReadCursor::new(input);
|
||||
let header = BitmapV5Header::decode(&mut src).map_err(BitmapError::Decode)?;
|
||||
|
||||
validate_v5_header(&header)?;
|
||||
|
||||
let png_ctx = bgra_to_top_down_rgba(&header.v1, src.remaining(), true)?;
|
||||
encode_png(&png_ctx)
|
||||
}
|
||||
|
||||
fn top_down_rgba_to_bottom_up_bgra(
|
||||
info: png::OutputInfo,
|
||||
src_bitmap: &[u8],
|
||||
) -> Result<(BitmapInfoHeader, Vec<u8>), BitmapError> {
|
||||
let no_alpha = info.color_type != png::ColorType::Rgba;
|
||||
let width = u16::try_from(info.width).map_err(|_| BitmapError::WidthTooBig)?;
|
||||
let height = u16::try_from(info.height).map_err(|_| BitmapError::HeightTooBig)?;
|
||||
|
||||
#[expect(clippy::arithmetic_side_effects)] // width * 4 <= 10_000 * 4 < u32::MAX
|
||||
let stride = usize::from(width) * 4;
|
||||
|
||||
let src_rows = src_bitmap.chunks_exact(stride);
|
||||
|
||||
// As per invariants: stride * height <= width * 4 * height <= 10_000 * 4 * 10_000 <= u32::MAX.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
let dst_len = stride * usize::from(height);
|
||||
let dst_len = u32::try_from(dst_len).map_err(|_| BitmapError::InvalidSize)?;
|
||||
|
||||
let header = BitmapInfoHeader {
|
||||
width: i32::from(width),
|
||||
height: i32::from(height),
|
||||
bit_count: 32, // 4 samples * 8 bits
|
||||
compression: BitmapCompression::RGB,
|
||||
size_image: dst_len,
|
||||
x_pels_per_meter: 0,
|
||||
y_pels_per_meter: 0,
|
||||
clr_used: 0,
|
||||
clr_important: 0,
|
||||
};
|
||||
|
||||
let dst_len = usize::try_from(dst_len).map_err(|_| BitmapError::InvalidSize)?;
|
||||
let mut dst_bitmap = vec![0; dst_len];
|
||||
|
||||
// Reverse rows to draw the image from bottom to top.
|
||||
let dst_rows = dst_bitmap.chunks_exact_mut(stride).rev();
|
||||
|
||||
let transform: fn((&mut [u8], &[u8])) = if no_alpha {
|
||||
|(dst_pixel, src_pixel)| {
|
||||
dst_pixel[0] = src_pixel[2];
|
||||
dst_pixel[1] = src_pixel[1];
|
||||
dst_pixel[2] = src_pixel[0];
|
||||
dst_pixel[3] = 0xFF;
|
||||
}
|
||||
} else {
|
||||
|(dst_pixel, src_pixel)| {
|
||||
dst_pixel[0] = src_pixel[2];
|
||||
dst_pixel[1] = src_pixel[1];
|
||||
dst_pixel[2] = src_pixel[0];
|
||||
dst_pixel[3] = src_pixel[3];
|
||||
}
|
||||
};
|
||||
|
||||
dst_rows.zip(src_rows).for_each(|(dst_row, src_row)| {
|
||||
let dst_pixels = dst_row.chunks_exact_mut(4);
|
||||
let src_pixels = src_row.chunks_exact(4);
|
||||
dst_pixels.zip(src_pixels).for_each(transform);
|
||||
});
|
||||
|
||||
Ok((header, dst_bitmap))
|
||||
}
|
||||
|
||||
fn decode_png(mut input: &[u8]) -> Result<(png::OutputInfo, Vec<u8>), BitmapError> {
|
||||
let mut decoder = png::Decoder::new(Cursor::new(&mut input));
|
||||
|
||||
// We need to produce 32-bit DIB, so we should expand the palette to 32-bit RGBA.
|
||||
decoder.set_transformations(png::Transformations::ALPHA | png::Transformations::EXPAND);
|
||||
|
||||
let mut reader = decoder.read_info()?;
|
||||
let Some(output_buffer_len) = reader.output_buffer_size() else {
|
||||
return Err(BitmapError::BufferTooBig);
|
||||
};
|
||||
|
||||
// Prevent allocation of huge buffers.
|
||||
ensure(output_buffer_len <= MAX_BUFFER_SIZE).ok_or(BitmapError::BufferTooBig)?;
|
||||
|
||||
let mut buffer = vec![0; output_buffer_len];
|
||||
let info = reader.next_frame(&mut buffer)?;
|
||||
buffer.truncate(info.buffer_size());
|
||||
|
||||
Ok((info, buffer))
|
||||
}
|
||||
|
||||
/// Converts PNG to `CF_DIB` format.
|
||||
pub fn png_to_cf_dib(input: &[u8]) -> Result<Vec<u8>, BitmapError> {
|
||||
// FIXME(perf): it’s possible to allocate a single array and to directly write both the header and the actual bitmap inside.
|
||||
// Currently, the code is performing three allocations: one inside `decode_png`, one inside `top_down_rgba_to_bottom_up_bgra`
|
||||
// and one in the body of this function.
|
||||
|
||||
let (png_info, rgba_bytes) = decode_png(input)?;
|
||||
let (header, bgra_bytes) = top_down_rgba_to_bottom_up_bgra(png_info, &rgba_bytes)?;
|
||||
|
||||
let output_len = header
|
||||
.size()
|
||||
.checked_add(bgra_bytes.len())
|
||||
.ok_or(BitmapError::BufferTooBig)?;
|
||||
|
||||
ensure(output_len <= MAX_BUFFER_SIZE).ok_or(BitmapError::BufferTooBig)?;
|
||||
|
||||
let mut output = vec![0; output_len];
|
||||
{
|
||||
let mut dst = WriteCursor::new(&mut output);
|
||||
header.encode(&mut dst).map_err(BitmapError::Encode)?;
|
||||
dst.write_slice(&bgra_bytes);
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Converts PNG to `CF_DIBV5` format.
|
||||
pub fn png_to_cf_dibv5(input: &[u8]) -> Result<Vec<u8>, BitmapError> {
|
||||
// FIXME(perf): it’s possible to allocate a single array and to directly write both the header and the actual bitmap inside.
|
||||
// Currently, the code is performing three allocations: one inside `decode_png`, one inside `top_down_rgba_to_bottom_up_bgra`
|
||||
// and one in the body of this function.
|
||||
|
||||
let (png_info, rgba_bytes) = decode_png(input)?;
|
||||
let (header_v1, bgra_bytes) = top_down_rgba_to_bottom_up_bgra(png_info, &rgba_bytes)?;
|
||||
|
||||
let header = BitmapV5Header {
|
||||
v1: header_v1,
|
||||
// Windows sets these masks for 32-bit bitmaps even if BITFIELDS compression is not used.
|
||||
red_mask: 0x00FF0000,
|
||||
green_mask: 0x0000FF00,
|
||||
blue_mask: 0x000000FF,
|
||||
alpha_mask: 0xFF000000,
|
||||
color_space: ColorSpace::SRGB,
|
||||
endpoints: Default::default(),
|
||||
gamma_red: 0,
|
||||
gamma_green: 0,
|
||||
gamma_blue: 0,
|
||||
intent: BitmapIntent::LCS_GM_IMAGES,
|
||||
profile_data: 0,
|
||||
profile_size: 0,
|
||||
};
|
||||
|
||||
let output_len = header
|
||||
.size()
|
||||
.checked_add(bgra_bytes.len())
|
||||
.ok_or(BitmapError::BufferTooBig)?;
|
||||
|
||||
ensure(output_len <= MAX_BUFFER_SIZE).ok_or(BitmapError::BufferTooBig)?;
|
||||
|
||||
let mut output = vec![0; output_len];
|
||||
{
|
||||
let mut dst = WriteCursor::new(&mut output);
|
||||
header.encode(&mut dst).map_err(BitmapError::Encode)?;
|
||||
dst.write_slice(&bgra_bytes);
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Use this when establishing invariants.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
fn check_invariant(condition: bool) -> Option<()> {
|
||||
condition.then_some(())
|
||||
}
|
||||
|
||||
/// Returns `None` when the condition is unmet.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
fn ensure(condition: bool) -> Option<()> {
|
||||
condition.then_some(())
|
||||
}
|
||||
179
crates/ironrdp-cliprdr-format/src/html.rs
Normal file
179
crates/ironrdp-cliprdr-format/src/html.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
#[derive(Debug)]
|
||||
pub enum HtmlError {
|
||||
InvalidFormat,
|
||||
InvalidUtf8(core::str::Utf8Error),
|
||||
InvalidInteger(core::num::ParseIntError),
|
||||
InvalidConversion,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for HtmlError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
HtmlError::InvalidFormat => write!(f, "invalid CF_HTML format"),
|
||||
HtmlError::InvalidUtf8(_error) => write!(f, "invalid UTF-8"),
|
||||
HtmlError::InvalidInteger(_error) => write!(f, "failed to parse integer"),
|
||||
HtmlError::InvalidConversion => write!(f, "invalid integer conversion"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for HtmlError {
|
||||
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
|
||||
match self {
|
||||
HtmlError::InvalidFormat => None,
|
||||
HtmlError::InvalidUtf8(utf8_error) => Some(utf8_error),
|
||||
HtmlError::InvalidInteger(parse_int_error) => Some(parse_int_error),
|
||||
HtmlError::InvalidConversion => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<core::str::Utf8Error> for HtmlError {
|
||||
fn from(error: core::str::Utf8Error) -> Self {
|
||||
HtmlError::InvalidUtf8(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<core::num::ParseIntError> for HtmlError {
|
||||
fn from(error: core::num::ParseIntError) -> Self {
|
||||
HtmlError::InvalidInteger(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts `CF_HTML` format to plain HTML text.
|
||||
///
|
||||
/// Note that the `CF_HTML` format is using UTF-8, and the input is expected to be valid UTF-8.
|
||||
/// However, there is no easy way to know the size of the `CF_HTML` payload:
|
||||
/// 1) it’s typically not null-terminated, and
|
||||
/// 2) reading the headers is already half of the work.
|
||||
///
|
||||
/// Because of that, this function takes the input as a byte slice and finds the end of the payload itself.
|
||||
/// This is expected to be more convenient at the callsite.
|
||||
pub fn cf_html_to_plain_html(input: &[u8]) -> Result<&str, HtmlError> {
|
||||
const EOL_CONTROL_CHARS: &[u8] = b"\r\n";
|
||||
|
||||
let mut start_fragment = None;
|
||||
let mut end_fragment = None;
|
||||
|
||||
// We’ll move the lower bound of this slice until all headers are read.
|
||||
let mut cursor = input;
|
||||
|
||||
loop {
|
||||
let line = {
|
||||
// We use a custom logic for splitting lines, instead of something like `str::lines`.
|
||||
// That’s because `str::lines` does not split at carriage return (`\r`) not followed by line feed (`\n`).
|
||||
// In `CF_HTML` format, the line ending could be represented using `\r` alone.
|
||||
let eol_pos = cursor
|
||||
.iter()
|
||||
.position(|byte| EOL_CONTROL_CHARS.contains(byte))
|
||||
.ok_or(HtmlError::InvalidFormat)?;
|
||||
core::str::from_utf8(&cursor[..eol_pos])?
|
||||
};
|
||||
|
||||
match line.split_once(':') {
|
||||
Some((key, value)) => match key {
|
||||
"StartFragment" => {
|
||||
start_fragment = Some(header_value_to_u32(value)?);
|
||||
}
|
||||
"EndFragment" => {
|
||||
end_fragment = Some(header_value_to_u32(value)?);
|
||||
}
|
||||
_ => {
|
||||
// We are not interested in other headers.
|
||||
}
|
||||
},
|
||||
None => {
|
||||
// At this point, we reached the end of the headers.
|
||||
if let (Some(start), Some(end)) = (start_fragment, end_fragment) {
|
||||
let start = usize::try_from(start).map_err(|_| HtmlError::InvalidConversion)?;
|
||||
let end = usize::try_from(end).map_err(|_| HtmlError::InvalidConversion)?;
|
||||
|
||||
// Ensure start and end values are properly bounded.
|
||||
if !(start < end && end < input.len()) {
|
||||
return Err(HtmlError::InvalidFormat);
|
||||
}
|
||||
|
||||
// Extract the fragment from the original buffer.
|
||||
let fragment = core::str::from_utf8(&input[start..end])?;
|
||||
|
||||
return Ok(fragment);
|
||||
} else {
|
||||
// If required headers were not found, the input is considered invalid.
|
||||
return Err(HtmlError::InvalidFormat);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Skip EOL control characters and prepare for next line.
|
||||
cursor = &cursor[line.len()..];
|
||||
while let Some(b'\n' | b'\r') = cursor.first() {
|
||||
cursor = &cursor[1..];
|
||||
}
|
||||
}
|
||||
|
||||
fn header_value_to_u32(value: &str) -> Result<u32, core::num::ParseIntError> {
|
||||
value.trim_start_matches('0').parse::<u32>()
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts plain HTML text to `CF_HTML` format.
|
||||
pub fn plain_html_to_cf_html(fragment: &str) -> String {
|
||||
const POS_PLACEHOLDER: &str = "0000000000";
|
||||
|
||||
let mut buffer = String::new();
|
||||
|
||||
let mut write_header = |key: &str, value: &str| {
|
||||
// This relation holds: key.len() + value.len() + ":\r\n".len() < usize::MAX
|
||||
// Rationale: we know all possible values (see code below), and they are much smaller than `usize::MAX`.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
let size = key.len() + value.len() + ":\r\n".len();
|
||||
buffer.reserve(size);
|
||||
|
||||
buffer.push_str(key);
|
||||
buffer.push(':');
|
||||
let value_pos = buffer.len();
|
||||
buffer.push_str(value);
|
||||
buffer.push_str("\r\n");
|
||||
|
||||
value_pos
|
||||
};
|
||||
|
||||
write_header("Version", "0.9");
|
||||
|
||||
let start_html_header_value_pos = write_header("StartHTML", POS_PLACEHOLDER);
|
||||
let end_html_header_value_pos = write_header("EndHTML", POS_PLACEHOLDER);
|
||||
let start_fragment_header_value_pos = write_header("StartFragment", POS_PLACEHOLDER);
|
||||
let end_fragment_header_value_pos = write_header("EndFragment", POS_PLACEHOLDER);
|
||||
|
||||
let start_html_pos = buffer.len();
|
||||
buffer.push_str("<html>\r\n<body>\r\n<!--StartFragment-->");
|
||||
|
||||
let start_fragment_pos = buffer.len();
|
||||
buffer.push_str(fragment);
|
||||
|
||||
let end_fragment_pos = buffer.len();
|
||||
buffer.push_str("<!--EndFragment-->\r\n</body>\r\n</html>");
|
||||
|
||||
let end_html_pos = buffer.len();
|
||||
|
||||
let start_html_pos_value = format!("{start_html_pos:0>10}");
|
||||
let end_html_pos_value = format!("{end_html_pos:0>10}");
|
||||
let start_fragment_pos_value = format!("{start_fragment_pos:0>10}");
|
||||
let end_fragment_pos_value = format!("{end_fragment_pos:0>10}");
|
||||
|
||||
let mut replace_placeholder = |value_begin_idx: usize, header_value: &str| {
|
||||
// We know that: value_begin_idx + POS_PLACEHOLDER.len() < usize::MAX
|
||||
// Rationale: the headers are written at the beginning, and we’re not indexing outside of the string.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
let value_end_idx = value_begin_idx + POS_PLACEHOLDER.len();
|
||||
|
||||
buffer.replace_range(value_begin_idx..value_end_idx, header_value);
|
||||
};
|
||||
|
||||
replace_placeholder(start_html_header_value_pos, &start_html_pos_value);
|
||||
replace_placeholder(end_html_header_value_pos, &end_html_pos_value);
|
||||
replace_placeholder(start_fragment_header_value_pos, &start_fragment_pos_value);
|
||||
replace_placeholder(end_fragment_header_value_pos, &end_fragment_pos_value);
|
||||
|
||||
buffer
|
||||
}
|
||||
5
crates/ironrdp-cliprdr-format/src/lib.rs
Normal file
5
crates/ironrdp-cliprdr-format/src/lib.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
|
||||
|
||||
pub mod bitmap;
|
||||
pub mod html;
|
||||
62
crates/ironrdp-cliprdr-native/CHANGELOG.md
Normal file
62
crates/ironrdp-cliprdr-native/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.4.0...ironrdp-cliprdr-native-v0.5.0)] - 2025-12-18
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Prevent window class registration error on multiple sessions ([#1047](https://github.com/Devolutions/IronRDP/issues/1047)) ([a2af587e60](https://github.com/Devolutions/IronRDP/commit/a2af587e60e869f0235703e21772d1fc6a7dadcd))
|
||||
|
||||
When starting a second clipboard session, `RegisterClassA` would fail
|
||||
with `ERROR_CLASS_ALREADY_EXISTS` because window classes are global to
|
||||
the process. Now checks if the class is already registered before
|
||||
attempting registration, allowing multiple WinClipboard instances to
|
||||
coexist.
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Bump windows from 0.61.3 to 0.62.1 ([#1010](https://github.com/Devolutions/IronRDP/issues/1010)) ([79e71c4f90](https://github.com/Devolutions/IronRDP/commit/79e71c4f90ea68b14fe45241c1cf3953027b22a2))
|
||||
|
||||
## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.3.0...ironrdp-cliprdr-native-v0.4.0)] - 2025-08-29
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Map `E_ACCESSDENIED` WinAPI error code to `ClipboardAccessDenied` error (#936) ([b0c145d0d9](https://github.com/Devolutions/IronRDP/commit/b0c145d0d9cf2f347e537c08ce9d6c35223823d5))
|
||||
|
||||
When the system clipboard updates, we receive an `Updated` event. Then
|
||||
we try to open it, but we can get `AccessDenied` error because the
|
||||
clipboard may still be locked for another window (like _Notepad_). To
|
||||
handle this, we have special logic that attempts to open the clipboard
|
||||
in the event of such errors.
|
||||
The problem is that so far, the `ClipboardAccessDenied` error was not mapped.
|
||||
|
||||
## [[0.1.4](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.1.3...ironrdp-cliprdr-native-v0.1.4)] - 2025-03-12
|
||||
|
||||
### <!-- 7 -->Build
|
||||
|
||||
- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa))
|
||||
|
||||
## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.1.2...ironrdp-cliprdr-native-v0.1.3)] - 2025-02-03
|
||||
|
||||
### <!-- 4 -->Bug Fixes
|
||||
|
||||
- Handle `WM_ACTIVATEAPP` in `clipboard_subproc` ([#657](https://github.com/Devolutions/IronRDP/issues/657)) ([9b2926ea12](https://github.com/Devolutions/IronRDP/commit/9b2926ea1212d3f9dec9354334d5bdaa1bebd81e))
|
||||
|
||||
Previously, the function handled only `WM_ACTIVATE`.
|
||||
|
||||
## [[0.1.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.1.1...ironrdp-cliprdr-native-v0.1.2)] - 2025-01-28
|
||||
|
||||
### <!-- 6 -->Documentation
|
||||
|
||||
- Use CDN URLs instead of the blob storage URLs for Devolutions logo ([#631](https://github.com/Devolutions/IronRDP/issues/631)) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b))
|
||||
|
||||
## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.1.0...ironrdp-cliprdr-native-v0.1.1)] - 2024-12-14
|
||||
|
||||
### Other
|
||||
|
||||
- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a))
|
||||
35
crates/ironrdp-cliprdr-native/Cargo.toml
Normal file
35
crates/ironrdp-cliprdr-native/Cargo.toml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
[package]
|
||||
name = "ironrdp-cliprdr-native"
|
||||
version = "0.5.0"
|
||||
readme = "README.md"
|
||||
description = "Native CLIPRDR static channel backend implementations for IronRDP"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.5" } # public
|
||||
ironrdp-core = { path = "../ironrdp-core", version = "0.1" }
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.62", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_DataExchange",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Memory",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
1
crates/ironrdp-cliprdr-native/LICENSE-APACHE
Symbolic link
1
crates/ironrdp-cliprdr-native/LICENSE-APACHE
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-APACHE
|
||||
1
crates/ironrdp-cliprdr-native/LICENSE-MIT
Symbolic link
1
crates/ironrdp-cliprdr-native/LICENSE-MIT
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-MIT
|
||||
7
crates/ironrdp-cliprdr-native/README.md
Normal file
7
crates/ironrdp-cliprdr-native/README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# IronRDP CLIPRDR native backends
|
||||
|
||||
Native CLIPRDR backend implementations. Currently only Windows is supported.
|
||||
|
||||
This crate is part of the [IronRDP] project.
|
||||
|
||||
[IronRDP]: https://github.com/Devolutions/IronRDP
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue