From 98df08410547f25ebcdeb289163d5976a08210ea Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Wed, 17 May 2023 16:28:20 +0300 Subject: [PATCH 01/16] Integrate Android example (#275) * Upgrade Gradle version to 8.0 * Refactor Android example * Add 2 Android gradle plugins (without applying) to the root build (haven't managed to isolate them in Android example module) * Add android.useAndroidX property to the root gradle.properties (for some reason defining this property in the Android submodule doesn't work) * Add libp2p.gradle.custom per project property which indicates whether common gradle code for submodules should be skipped (set it up for Adnroid example) * Move Maven repositories definitions to settings.gradle * In the settings.gradle detect if Android SDK is installed and conditionally include :examples:android-chatter module * Add Setup Android SDK GitHub action --- .github/workflows/build.yml | 3 + build.gradle.kts | 25 +- examples/android-chatter/build.gradle | 58 ++-- examples/android-chatter/gradle.properties | 1 + .../src/main/AndroidManifest.xml | 3 +- .../io/libp2p/example/chat/MainActivity.kt | 1 - gradle.properties | 3 +- gradle/wrapper/gradle-wrapper.jar | Bin 61574 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 269 +++++++----------- gradlew.bat | 181 ++++++------ settings.gradle | 40 +++ 12 files changed, 283 insertions(+), 305 deletions(-) create mode 100644 examples/android-chatter/gradle.properties diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7adb1c2c2..4991a5968 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,5 +16,8 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v2 + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + - name: Execute Gradle build run: ./gradlew -s build dokkaJar \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 2b09f3ab5..03ebf0a9e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,9 @@ import java.net.URL description = "a minimal implementation of libp2p for the jvm" plugins { - kotlin("jvm").version("1.6.21") + val kotlinVersion = "1.6.21" + + id("org.jetbrains.kotlin.jvm") version kotlinVersion apply false id("com.github.ben-manes.versions").version("0.44.0") id("idea") @@ -19,16 +21,28 @@ plugins { id("org.jmailen.kotlinter").version("3.10.0") id("java-test-fixtures") id("io.spring.dependency-management").version("1.1.0") + + id("org.jetbrains.kotlin.android") version kotlinVersion apply false + id("com.android.application") version "7.4.2" apply false } -allprojects { +fun Project.getBooleanPropertyOrFalse(propName: String) = + (this.properties[propName] as? String)?.toBoolean() ?: false + +configure( + allprojects + .filterNot { + it.getBooleanPropertyOrFalse("libp2p.gradle.custom") + } +) { group = "io.libp2p" version = "develop" apply(plugin = "kotlin") apply(plugin = "idea") - apply(plugin = "io.gitlab.arturbosch.detekt") apply(plugin = "java") + + apply(plugin = "io.gitlab.arturbosch.detekt") apply(plugin = "maven-publish") apply(plugin = "org.jetbrains.dokka") apply(plugin = "org.jmailen.kotlinter") @@ -36,11 +50,6 @@ allprojects { apply(plugin = "io.spring.dependency-management") apply(from = "$rootDir/versions.gradle") - repositories { - mavenCentral() - maven("https://artifacts.consensys.net/public/maven/maven/") - } - dependencies { implementation(kotlin("stdlib-jdk8")) diff --git a/examples/android-chatter/build.gradle b/examples/android-chatter/build.gradle index 9d4c46bcf..fdf829839 100644 --- a/examples/android-chatter/build.gradle +++ b/examples/android-chatter/build.gradle @@ -1,32 +1,16 @@ -buildscript { - ext.kotlin_version = '1.3.50' - repositories { - google() - jcenter() +apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" - } - dependencies { - classpath 'com.android.tools.build:gradle:3.5.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -repositories { - google() -} - -apply plugin: 'com.android.application' - -apply plugin: 'kotlin-android' - -apply plugin: 'kotlin-android-extensions' +apply plugin: "io.spring.dependency-management" +apply from: "$rootDir/versions.gradle" android { - compileSdkVersion 28 + namespace = "io.libp2p.example.chat" + compileSdkVersion 30 defaultConfig { - applicationId "io.libp2p.example.chatter" - minSdkVersion 28 - targetSdkVersion 28 + applicationId "io.libp2p.example.chat" + minSdkVersion 26 + targetSdkVersion 30 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -38,20 +22,24 @@ android { } } packagingOptions { - exclude 'META-INF/DEPENDENCIES' + exclude 'META-INF/io.netty.versions.properties' + exclude 'META-INF/INDEX.LIST' + } + kotlinOptions { + jvmTarget = "11" } compileOptions { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + sourceCompatibility = 1.11 + targetCompatibility = 1.11 + } + lint { + abortOnError = false } } dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.1.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation project(':chatter') - annotationProcessor 'org.apache.logging.log4j:log4j-core:2.11.2' + implementation("androidx.appcompat:appcompat:1.2.0") + implementation("com.google.android.material:material:1.2.0") + implementation("androidx.constraintlayout:constraintlayout:2.0.4") + implementation project(':examples:chatter') } diff --git a/examples/android-chatter/gradle.properties b/examples/android-chatter/gradle.properties new file mode 100644 index 000000000..5ba6d5df0 --- /dev/null +++ b/examples/android-chatter/gradle.properties @@ -0,0 +1 @@ +libp2p.gradle.custom = true diff --git a/examples/android-chatter/src/main/AndroidManifest.xml b/examples/android-chatter/src/main/AndroidManifest.xml index 0ac48f735..e1cd80406 100644 --- a/examples/android-chatter/src/main/AndroidManifest.xml +++ b/examples/android-chatter/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + Ze*iUrFwrx9&+1R#iG-i`@zk7XK>p12g%v>|qoHJqj zkb^Uj4fNnmyAEpK5lbP-WcZ-Kz}8^Ez(kVge29}tE-8Uhjcql24UB)=c3kk2-&Cb( zQd$FAIiX~$G@DCm?E|f?X;PI@YI)O-xa_*F4lE%*@!$8qi_{`jR3O=9o3lZ-^-XiR_ERDky&Az`a}u+fsAfzmzz19zvn_%O55(Oe*MMrBXm7IB^c1PHTEDgz@Q2?}qEx zsj@qDPy-ruKG?pE);`M60@ZR zB8EWwv2h$7--K};Ogd_Dh;ldA2(~RQHHDUE4`w$5oMD<;khcqBb&K|s-!J|R_Vey< zjy~sGuVYU|+TZPFDY5L;*G-oTX+c8@!=)bFP=$jpbJ1{1ydr zl}=iD>j+88Up@LG%v`Lca$0Mbs7`$yWpK@HW3$6pnZiZ0Pgjo-j$K^BnnBqcD~)o~ za}sSP1Vh+(S;s?jPpb^!VJita__~AUyYY||$P0@M=bXBSm6#3cH*BJny%yHA_PG0e z6V=J)^lnM{$v4;)qv_T|S|N{|`+OG#V%GWn^6o8RS_R47Ab1tkuq<-o{*1H}CDdd&X55=1CWvuawHl-Wk~c(y=UucF z&y?+#kVnY(TXMxc>R~2{99a9{ZIV1u)kb`AWF0Zv)#{n2dBbq5_?CpMoq7Q87>09V zJtOnt%232_%I)OaV~9VzRD{lt!r$SLNv@(sp)$>8;WtI3A?NC%<#aoeiv?e>MeNt_ zaCkSnDtnOPb5*5xP#4l)@)Dn+cn|b(``o|&$x!=CeFKc4fJ*f^^bSd~{4#O(Um0%q zHp>}TlHNM07;1c(DP>7j!CZPyGXcxOpm{`<$)7>Ll6LO2WiET?L4(o7=No+0YfVun z$XvR_D)V1Eh0CKqt)2PH=R}9}y#?c`mxY)O1jp3EPJS!PK|;ZSfqnlD_FrQGo9|S- zVt@n#>-&~G?ZXdjtD&i2{4)^8UX_ScR(l}lgj84lyTGBwIw4A_ym)01O1L#(pqsV? zR9Ibt=jNOjmNEOBA={RnZ(-t!OT~u!%m|!%&V1%Jof;y2`F}tee`l8PnxihtVyP{1Ub9^>`|1EpOdWESxO8cF4Jz2cl#qb?I=p5jEsc$3#ff zl+J9zY~g=rXhi&GcbJG=ZE_i@(&*z5D}R|H`z8f~vDbq{xIP8L9cs^GN0Ze)_H!fZ zfXK-Y`NkpZ6(I#dB$v^T4y%NU%0h2~^neh1tRW8^FS)6*Qi$KhsKP{9NJp3$3hNI* zVBy+AS|EaZqMSdwdeiQ8gjm!#*)mf!7mM}LiS7OCr4D0E-X;6TWfUhC=A@J`25;xXkMV%DC-Ee=BR#=h(aFdOcCpu*^z;gd?7 zrAh(p)|ZmCIq{#V79TN|x7Ehrb3t559)iG8=pJ)(Q?mZ^smdMrvu8wtW~P*eIq|>* z6G0D1y-Py(zG6L?WAg>ES2g`%?znDSY|aC-^ZgL};MM-_5j~wHr#(zD%Eb)LpoDeceO@6emYq@EdAlm&Q)CPJVnjMt zRD_(uKGdOMv1{JxJI&F4R=VPzi9h^qv}V%uP@fn=9oiugk}J>IZY9xvWEt?k#Y6!% z?th332BuAt+z(I#wYsog_@nOr@lcI&P9PC9%Cis)LJZ`&B=@8=yTl?2>2C3a6k44m zt-hoXXw&^+QH)Xq6&4%z?3kHnnsDH5Bq@nf~yT<54S(wmRc@y!ZK zt362#{}8Z9ghiVd>)%7xGr1jid>-NeOD*q1DJ>)NB1Yh&CR}sz26TqwS545pJ&=`^*&uZVGwdVUQUMZ+y-sALC8m^Ti)#={}>w4NCxxv)!Qw`}vP9>mAf-!0Sxz zF`ww28F+V`|4`yl|0i5Z?0qK1QOr^~MYJ(QX+iO|grea@+r|GV!KAi+Z4#;>z2_oA ztF>0_6dK+--=@BDdn7xrUa8NR$0@2>J7AbG9WCCZ%^@eQMx9k!q(g<5KQM`DSa>gs ze0{2G7ot_!y&*;oik>l!4t7`+_aCl zq1gfidswNCsBB#H-4gqH8aq|@T&Zo<-D1bN10x2aKGhTUbh~Bu4yi8{zFiZZ72R44 zc3SOX`+oAeEJWNjaT$5?crs<2e`8PuFq>X7(eE-UrQR7_m)MfV>#P^J`o@l+&$o_t zfD{4Cp=)dHHU*Sw7$2mUN1|&U&ZTu?xaa49+DoF(njN&o!(v9#&8QKn%?vueDX)c< z!{-DdIb1qVHjQTFKfAl@038N@7%Gfht$Pkkjw{aQmD&wuh4wqH79Q%%?f(FhfEMz*e!9GGjdK>E0Iw=D zxWQ-pLnroozG7@l#1ak94$|=565#clG^VCOK`-7N_fEx!K`9A!Jn0pMEOWX6iJelN z#QGNTN;l2X0~?OkAtY3#A))ZGZ2dd|SI}$?a^87Rex2vAUn`1L&A=hJhn6q#7wR`L z;!!sl4BNCL%iy~Y8Oq9doM276xPP4+7(4a1;GxKL1nRE|1L3YyDrL*A5I~bn-Gde; zifFR4o{*NYt87H2*yF~NIR`&CI~OM6hc9&`#%+kJ8AW`AE{mWk1ccJ1jM?9n887XX zg|K%r9$kcKTS-K|rm+jID^_$;!@N-|Tyj?HpW{=3M(h->XwUre4b)QBA)s`U3Gv}~<*sYAogC{rx-@pas?K2h(}{2Eb>`~fMBKc@*Jko*;Q zD8$QWI^MFya*s(Pp7$)b?0U`DXx=f>{x*<&_oR$~?3fT$)*;^RFf~V~yiM#*0r$R^ ziBl6o$q4luhT;^o9kBZ9u)-v?Pwe|5i5A@wH+9&+F+WZ=ct&RNwg8&y;FwPpl8XYMs8Vk*lM zTw-Z#>%&e-!t9r~ZBv%WW4!iByV0RcuM)?N*xg?#w;${a7eE_38>hce53OA%acZ8C z1Rel}{t;j;NYi~^a3(;J70ymC2(`-;5EPKAL;ly9PS|AUsOWP6<+uGidS@!fhbXl{ z`D*5b`In7v9du|YE0gqe&pnG4E#v?Y-*=X zF`s1=!g@m<1Ke(beLL`|N zHDZOPi05Bb{1J$-vXO+%8RnS4bq`11ilwJ?)+9owMSjFG-;U;l%o=quwH47pc0pQF zZN=sJ-fFzU#^xWz4+6oEf3rPpKQM$0`qB+LE54Jl18>kz`g>bm3{Jdw@lem_tqw|5 z2n15MHT;V|(NT=8HTzS3b z=t(ZofCjA^rPdmy_sdo4&7)lfkU^}_cs@i9rbyOa@0PvevM5O>?n|NGFUm7vBE=$8 zPa?bDukNJ8F$bWlH}`D~l&?V6$(%8LxpWKxw@jpezqhM~EvSU>sYS3Gq6Wa2(>9Xt z8I0x>R41;!PP^woNnzIn!4cX!Ed>Ee6BQV?fIIw9O!d!S;a^vn8v_1RXe~+iWB`JN zMfL)~GjnBt7OEoj@8VB2v%eXptHzoGzmU`9=@>&hjZb5>|KSeCpTA9U8`?*`S|5z1 zSd)aH8Ihn(ke~7fDdlA%K@XfE?O1Lp$bq(7H`<3{h^*D)Spm!+PPCPB7k}a+zAf4W zP!Q=Huaw9?g9vv?W1tfa^M}>c*0?f_>EZsu>42wQ ze}Hs!^`Kj4{bn;zCgJ)gFB}PW-2z>6Nm7;eRz%r*!*h@Yl75P_?+05P}2-d#1C?QyJ zMks@Okb$DnySpGYdW^_-mzU%zNcjrrQPp@92Rv%KC<>ZyyD^y6hzp`F`TmwM)hpwI zE12ibp9X0JCX3hoI9T~DN74irV%AizbsNWOZ#BBm9}k-G!rp0n`MOp{5Fut zarWKknCQ8H4R2A}IQXk)&`20H`-=^ zs-fjD-SKnb^Zro9B6Q@~dB=#cz}6DKgs)5#NS!UMz@9YKp>TBW=NQYv;iJl1Zks-x z={kCqtho{meJUN(54yY%xH}6ku|bSf4i3Yzg|L&!Biq{!CN-Twj*f8r>@w*Nc{-d4 ztEYM@3195+6+Kctw|^49s5Y1C>{_X>vz=0w#j#9ekK*$@y zbx)11wCVoHoJ|U&oqe;)D?U!LysEH!rK^lBSk~y42j}MeyR<-gY-0;DIkAyX^>&n5 zv@}|GSM{^7*;w$+_0C>&yM%XZ&_IPNx7obKuz&d@y{OnYYp|pFxUh7hR(961 zFMtDAoFkmY2V;rP28R>iCM;*!mLPY7a0k#@eBo@oxffVttR>GtQ@){+Hd!NQh`f;a zXpe-Q>AiATe#rE_@lgKRBjYpYU z%L&)Kt28@rm0$vU_6ln($@7?3I$Lxa_)PKq$gg3{Bq9`QNcnt^_-^x#Yre+k#_ord zgpsJR?khe!F7?Rvrai!S{_yCk4u`*{qc^g72K^55s*K7$!#=}K%q^1cmBD0J)~uYf z4fUXZgboFIE9Ro%{A3E5>D^Sdr#j9Ug}>oFFnHx>dz?Ba{>BDY;P?Dc6NAlIYg7P)K1~15#>-h!3n+ly4-*k9y3Fn+d=E z1tC)4+J&v<6tUdm-3nX&q*mCt*J;BGV?^$Qq^SI_XsetaXgvPM3*dc*rf^Zo?OHj-328Vk*-XO{kb zfCLrshWW0sM#H})P06loM7M|o{phX&S#FsiP)7g@CG%fmA3KxBXH6)AI@a+=P}hO$ z*n(@DPQ{-AIrKTc{co*J-?V>nSlu6!{rY)i%4BwN1oA@#?}pkm5}o#gb^`G*t8tyy z>@qarVl!!f{Ji%s1W)o8NT;SC@W+=GH_f|^8yH}^$!O-Ht9jZ9 zZe2bWASbfe$?$6LIpnNDV7cGL#Pb05Rj}|RvqHkQ1s^f;`8aC0!oq%TDOjhR%wa|u z<+v(=a6Jq|Ll@hvdxGS)t1E(5ouu`XefTn1$JVp)VFnz>#;m^&IZpI|oaH~#wz82& z|8XA)49C;GQ+Zn?H0Ao^3vmAcq-?(o2;f2{jcE6DKZ?7eI+lM>4E=-FMr46``C$J4 zd?5Mb60hknsTvM3(~OI|92N`=5ep2A?mwoFm8=Mk2Y6wOVt&EoHkU7x6{25T3z`X2 ztAx;gi?$?%m2n~wh9GkaIBu4P@oY17j8FO@ph!7fvJtt6&PS-K_zRPy=SR=W#p8|` z+UU4YSNUQp^!emVVMi{vFCyuXRCIPhmMY- z17cR=7T|}TeK~~o?^Z^esrEuOyc(7J@Tv^*QD2fB(bZ3gW>&j%=@#v$*O+n}uUEaZ z-J&e#q6cUc;@$i~% zz$-k;^T6Q#a@)l#o?z#4R6>ZUvSR4((Z?s9AP|6DHD;_m{GCYkjztpFSGwN<^U_&j z*r5GlH2gR${`F1;nm9S1I6XRF`A){S3NC-d3WJ}FM~I$O=8Hg(Ih?uTm8`eqVDdF8 zsJ?0~t{!&kVr_E)%SPx|eYxLF;>@4iYpG7p7Z3LvwD01IXXQ_2?Rf;&7mc;rF2=!q zOMoVO=C7xc9;5hj#6aco=ho+)v^r@YJ*0A`$zN7RT0V|(y!8RPzbZ}57;u}o;Zs8K zpW$D1r~PHCBZrbk>f8=8Or9=A55m+JVlM7JJ28_V80M{zM^qu?$jUh9IE>Ffor}+7 zN|6z9HPyn|1@>k09P6tGk`WZ7F;b}6*RmMQubaG2B`{-fB zI!$C9D(vfw#3T&xVK#*>M#-Hh(rsDX5ABS4D-NQcww&m&0_)7N+t;@!6>PRtO6bxK z+IroEr}`6IXkD&F_h7oCt*BzG;a50^r=$ysi}`KwSBt6=4Q`1)jW*(j|C<^MiU9Uc z;?!#n^yVfq2P6b(i&Jk2(gOw;BZagD|CNX``{a-4?pI_C7n!!U{8+p?9=P6_y|d5H zVF#3UT78^^fy|iN3t5*`L*=0zWrCTF8qqhlvQW9T4`<8LCB}4AnL&q*wzlY>K`tlQ zC_Xck_Mdh$ME4>0X!hyrc&loaT)^Qg7kloNV;jThSEWEJkwAwUv5SjgKR1+SD0Q4| zWR)IO%ef?}y3bTICg;{;o{meergWe@Byt4TgFKtV#V6i#k<=i1;)*eO=|0Wb>YRAF z=pgqUIRY7oce3jpgNmi8w= zz=tT*hs^o$T}bm59Wnl(e5&Y9Y^k#R8?HE_(BLfQhv*>l9X%o|zr-MlqRo%Ma=O=+ zu-&~j2v`28i=^WpM5p^6*fYwW$Y}ee zS!$M`p151UURD@zfHygqI|ctU zQCOot4cOmx*)@avD&BYg&?+P$9m0@Eu^FJAz*H%Om3Ylm;{GA>xa6gHr9geHJut?I zW~6Q_nyUL6rv_3`hgC3ktmd zWJVuTP^*k`s*F?;!na2iaa8os)g)Rlw2C8avMMnDTYGIxc^7IpxCQsJG6H`_WmHD$ zm~yeJLSeE>p}SH#=S?AtLe=}wluo@dL>{u;>lYvMDXvU=ZSglTgV@Fg*92CGu|n~w z7E|b^7|wWSS3r;vz#sj}?1*q$wu*Aynn1S!=?(PCw87NRdoMc@dsZIo^rd!HGjnDd zPu_d;+B>^U&}Pm-pQwQG+S|41vvwDaeUWVID7;;+xWtSKR=6mx>X9!!@L_0!9jidA~?EKHNfWlT>s`<(I$Q1!sl`5C<5BJ}5 zC9LodnXrU3rK&cYQ@y=8@~I^Uy=tmP=h)A=jYLP4tlN$A(Sjz|Gwzj?$%jYLK~Yxu zTZY&zaY3tELmWzF;a)wk#CB`(*`)u{p>9bdzI%uoV_T^y4}&~+^bdVKce=wK1V9fq zw-4FefO}j|3CEq*ZCkf9jv5mW!`~m8KZa1AU6=H~5%i(I>O~3?P+)UM`&6i7+Gt5B zW9jfh>?$H1cS)+ub0d_lV?SE#386fu8cA9h=@e9z&tlJcvt?w7JpJg9Oe&YT4^&xl zu`1}`*JgTIf%c2Vm3OSe>5s9btVqyhjmk971XBC25Q0SNrfi;JJ}0GEv=mP`wV3ex zKP)*bxZ+Gjj2c`pg3R{HgspM7<4sMB=7eJO!WykHqG6jUg5RB++Q|DF^9+#|=f0_% z(OR0|o|y5h7(??mChc^m#%WKVPV3^dBU&{QDT%TF8IjDfieWN_uPaz~-cGnAA{fY3 z1g<3C_<7|$HOYd>Ke9w&(<9t)EK(jw+z7~5i6~Gdgbe&s^uNWi6MV}R3=pA7afhjo z3&$OOSUP}GM7xG~iYD)Oq9Ejfp!)sxJSNE_TSL3R#_NKy-#a2a$bu);kRmvhly;Ih zLa3@R9AIA|29q}DYqKPe4TqH)@Zi403dp81_tx~atT$Jgex!B$W5S|bjq5(Da`}J| zUE_&JW7uVo+DAxxaFi&ZZD0%53vaB#spCU-&_s%>pJ&)AcFSGr;x4dk;0@-)t(o3y zB}6v1HOIL8n}&4wT#YeLu$It2@|Q&q4Kmv|y|JUdrqK#YFU)yVgSX^XCpe$%TwVD;WsLx!1QDXj$`Tzh*o?cmDRi{8gCwJS_|duNe?J zF@a=oNZRU7z|YRc;Esy{uu3Qi5>W>tLWH{O_MY+B_i%aEUreDr8TU$l_NYMB!A=C`kwr&iuHS-+Fs(liUh z!fjkOS8XEqGu+Jrlw?2lO*sX`E~;8Cy%%5uWtB1uv+kskQ8S!Pzw?TD+YZYH9+x&Z zXVXe?B9v{GWOp`6?A2whe}~UiwoT)^q$IX2TW;}z{;1WA>F#Y1<8G;QGgWSSsR$+r zpFl#IG+bT*c0BGey7_DIQjn-Sl)2!dp&S$C-p(CR(wJ_wB8;X;jbV zG{sB9BOd`D(3KS^X~8s8aIP^A_Yi0Q9`E)Fr^I`WpNLdM*{=~=$CozMQQsNip;pM( zigNQoBo&JUcvi`zOAM4#F`;2%MRECPP)#xk$1Y@Hegw7KX^a3SMu6qFWKkH zBv^r2NO9P2W}S=cjz`wig$TIBr?wVnotEg^Wo5(&H*Lq@X+{++Rq}{w$`6!d@<*^j zZ#pLjDXi5P!*kCL#=^ClW(R3D)2;;~j8ABl8WVH@P-z%qZhE@hrdhSd5dWFiuo3 zwEKK;z0o(YP+3V;*EFw!{=$;SV8Wf%I{V0!AJYBtnU~bBl_B}GuRQ*Rd;I>Dds<%d z{fDnWOVDn^*Djf2hRq_vw28+6F=*>od}0ChEg$S}ty`E6mHy<{|A?6%8sTiF8J(IQ0-*pN<6PK!)Nil=(tz2EycaCrkIeG4`>?&S(nO4e$k z>soFhxmQQWO_cOD%p);shY7Fpe5RqbCkLH=TJA&APnEp% zG(&ybLN42%Rfi{aePKzdt>&>gD*3*g`V^<5oL1=*<IlZP_$WQ_*I@6tyFRK zR{6Te7j$E^;>p|_2WO(ylkG}3XeJAuw6P)tNmMn->f1iZnYKhZAefbj24l*Ca!F}d z3vE}Ur&Y1dSz#Ztu=UkGg>NOx%H0|`$MMv@YlmNfaI#{~Snnh3tzd}?F&Qpb(3>o? z&+4+d<_Wj!md?%g(5B(y?Cu;h3+>w{388tR-F?x1{KX(U=^Ih! zY#=d;UY;Gul`4Sd1%x+2 zA?Ff;K{G?X&O~@&A4~Swl#o@Pz;a!~F&ZmP}eM3^4kW6#2^O(dofup$y~5#862vvnbp; zXovl3_#AVn{vvm5pA#>dAChDKP#IxwlGBq#c)S>;?gz+ZPn&?1rkJJDbJAnoYBS z-q^2g$W}5^Du;u#NELl{6Eced;e{cLKNs3o+MPw@=;-Z?gJNNBtDD(9n+FX&8=4X+91r8fwo1blG)Yjx6vIr6Et9FZ{<@Sx!Y(rke&>{qhdn zngPUN7W3rb?;x%K7WX)m_Qp7w`P;BxCNClQ?GfVPZ_=Us*z+1zVE8cOB`WJok?p%w~ToJV!>BgaZUP#_1#k}`bAHRCr475gW$U4s{RH002Hb>xkwM>qXf z+?d5d3L3LJSC`fl6FSV_91Yg+<_*7hbJP&7i~m!Ph({g}PQ@5tU`&i)U?l&;IKL`! z0242U$PgAgBxw?Q6DbJOWE2V<2ucRTr3<Fk8WlLp1jq%uVW->Z6P29nqn31asCr{f=%scuE_gX7 z3=qKGqZ#6r@ETI(PnPPCPEXvq#mpkz(#2L=c)2I+O#Uo+$N{TQJ?#+jhgpSc2Q(!w z#NXXTwTVl}42kz1jygmR!$@k6V)62cOEk2Ra$^_llFDRW?b0gsAU+h&*Q=bHMm8b3 z1^W0(%&Zb@-wYO-q0T`&rFRaoyNvbNQ0Kn>N(o%|p?F3ZCDQ|b8* zqz-pT+#@x*+8z0OW_(IP^&>a%sxjFmpx-F|!iSR_z5LQ?bA|)NDX_$77D5Ci+(U>4 z3*G%%)*q;`_2vus2Qr%xl9!!RtpXS94!Z6tp*nR9aPn`J_+&w?}FYBVF#pVo4 z4yu}&D#c0b_$&>9wE1m&U5!{<6n4n#$UH9&lVPPQ8jtq|NEBolOq(r%C;3=^}XK}@nDmgo*z0kbc!n+BXHs!W9$2c;t=Q^rA|OP;pxH~hg_}qm@ok^CbdlX|$*-mo8i~Cd zXItR4b}%J1XKHrXHY|b$3JDWuQY_XI#eLl?4?xS&AAG~Nxj6C z6ko@-B6)PeXXnMbjoub;iqH;ln6(+gvEw6RbZN7tPeUa;oi%ED!h;7CVjF_PySExKawj{Yqy+@Xz<8Q{* zoXhj14O5#2lSk(Oo6kIPpd;T>5Qr6OoFQCf!A?#GwX5D1eGb;TF4`?i2EUu15F&Ws zstWP$PfBd6JUP|;$_C60errDm(iI_WMprO|F_|uI zY{fKgaSy6^S`k+TI@Nhxe8QGpLpX-pr)!#P(@u*#up(Hd0?%Vqt?oil1YVl@nnUZICQf})qJ%bmf$yZ)Os~*tLtw6gop!W%ywdv*Eg8jk!#A8w% zevvG=%dB%wy9Ce2ZL96AK~D)}ZGqFNF_fgJ?T1_^Ani{w`NdOi#8%u8_5D|rOUCl3 z8Y?@Hy&)jW;Mhg`H@cNOnJ#*y?vHSiP~jal_;Rbbr;%crsjl`xR{xfTYr(fB5Jm>S z3aiPSa#?Ph^*mXb;%Y5Z6Z029Ro%TKvA~dKIo^OVDGv97$W0!4Nw>~RJ80d(qU-t| z<8oPnP>)G{>b_u?yx*egrICkJZ!nlVn>G&I@Q03lGW7l+mIeZEWfkS zjK)wrMS{=QT&7E|TRo@i)GepC$sgf9`=H&ae<**Gj1?j$hH>kSbX<9B@@2OjYZC_k znwmWG#L%E*H?lca>uN|zQ|SKtqalFbioTCze&4VjZO^lTZJ1MfsTVdi3rW)YO|d${@T2rv2)K`z769l2`? z;`HxZhGKsCrn`Rt@zUES3$5pM!Y}HDG8dm8^ZQ(!TPA%S*RD~A!*^H#o2dYmO8#3S%xzAXAIJw|D!X?2s|>rYz+!x*}_VZ?GOb*oL$FC zZASD60$iPw^b}8=HdZVsy>kwChZ2_&Y#&+gSO-*iT9_6w+5lDcT(FWWCL8O7^6dUC z2MJ{e&#Y(ZYwGlrxL&MGt|*$*A`{uj%Vp75a}TS|TerHvot0eKewxN*5KZ;YEwmhp z+xlfH{fHem=U zWu4IIW+dWZn{X&4Vja%?A^)EczFdCKkE$Py9u7y~e2Ix-(G-_v z+gysZUq~(F70HV@mR+N|YZ@PR)4kfZ&#wBzyiX6g+yL%6{H6EaI4joV`#R&*Pjpy( zQe!tqQPLGV{4_?Q42omJ;>4(8ng%Ysv&?IMdUgUz9lx}a*_7fjB?Ii=AlM7kac4@V z)|6<3)31SXMb6rBaSn@Yw436xqmk(KOY;wB7n_!yj;IM3sL={knYziP)`s$VB=KDL zdmk|dRseTk3HMH%pv7h>(b6_@GBh$OtVCOPXxe@0_Bvt3Ox$jXa1BA-?!^4QCMhS8&c3zXhzAHLgwaRP8MQUmMjALLL|+DfQz1! zx7K8?Mwzl|`I;Ks(v*hYS;#7e%Tv-BfsWjBA&?cvlj$he8DNzK$ja5tIJtOOuWKJM zIr+FgOI73qF4JXmQh6t=AdUl>oGv!{t8*xUmNzGH=*FEQ+oDv22pxu#p|UPLocE%3 zs_BVaDGgKa6}Z8ADDa1emi4(`1lRd?5c9@Nr*QMcn}_-?;bDoKC->EKTC1q5KGZ=C zKt0T6w2Z4Yb1GV5yyzo%XG{jus1}i50^HFI_zkoRcwwH_g5XnkRNcq}!knQPvBY0i zUjrm0L;llL#JTogB5pslC(X%;jx>h?B@r^>+M>@#8z>Y- z)au-|AWM$@9IN~7TyyH#^u#_R_^`bgc{jEfO!669o=>(=A(*45)HlGA1ElHoK)u)o zvD_=_xKqo~#CCL9o=M*wx}z8NrHgtGf-5|#2^RZZbydC&BUAr8DL3{{SVAaQ=bEs@ zX%cL@@)@N03jaxZ)3|A)B8V6YbccSG6915Ot!_zJQL+L#7T=NX9!Y$1I9iQS8w~T+ z-#+vgQk!?Yd5Vt7xN+segU^=YJw4JiT^EQp6Dn7uW1tG3nznl(=gO$)3bW#{#euMD z3n?sh)ZhH(Z8BGHX%Y1jxXHB{R4{uWt^Tpkw(2GG26)@kc-vx+RMUp_^ND=`4e>5s=-ZF!)rvm~Su6vopBxEc zCn#R;;$LiDbF1ngW=3OxS6B^Bw);iRCc3=njV6__^q!AZHE_z6K8#KZ8T6+#6a*Ck zmldzN{36n9jgg4J(5p61(wm`lIOh-LZ3G+pLb_M?4W+(t8AkvaYa78wKLn*y^Sqj3 zYNJN1KV^ASWsM72pQ*^_TyX7*U)vw0*zg@|#N9#%w~N@!v`Yp!;!!k-jZ+X(d@$}o zwFH6j{;+%_O~a5{))18Ew7`UHF4@qdCGC8q(nV-y+SF;#SA$FAZu6RNEb186THk_L ziW*q%713QsyGNW`M<$p#n4Jd+rFA8ake`+H&5?G>6k5{eSwXV5^o_O9nSZ__EwmNp zBQMdP^LZsds-*)hVuS)EswLer?I)FOH&qRSHAyu8;1OIoKZpc!+_Wwgc z@Q1wFL59}3SY{~{b zU<@182ueMl)S}(4(2zqLwXB1i@dLk^gbdb{unEJsLccduTX%Wy1W+9G zIbkbE_z>3!eE+*0pZkDALj(o&TVXjshtaqNou%_TpuG<1s4pqh_d(r%t^##tb~~QD z_xvN^1L`@J*u=;SuJ4o8@=F}ljPJA^xqSv3BXf%x{6$-{BGDx5j=Q2St9Ndr?9;gO z_Nexb>+6bWonM$=>?p+l7eS!9?@JjA61lQLQUo61Q#7y#rNWyD{@EK2Dy+eQ51=I7Xr@)O=tW4KQq6AYV6GvZrqBulnp3(E;z$Wvtt;7Mr#x<oN z99qLKat$Ao+OL>HE7iBd%!!sjXzd?NT~T06(wMcva5w`SVn%H<1;+Ix_;`)4dzTWvNA82qd&4P zFX6ebzfftssAmCJ*|8ApzA>GtZ^luCH`?wAVSrjy{?8y&GxN_rs({!tW}Huag|dGl zb^e4SZ!j@0O1$A;={q9-Aj*N@RTN4x7w(L>qCFV z7QX|%_zxAU3#@B7|BTbim(4at$c$0OchxG^PvDcYXVCx|7wU0!f3gssjN~6uW%mT; zH=BQD<9I!BKll_i@P@+@G+PLACjTE--xQrm*e;uiZQHhO+qP}%i)}j-+xEn^J+aLR zPxd}%?X&)U->YxBx~saLdg_JjWb+PIq{a109yx;Rzv!ingjT}B4m$BI0+<^|PVqP5 z8GnF~J3b?=TZ7dO|66kgkvl3sHE*rH-H}D1p;qfz^>93819D++RGGciFPGMYPskrt z;lE%_rYO+Plzre7u$X0-(JsV%?iJB0#8jKHv7Ys?rbQss5&bSZuN|ZwxsQ{vbDedf zJ~S$zfcRT;U{!FzBtu)13P?3^K!DX)@oyP|B@6CdX&@!$sIGL%NE3fIVHq|lfj+`V=mAX3*?fY@t z14D3Auy}~_*U5^-A<&JK4&2Z@$AmXrkBh6UQZP*gC?}_p9EgXg=Wgf(RoegM&weS=w$a-Uhss& z7U0)^2RRL|v6B~lP?EE+VVem1{D9%4rRykSe*ppA{5bHO|7~{u>1P7mq2G9b5m=*v z40DJjpbJq_QwH_gZI;-_O0Abh0ZI&!d~ZQMD4pDOhAIhrr=~J9SzOFtu0D^@1brHiO8amoErs__R4?1@AJ9rTu{1pr{F4Lfv8cx_rO_Y7cOmq2^`?UPb z>8BcD>t|F>OXRfPRSf}fAEYKQ`*YRa-{#O%I5C`&YBUZrnp=};mzkF2j&T~_!WT)F z8;|MG3DIpHX)XtM?zbp{dPH$#3xiScc5IZr!NIB62!zeS`RkDys2u0aZ@s(n%+{4l_WVxkttUk(NwNQSq9)Wi&n$ z8Eh|J{lYaUc6Y@bK~t&LEFih;jFdIz$UlvG4og3A`32ED(JZDWQgorzIl8_b9+os)B8=DHFV{YuH(M_09w(pgR~);*j?mvpjshz@jL>oe zYP!ji)OU)<03i&c+h=fwJR~FOg!&mCbQyO{_}Nvmi1z-?Y+l>Q*Y=QLPxS$cTPdh~ z<4`4DjFB=j-uPL`vArhRZPYuiJOKtHNQT?n!U%?lS7S_rBu7*O_yd~i4>kOs?2m!P z84j^N^g%YQKTV87_#Gb9?(j>-Pz_@*!3e_ZEgcdA0EUy%UN@X`VrbkE^$Jg@0w2C3R?0r)|ZtSkV+VevElB{=77`PzX7zUehd; zWqfQc01{Fb3rzVUbKcc$h3Z7rMQS`%wRQGT16Adqr4OrqmCcxs*(75)#e&*xP?eos zm4&Tt?c!v-irFg5^i1qMfIxZPBXe?zxzU$P2 z*swN5r17cLOH2L2y`zR!3vEexX-lE?I&c^tnang#skuW*W7jwNr=k7i@wsgmFO?u6 z!MjI+yu76_|KjmyUbJJ&OG@o95o1%04gEUH7VJ&+}POHFKgWhppy)EdhEDN z4X@-VfPz-+0~8jDr{oA$4%!_Fmeq&mNV2E&2=%cUZxrqJ)%F;(e~^ahq0(=iohjpm zd1S8(bXf~i)^pt!b5zktDUx?tk$ zZOq(O^F>s`RH#MW2IFc~`o-D&1{;d>$75KY?VPhi`u@scZN65;^fhd5+S0$-1MFc= z?XGi~cLwk(QTnJBa^1=i;xJfMhIY+9f>GE40m|#np*fMtXyR-%=clW#2jF*CcAPBf zU6sdId^*wo75Sd=FB$No;@L1yhAJRAt~PZHD~*4%Q`Z5+v!$+;l?^YhpP$MH=H@s& z{J1vf({usq-Cd$rQwOIEyznp00N%c?#ocXUX{#vbam9#EODc^eGZhra;-_f`QQ4gy z@~!X1j_%qdr@jQQLSN@^psUTdfh~dVYmA6k=WiG;9I#>XcEihi(k^uB*A?N0n6^cN z$!1x@?q!SL4NcAV*c|N!ok$nO9KRA7%ClWGFdM-pJgMqC6QO?7{k_3==W{|F{40y>dOaZCz{~;+J z??eU}LUH(A6SyGP96(us3lM`4*DV&wYmFa5UNlCwl6}u`NG3}>&b|{xyJ7=vyfcOC zUw_ps44cdR!#wNN9#ohxL_VTBOYfa?k+)sE2GAvp=f0)=^vF{5 z{!1vL%#%yE*mlk;W*+rOPy`7IB{2dkMYK?SlR*Fzt^O-ZVJOn@-R@DtsXT133C@7` z%>@5bS_N7mXCcabLHdoF6B814MJ2`Pig@RND>j6ZIC*QJ2CGo-LQDUaO;_^p;fNwTi5 zUnHbb*GoY`I+Cm^@|B3%mWI;}j7wB|9-nw*HW7`MYvL|K7l7;{uSVx$kZd(e5_?Aw z#~?bB=mfig2Wt*4nvz8LQ`$3)yY^(rbWN6k3;>Xj5>2=t*Y3``MHzA1U5SzFh@D{e zUN0=GNQtF9WR@q}NSNJm4%mvRbNKqIi3xr?JKJ0w&a`m4l|{aynEqHNcc>-SaHEmx z2mWOjLyL+Rk;L@dpO4>YZIaQRb95?ie->iBf$+XTnz|_q>una@{J=_ z(k(V1HrY-AMF?@!qTq4kKw2gr)E5}d3SqAp8k!hNQcV_31PpmMfiCArvLmUhirk-C zj+DIj7UZ=chQ$LW#Vk96oTZ87A^-F0`e}C8pzqgj;7RVEG}d1u;Nv7iL9t*7C{Niv z-b^rNg)v^Trt*=+G8V0XS8PjjbRm>O~{Ltg- zw_d-P*^|BHz71`?I<=S9i{+Z0)oceq6}YKkMt_EI=ib{dhZfA@8Ro~-f8eG@YXQ8O zz?npnI0;2mD&POq2uFuWY}w;3KY_?^>yie(HE`c7iN*aw(BG<_pXG?5D@%!=clCy#n=yYxwFH>*hjm(F0yxdLw$rn<(rcKeJsyefaw+q zg*x9eH-fU}ghompEK4nPJ-~-1DGN?jG;;Y%7(oO_fm(^CfP9Vk_W5V%=_*b?oNBG5QgLL)LMQ82GYP=<9v2yA0iRtr9%*DvN8P^k$* zHL@`fHc4ghb;0Rud_u5a%qZ68-~X-k|9?Z0#78*-+RxG!?oX(J^1rlW(k(=?=^`TF z1%Psb^_@re@9;pA8Qc~YX(S-Q78Vqerg$x5e?_FlHiD>zOUB||aD}Q2rKzQ#o^+!*A7o{GAHGy(2aNA%lqXCU(fvwh5>jz*mu%h zM2v)pr>qd#4)uW@bP#8m;K1g;Lnkc24dmVrd6xWz-KqJFu0WX)?!vrNWJol#jC|tb zwh8i5w84L4dG_!V5?WR|vhu zoy|YqV`1jU8931lgqiyC0`oY4soux%dLq;J;|i&(f170)gkdiA^3NMr7;6WV8}jr{ z9M(}u9v}znt1|{`foa@6A?l~h)%Wpman5Zsv94|Jaxk;bu_R%hD!9VI!plIu;I7JB zUt!Vl?Bq4mk?{2D41CD5WVf|c`$EVYs8yoNb6+&?G!ea7r+#O4^ z+muKW=|N7zJClzgg?NYPq(%bpr9g9%S!8l;H^T8T7*!N`_p>u!MecJ|^!*-k`!kmC z1%oR;XnVrvv{XEGnZZPFBoc2G+ZL~*&0W-%-`I@1Z6k0wl^axMXw_s7swimjfd zFVb`%dO&wzFECaZNA0;CM+_&jSa;YK4V}&BMh7RNJ>9z`42z-kbMJrwXeT zQi_4yGarwM%B(H`FW>?inO%uYtj|Iy=!C`7khNCHqumqcx7T(1-QzqaU&CV3O}k~U z30Cduyo+&iQ^53vdEdTGBdcK$5`J=@5)SnA0>$?9t+FHOD@IxDXJtp;Y^^d)w%f=~ z(3UHlS~@2r!6}Tml2CGOD3nIygb*7EpP!p zwKn6jH&*fWJ(nB##D%vE^|7r}U7j1rvj_&&u>}HJ#e%X5$~Z~LJX1+54XQfI2D!;= zwtFvQy7ekjF?61m_ToZf_C`pk>F+N0F=&Sd=?L{(o=tX-u7;&+RoMM((v7;#%x60M zFjZfi-GULlMlZncgqB#T9!+axZM|G#vfD9CDip0g_CEe3cB^c*s|f$BN(+)yIjAZ? z3Qua(&At!q6*gNwPc=ST-Qa~9?jIX9T+ue@SU0|)BI??McT&K3Lhd)gjN{x&a`xNjaR%ieLGxjGUlJsKA)3}g)rl%7# zP>5Fy*;g`@bKg!fulhw=(}e+?9Gqkns!?#Ly0Rkg^PzmQm^|sr z$!U~Xmh)0Iw0R?>sj5|tyz<)XV749PZSIybik;Z()lkw|TX5p4-1O-a+SbJ#d9~|) z{>hCuuANcmYlto}X~210S>+kV`A{BJ)LVFd?+=(tMa(JUc;GIZr16Yy5iI_olY-vn zHa^64lHTcZqJ(dkabj=SY}++14#lvBBG1g4`v;aWdjsjmV`^6hf-Zn1z3ccS_FSBt zP1PxA1iMjM9z2{%VE$q0OqP_Yw2J_83|Y!dzPaRiVUcVri_}NQZ!5a7b++0s$NuUj z-UqNqdz{!D=tSR*kAfCy$oe9hDgp?%SpL!9lIU8UtA)Ic!EiXWuV}!8sgjXAEaD1W#c7cSS zEi^~JZw|O9n>W%82aTSRLg;N_xRwtM{ z{82?qt4?2}J*Fwhf=Cj>nKgHuxO|?Kh}x};BKK&>EnM<`CLADNYSN@B_y-2cbKZy*6TL5{lheutUtvV2&QR(SS;<^UAh^UBOMhe$Hf^@h2t0OX9r~nNDyPPEMjgOq z;gJ|l%%XXP%gz&S18NfO(z?Q>Lou^rAZU{BI0!xp=j1N3No%aZko1Mb8)|{*hBp@| z#%fP&voiNRJgq7Gor!abU0oQmUF$q!JJ!lU{f(0IDq4pR7c-*r%RV3IV< zlJu+PAQ!4E)5fnk=II5?NQV5X%f~_XVTnQaZcDLjd*R8@cUYt8!cQDu>H(Vexx=j! zQ|Dx8Z;BGoDam7vq&xy%{yvhFkqXW*p zH);`ZFs|pCxX0}-V7oi;i5NbNXi0fN`Gj~DP1%mA2%TJ=VO=;BJ7q~I5*QUzSxhco zNe?vTP2zB{DEJ8ET>f5NkmvSw?5N=qAT-+&I4%YBuqb2^$_V-iNdSdjF-VB&6=BO8 z(Rv=?PRvl1N^>Q0Mw#B!i6sVVizt?CXp@)5Zz4xHHn_mE6&uJ_`8wYC4b2>6Z*6Ka zCl;eXMpRiP56=i+r!ZR1=p)aJq%)*Kf#W+98Kv~JLVCJd`tB@Z&}S2p9a)QCiKft% z24b2cnTB*uxpdwvLcCT}ZCRouT^y(Eje9_=U$L$Fd+DQm`1aK23y0^#dvrnHd*8tS z>BB9VMZ*yM^iJX>7s2qs9#T_J(~M8awHyHhC15w9!_FdslA@XWB%k3a0;u@v=@bH~yRqfw!Sq;7Xx^)7_h$u`A&!SV+*{)h%k8-JADWyUzo*asTxv|=KE z=S)uJIP`Myu=x0Yechw|Ayg-2C0URZQ5A+EMmENLG=_ZP4CA_nnJx#7V~T^oJwWcx zNFm%K8YPLN0+0VEO$S;C3)q(09usp8%bBK0l+}+VeXOC=nj}|~(@KjCG9DWD)?HS0 zNiu3(UH@=^k;$QJh<>SIYDe)_H?l+uD#@*BHs(!U^h#3K15kjePjZZzA5+|8RJ}#U zGr}!o#Be8<+T=r?Tb1j$!kuaDxLeR~gysC|)SC%rUVMlF3Ly0C?cZ)pdriL!qy10m z9xp8X`z$q@D}!ZR8Rirc-CdgNiqK+=`L6UV%Nn#!*J#hzal38t@klQS z@zxKJYk^jx0B`W)n6=3qI8S&zgj=i%paOrMpssH%mbz}?@=CsLEO~$o5E4s?#-8Wf zJ%Fq}fp(r0Gg^DUM8p>J#MI|3Q4DR7YHXpdXhVT?8p=WKC?r(L9bvXbg{tirC&Ye* zBmKb}I3uzGSz`d@mh9UoxKqD@x&nn)AJ4{WhQZ8^-KPUK8+ z-L^Y``Nu4)Y}}58uZdgAIruo+9?AnOq%Tl@h!Oa%H;M@UZY>JQ?T$IxXya-Ccv`CM z5#m2zz!EIcqU2a2IY4tcA-@J_AdO={6!D1YEfFc2T!Of#QDd_OSI>3kRlzIL2YqjX zDB1iF!ryS_8U0!3g(MJ@tjn3HnJ;s55Bv9r)iwekbEnOKI3*CP7`J5B+9FHBNGTAN zdRN(t45KmCZtvV^>v1IZLB;W8wkmS3<(8VL3r&R%+qLBqKR`ZM*b<%u=a*hsVuR<) z9kYn@coi~l87$4_lbjC!K{maXd`dU8$D}T|G(xNtVA6`w^rq+0?%!eTji+$Mr0|;a z_)%<_T%{YMGpTt(2yWbUS@nb4m;a*vQ^#Yrc-U9N0?k*N4m}2&&2oBNIbDncBWZhe z#o-1zmNzew5a2Y!?9^sry1e;R1r7&q49|ENV}3;a9U|6hi?iU>_}xB9nG_d#M7`~_H0TeV>E1|b$gx|2P_^3 z=CR3{&3?=6P9(erhv!;S6jRxOIeHu8ETdO7xM>FG3}BsQ3cru-^dVM2Eo{COhwri% z+Q}c5fSc_NV3}b_@MY6!=XCaPw0s_nXK>H#(RL-GAHWR#s9SmlS0W+!ges*sr<$Kw z65cFPY{hCJ+X^vF>n~)St_s(~`b;dulV22FGiZo2mT33;ERI(mIIy@r#=mTiK={N` zU0-|O1Q4A{jI$OEDPqIK+L!~68O({njK2sU6}ROd(BEFNk0+?V+%aT zIT>xayrCqHcCi;Y{*O)yRt75b+*j}(o+m2iv!uQ8s{A2i?%r(2<$BZmRT*E^=R>}M zKL}%ZIJ3OT9Vna(b6EXYVlt7DmNt~-X3of8_@1M4lG`l{RL+(WIac9UyC zpIvsaW5Cg6!Qj&kPNRFfFg<(2G-0LME_W{l(U#TPY`eECt%@}Am`<+7zOt3*S+0Qe zFEeb-IRc$dpV1n?t!#~w!;YDMc+{3++GePKWD^6_h3!^E|#w$$qXFC{+U{QUs5BV-0M!ZP!|XK|pqQ5D5+?n>ux5Wr!NF~W5It&i zjBg#MQ?P^+=c7pLi>m;5o(!HZn=*;x{8|a}xU!-89TlWgsW zHSw_Q7-Jn46-Wtd2V7~5wr#|@3%xj*qT!(<1_2330}Z;^o@4Fb8z@g-; z0qfF`C#fSTaF(yoQi)6}q30yPonG=k!#{V{czqwtZ+`9z0Car!mCN9bQX4Pv{PcDY z6C){gTcL>xE;%!H%XR5ABP>*tCJ7KFNS3-IeFQ%L>J~YWavC|FA4Nwr$GD{*4?Bx_ zWReE7?$ws3CB^K4WD|_iO1=cv^p+__Wljr2(L8;P6h>NRhL?#31;{!Lluu5Q`Qp2T3Ji8*e3+` zB_1Sj3YU=(0$XGkpi62LW_E2gfLyh0K^K(Rkc(M^1UNQncM6&k5&k+eKdOwykDxb%DgJPTVWOx*Dgqo!g zx+ha3?N$V&m<`Cs?+Df>vk@b{l+hKyH3-)91>yMvi)@L(~UeUX!yEv}a> zA9)J_!1pcEA98k>dL=5uDO#>Awn;u58S)<6B6E}B5o)5o5??es6z0igJ>ga)S2Cl* z5`gbN-?`mnZ;4B<9ICdUxLfy7_2V4!rg<7PA5C36K!sYoeqLEDiEydw+F7lOD@ zt0?}natTj+vMu9m{YJ&WfWcEN;2b@NMnGfj#x4coD(Yt9X}3Q)yvqDZXU`f>LT)*K zP6fN&R(wB6iTzUS!%DWB@R7k>$X)CCKVodmI~_olIU?2U=h50q{a?FM>gM3U>#?#4 z$!sj^HuET4RLAjtY+Z;8N2rqB3S3#^3Gnx~Q^5DS%Vi|tz>-)LWOUIsv25v$DS*QA z`9n}u&*kgPg*>Wz#aHdo-Jp2L=8*IV zmhN>!3MBh`DlsP1s=?ef&BV*>2{iMnqit*&FWc`qqbW0^!WQun&5K(su(!W}fXRlg zLRM?q9-GCfjJfuHW*-YzchK!P(^ih0`)Gf`4p89N_6jp0ur9Q+s3%%YiV&euDld z@O&?oXXbvEEy^ku81f}B}Wk_ z3|{5xzusF2-(e@O9G;x#JuJz^cEDEu4%1(Dwme>*$N(P?Pi;WjccC%fTVzGQJY?D> z#HrX*P(%;RNEn>-k-JZ0I;Dh}gMLAU#fYuZ76I$LrbHRm1C!H=V<#y^`}|u>1N@yR zA8`aFVK!c)Ho>_*$bL85ih;nq>xaZQ9(#t~3JUk~5*;Y={lqj7&<~`j*BeNdeM<@X z$rX_N))c8V%IvAN%aDSaMKZkth4gdJDz*10W*wc!3rwS*ly4=qqf1=S*{3Qh8N|k2 zni5SnI_I!zE!vExSTLYMd?tgW1#rVvD2S|~-Qm^)MN$wm1tv&N;A*(ILCvDH)Cn_y zfM!KsaR16z!&_0dYHe(^X=1N#Db^!dUNGaN-%fqOmQrz9WM|UnJ@PL3&Rv)?l`-_d zY0|OE2-_rg{Oup6Pjg+dAD5YV2j>S|08b;dk~>Vvch(=<=~vatB$iM~51^?nd1KxD zdjo{&3<2w`_JqBN&E!`@?)81Y4~H(=&Yu(Sy+`- zVV=~MsjWiqwQ9drJu20deg|FECevTFMpxgst@_kYgW4U(hP;E=UR>VR1vgrtd;btB3$NYZ9pt#;Mc{PRFe9o2 z_I_6y(uUd{>w(%G4cLZ;|H}t<<3hU8xk$D88+)?ReFr8Pen047|WjWkQ zW%c5+-E@mB_UkZ=_D?fo&GZfKlA{%zN;L%Ad0#q42I)mtDF7W6E*%)eSij(BHl@;; z=?cNCmAmvn(FY4zO@^#e&SGqdt~wuKsZdg#c$xA$ks51;x^{Q5G}WQ(&zsfDKNhPV zG;Q8mo|%0!nlxpE>x?s5s?+fb@gp-xzSc3TOx}*JfP?7S%5EdS%0wvytkL#(GiA!R zpcex*Kanq(dH^|2m8?r9VpBxo^xnDDQ05jn_(H((txu3>){eWopNPM9{US=_R*?`< zLzNv3U-Qgg)@+tYpSl^)Ch}hqLn{z9d$uKXTzYHFU+2NneVo{*qq10)o(-gV4Q^)vD zIrB!(9tN10ddm5*;Z7eOQBn(X8?wA6+SfeSQWDt-@We>s6wK<2*LFg*+ZS<-isyx? z^gGjn@vetnO}4K(!xbI}gE{jB;Rdne@Z12Ak<4QaYm#8fCyP&ZVT{rmRotG{=RcV< zB{OgOx9_4FEuuJ#k!Lt;feNXAwFjLV@aPIv+yYpbkxUfapjclE!IBShjv@X<(vJX^ zuWxy{iP$e+?;xSXA*8mOc!KUq8Vv(D(h^s;>GW*`d(6Wg^x82JITaiMJFy6Atanr# z8NWES$EE0!y5!~7rM%@se;Udfah&f5eBZCr;+N%2?B!bL8 z6G=V`XEgNpcjC3ioWE1VcctT;{BF-wFSD@JH6h#=$54M0(LaIzWFw*(RGkUxJ06IsF`p~iY!CMehrM3sjO>s z2cE*aSck{OS7GGPAq;;Yft0(g)~R1I;FC$$Ih!v2CkYL_F-cKQDF+#5b2)Ug&Z4+&m|+I5_cHW)0F_$rEf%bpGU1FyYnN3vy|9if z5}e_%L$^&@`wjJmbJlF#C7U!xkiKz}1hOdSRpc~}{DPz_hy)#z2(?`9BbH)V4ee<- zmEL_E_S~FWuIw%Sg9T-8Pt1oM?Ud^=mq|CedB}vxMkMu?IiBd+JXPt=aRK{`Vkw4p z38lssfQN53h8;cju$n^Brm}r5(S(`Ek;ftV;BOLm!j&$6&4t(c99x$d_lTcO9gEipuHCgNIJKn2%~@WE|6@X&XuN zBX-Y{7P3)9o9)Z!OxJ}GN#Qmcq^sQGb5 zD+S5uIRN$SGvYXgKXIpsb;y*^vC{q40Fckmx<^hy)BGP?3#lLmD%zr5u9(odVZ_QK z08CeepUnenCqnCX|NI6n+=)~F$y{1UjsK#)tW}+J!2L+Z)jVen+Ssr1c zE~M1&78bI{n!Qz&5ST5%fZbC?MMB553{PalS!wOQDjQkV#eYeAYKLA-dMB0J4{DYvA*00Y7*8ZFtfV*R<^+$YL%v~(m%tf zK3%7B+dP9C|HQwiJW#ugip%o(SYb^iY#dNg6VwH3((jU&Z8z&Q<&4mo1BLB-j$OBm z-+c#KcPl>mF=?vSS<$&vvA4x2yE;8RJx7PnH`0@W()DC;edA;UXxb@pcdoT%q)#fC zEfFVJKmoJ8pLXp4uM?ETA*<6cYQ`9@^i-6Q~1)F(~4@MRb>w% zoivK@wTha@(w+sm%3Xp)Ist=ypNck>QB}=<4H^p<>7^X*zsKX97s;cf-$x$Cq9-|c zxcprgOMRY?L|(}c&|c>d-6iUQFjLWH7S0^qA?XNzEp@0Q*mR&_Qyc$NifFQk*EuY* zU0COn`V1{x6pu=+54rBXK*A|eC2vJ~>cx&+m=Q;V^zVC~Ni2f1hK1C*NH2Qrf0dlusGT6z*88#vwLhTkddfVJ#78H*XCmRS!aat^cWF%UIcG*+Nu z!v$%hPIss_+h9A}LM_xY^I0i*mld9fax6ch^6M-MMZl#fY*e7`miwq9gTa*=&vOih%`ApXuT3us^t}s*-QQ4 z>Y8tG^rKsYvODRZ31i#}Vs{2xnI7foQ>HO`XHRuo`}6aQ63xN_P89-g%!Xj z({-28%;La})*)}p!`qdrPcJRp*^4sD&oamoa~l{e834Pd`jST81Au%sM`3x!>xkTha$^ zc3&e-Ku&%w79Ai~-wPUD$+UkSXPLys@K&Vv`X%e-aV ziOV56C|X5?Z9fqfp$DJLC26YM9$LX}u6kxKaY|y2f~+m1KLIR$rUJ*R9+MHrHTD2b9pt|n_O`N*Ckr={yYex3GO z_h&piUB6qfFmk>Mkj&J(UQ3Byb2sNDaGd?p`{HKa=j-#0^oLm8_0dE;oE`(t>j$=~ zQ{zX8VO||_D@*mi3x*PO+w^#T@-O$Hdf+YlS30mFY@024t#N>7+V74v7fKf02ke1$ zv|hRc&WbPy_df~iT<~_YHQ~Ki>@+*`4Yy*a|6s+J-RS!EBM9pu+teREO4lEUdrdOo zM%kf!NVojfFEejJCTH~wDyDw36-H#u9NL8Mi)O?*W}8E3UC*Fax{gFwrjowX|2cXl zjzq-u^q1s(zLx+9uB-B)#%d@GIq_blj_~+|tCP@rSU{`6MI*JJ)R#1-P4$!Kjnr|Q>} zO~gRem*HqR>P6m%&U=wi_z)WpiUXBQF&FwC%r!q}VO#+vni8~Oi5E-88Rwr0b7W4m zZCLoCvtjldrgbcuzdg!=jRN#5py@+PEE9;6X-Tp(dVl~D@S}(|QkjR)3x_y(K=y_q zw8{5F;Q}HBy!C3l;f1|{rM;n*wo{+jHRbCnr$y&j$lbYo$=6iK*6$!o7?;42#0ANA z)PE8^(`^yOqjZqP?9nez z5r_L6RvZS(Q^d+Wqk&&kdwWNZV8xGWk#!5Rv*~<*+Qs>woZ5VgC98%DYmX#5C9*a0 zqNnIFSeAmcALjq@lb!DkfDd`yO7%OU;pRV4YAENg z2AW(aA^_NRSWra$lBYt2aS7O5;zC2>z{FYZ!HOYAmXjSb4_Y7Gp`WX#KUyqU( z@SPum_y+o>Jm{dy-X;{vYCe(Zd&s@X+5Gx?c)$n%X|^*}YDyHLiyvh__cS&d6%J1D z*^^@MmKtNt8U<~_iP0Ufo=v{q`)B&M62bVdODSLhCTu_eePZ#B)k?ffUy`yLgW6S1 z02zrQ`li@bZ56}l^nTKvLD9sbdT7iex8R0r+mIQ_xtNF4?Mc-uTrn$x0VJIS4bLy8 zTM&5#r-$WEinBMtq>O+{A&zGnMR*5>fe@=;MEeLft{!;|H9M5NAm>%XHGj&Ax$J7+ z$sEwGB|?-C9@vfe?!Ch+b3>ItHHO30STD0?MSJYEGB080>n~mYjgV#d670g}y6gS6 z#9_{8$0vt`eh*r8M{RrErh~36c3EI+lwaRAWy`zKcZCZ#vt=a8sp)?eva!Ec%PJEW zWRVeJ=~>ygb50E2G|{IH|JG=DiWcA)Oacg}YnZ2K17R0%;g%vpU1J~zYOM+w zmsX66q_#y#g4e*x>Qo!|4gdFqJ*_Rw?zDYy} z$8hw&vk{KdnsD1c%xv?srzG${%*-gR0hNkl0K;Lwb;{6`AYK6shgif1fsPCWWJ~p5 z%S`{*`Ud}h%S^diR))I6iC@xLnH=8Ut!C?ue1^7rAQx#&t@TGL{6JVT>}d^&2veHg zN#gjk5t&G>oWkt`NO6d9hA6;+hT)(=9i}jW>vHV_$Ya;YYtxNJ>%n{ha74C1qoe+x zJK6J7r&y66iXI9cYHn(7%d5)@b$C9{hyhxFdsTeyy9Vno4Yt132K0Q(*I;-C{`otq zuX2|+7P#-SJ%xX-aQv$U2QYYEugZf)cqDfxzPSDKzL%p+%ebo{s2b6)WD=v8tkn3Y%s=HhQ=sBVeCeK9M=ileT+1G#1~$2sPwN^h(`j)%^fct1w`!K8;H6>WUCXsQEB+orTC#ub{>c*A( zXjRjZ=XZ%)A z{1>F8q_bYaCxunCP>vI%S3GV5-69bbd5uY@o7mC$=L-v7e515rBen87FtzE*MIwFNL%dBlbk0i5s_5p z8`|00R8id~6ANmQW{Yj>VATp5UBe~^Pj=V_`%%nAZyX^-I7!)OA*qS6!r-N zh${?r_!d*ottyV5@LEmvmh0XC-BH+ON;^?k@o0j~5wi$=It?TqS@b~({O~{}vq&$I z26dqWwsWekoM-dOa-;UJU3;in42wobNo9;vKDaJm{M=TI7CjcrIab;G<8L!$sE*nQ zD;g_G7pMa*Ls%5ZCf|scr7m3z4drZkzR@y z(+p(*Ml_fYDl-cybrf88Fo4mtO_w2n$mdXBq?h5v(m6kn;|L;bD}Oi-*hU0jaql=2 zKOcWQf*a=-+`j@{+p*w##FtEay!m)cvAccaVZ*UO>fwC@P%ARThH5@c zbEx&6U=QQUZZj*BY<*r6Z(ngIN_|||(J4cKje--+%|^t!c1iPVgYyGP*oSC|@KvKB zDY6_YgpN*hkyW?Qy?7D}9lIPaQVdy?M;J2RFblPXD%LA{?doA!7%z3a)jdlpyi$Lu zku+_b_yFH9-Ml0*W|ZQ5ZJc4q@sKdxFdZIm1A3Td&RxZGV`mLZWiPYg)!^!(lgKlG zMvOeoR^RvEaqZ;!K#I?B=~B7#MOve>NL~WP*%FaSB-u+6QDm4e|16I3P^A9CV5G;3 zN2}q=Dj*jYB1GX_5F+D$HhwKR&BB0;9Q1gVJ zxJcfV=~6w}JV6VQgSpA^XA}!$$>aj?>u>L2=PQzSimF}cA8wwdE*j@5D1H=m)BFpn z_Pg?8v`ZknqtOr3Y+??fsemB6Os5MCekI6Ep7Q9y3G0|_R+ulHJLpc_MPuNtw%>aa zrNKXjj>?Tj8^#^fr?n#8kv7pc1ueBbB^YPf=WGAkkTXv#ffovS3Bh^Jz`hqSve{N5 zl53*h?W405xm=j!y^8D}0)4`Y!rqy2o6f7F*Jl&s%B~{c+UqcdJHo?#ffDpXYFS-N z!5bpU=gUqjUD>5Ne@#jX3@bM0eYblaayKdcYrWmyg4Dy1IdskPe?t~w0*JG`KhoMO z`5U)o>+`S_4ccr9>MI;4FxY>ly^-)VvS$Gwmc>liQuu~}VwtQap$iOcdP0*{_6;ZK zYaQL0SiDyamL98N2^(dMd>6~BL-n-*oGCvg z`x@QISTbW0NtUssUi3p`NtUuEgr7Ym#weq*rHdAflx)B3BsCiQ*dwyMS!ax`k(ojg zdarrY`_AvqXFlgS=YF4O?wQZI=a2I|&pD-c)iJNf{33nJJU2$8>Na^ANjV1(u3or> zjSE`e%g5d}?<67rTJG~nRQSinF-FpKOA1wcJi3Yd?Cgm4lK1>$@ye1(lF(prh4NoU zmi8a;41H}52bE72=tG|3CUx?S`QlzGsy8lIst=ofy(FtHOHDKe^R`Cx+dS1MIS)r3 zU)RAD?p~US8El-Z!V{fR`jxW31|YS{MWe+I7ZyEu2CNw3Cgr_1e76|2K8b$kBO7+F zR8W=#a;FYFUJU*2EfAsX)i)n%JdZ3}NC6K)pwNIu-fH$yf{;?9r^o%(C_ z0;4vP50UyJmL}rsX!l*;vyQ%GzL#NVs))j{Q+(h?(GrWTk|h(J<#C(ezLV6oMV|GJ zs(ZTXwuG&x42|;D)zZACm!+Nj*;?5XJm*MlGZkE#(H4o+NQKFLjY9Ngmn!EPLWyB) z46*T{y`Ovi$N2I$)bi+>vH~&6wP#PHC>bK3ZnZ`RO6BjW)9_L^gS*$8kiiI*@E(S@FpI#VWQ$f1mAl^RcD3caX+_xSdKj6Y3DmZd4=CgA z970irl$@}&z3pIppuxrQc&-oTFEA^d8+8SviH*^EUP|X1&R%_8)aL+RNX@@!vw3Ld zJ#x$Pn#j1n9}Vt8tQY!{is># zu%E&0yZMJL8dkMUodrz|gcizp6$5lXuIzE$^^yWxej(v^}ivUO>!C z2i};YuX~N;4|i%xElk_dC~bdNARa{mi>=9z0)(bzv}3ZB)$==ktRd98Tin`%YYL)T z`zV=q(J^icL-+EhuJZgyc22^nv{ZT|=*0*oB|XI#<1QH$^tqt4x;!^9g84H5F1e`+ zRfApFo0Q#b#S3&#C7j|a?)u6d*tzPF(_3u4_42szdqDM;VhQ~6z1wI7vYD8`1(g%{ za)a9sUV3MKd_kXKyP}q=ZX;R5m>vv^Cm7&4A9Wav+f~24d)W-1a#Odfrvc|(m+)ia z{IW~Zlk)d}NGFsAD;F3_W2_90_Kb#Z+^oRb;v21Cn^T8p&XNneFFQ58(ny{qU}xW^ zc;qc-o$WrH)2rG2b#2Imds#up%J#K5=MmJ3s?a(eb;JyxqtpLRJOpPorP*_C*=ufq zo{5_s;TmIbAGnS@5;`Eb0mMP?Rg%$-mk+h%t{_@$`Hc%~(}gQCPucJb9(OQy zJoGN5!Z8u>84_8yj*zTRjh^7iT=y4^U2K}hQlB<-lD_@15$hq7v zj?;e1*enbd&R)kiiLKT6mW()7aegyP~2qy%bFzA)ukmd*6}dYedo9yQ0-I9D&A z>wTK5T`{3IPEGVp_4js>*x7JA>EAOKt9xZV7h1F=j+er{-hDs*{I+N9TmPHBsp>Zg zBa|~$;>i_~zp62beoIA9D>}cJyC1A+(sd;tG>`ZM~T!Uo94}xp?5rpO@iYuO&CY3er^im&zDJT z7`+cqbHwCE4xK7bOZYnu)n#w|_8SVFe(!7gGP?aKCcEy-+U~F<*DBlUkFOG; z?N1%Qe*N8i&IjU;aU4C;zA{K#IqZV;t6q5;nS8n*lX5nwI4-m$akIe7=EE2ETM|gr zunAF1{(RoaX1952;ThL=5jJY6-r+=?feu}|l`JzZFI9Q&r;g6Iz1~mE`*3>Xz!K<$ zXq8S~k0gx#^!NMW5gH*}ZhbCeNXL?-vM*=Gka3nZ9n!NlwP_xOUyg*wd6Fh;l#*l$ zu0)&ZViF3S>LY{=Y>unS=8LW|me%-YuasOVqj{Z6+>x8ZFDS;>V6ZYr(nNM;eT zUL_DomBh?V5gJjyU1r3oeJg`j`emA4g%04EFLlQpoVO7|I5Mc4dX+s6GrSU;!+K9e zAeWO6^xaO>VY;2%kGT1Gfv@&Sa#L9{c6jAj2?cH`P<33Q(rZkd9UPmN05iwMK;8vI zn9MG)FG2(og+a`MGAwZqSiYbJJl_TX6AguL4nqrG`jDlaD;Mj(7vd$sL!Z=uzSm|n z6y(yXhkRkjcL=D=#d7h20iO&3S+@2l3LhJr?IAWcX%^tN0A%GLNdKe^E0PAM?yG{K zll-7NO#=294q|Ceup9*QvCP;Z#}`c&U^$(W#gaSf7bpgHjPrxiixM#F6U-*dMJeF- z8Boxg9Gpc@SDguM6>@@wi~E89KdS-S9sl>0iyhGW4eI3!f!v?@0hWFAf1}$S&p7o9 z<^kn@=Qensjs#fpkB-H80D#0veOP)FQdhl1i@2I_!6Ax(M)vr%(e5nzdR zV9{K{gAE!8uy9%)_@}RVhs(t-=RlAq^BGut%R2;A=3;k02m8=kAaRXYySa< CcabXq delta 39316 zcmaI7V{m3))IFGvZQHh;j&0kvlMbHPwrx94Y}@X*V>{_2|9+>Y-kRUk)O@&Ar|#MJ zep+Ykb=KZ{XcjDNAFP4y2SV8CnkN-32#5g|2ncO*p($qa)jAd+R}0D)Z-wB?fd1p? zVMKIR1yd$xxQPuOCU6)AChlq-k^(U;c{wCW?=qT!^ektIM#0Kj7Ax0n;fLFzFjt`{ zC-BGS;tzZ4LLa2gm%Nl`AJ3*5=XD1_-_hCc@6Q+CIV2(P8$S@v=qFf%iUXJJ5|NSU zqkEH%Zm|Jbbu}q~6NEw8-Z8Ah^C5A{LuEK&RGoeo63sxlSI#qBTeS4fQZ z15SwcYOSN7-HHQwuV%9k%#Ln#Mn_d=sNam~p09TbLcdE76uNao`+d;6H3vS_Y6d>k z+4sO;1uKe_n>xWfX}C|uc4)KiNHB;-C6Cr5kCPIofJA5j|Lx);)R)Q6lBoFo?x+u^ zz9^_$XN>%QDh&RLJylwrJ8KNC12%tO4OIT4u_0JNDj^{zq`ra!6yHWz!@*)$!sHCY zGWDo)(CDuBOkIx(pMt(}&%U3; zF1h|Xj*%CDiT$(+`;nv}KJY*7*%K+XR9B+E`0b%XWXAb;Kem36<`VD-K53^^BR;!5 zpA<~N6;Oy_@R?3z^vD*_Z@WqLuQ?zp>&TO*u|JoijUiMU3K4RZB>gEM6e`hW>6ioc zdzPZ7Xkawa8Dbbp6GZ3I8Kw7gTW-+l%(*i5Y*&m2P*|rh4HyQB?~|2M@-4dCX8b)D zh=W+BKcRDzE!Z51$Yk&_bq+3HDNdUZ<+CUu7yH>Lw{#tW(r%*Gt^z5fadN?f9pBoL z9T}2`pEOG7EI&^g}9WIuMmu;gT2K6OEydc}#>(oE`rh$L&C?k!GofS*)H33tYC3SVZQ{A$~M zi-ct|Ayy)!FdVj&#wd?!l@(YcK$P0@MdC`2!}UZGm}+1qK(OJ8^Lv&pIP8KGV%Hq? zR8(~2+CpsbcN~pe_+ajIP3k_Wmh;!Lx%(s*Km(6a_+d;NvW~2YCWHMlE!azSQa z+5IIa!eSDK!=|iOc&N5qoC2ap8rJN$cSA;0b(lZ?vJ?86Eq62`!&UNTrZ`w;~mkD$1&mvWT~=3QUfuiWRY3XzC&ZG`L|A$~E|7v35BsfRrJx z^%$zewbH#|N#uwM+%61leIx}bbwjnjBBeYZyV?9W_#qB%ia56nAXFhkgZd&Fxm@lv z#GFzj7(Zg{DFwwwFWY8YEg_|6tey?hUY;Ifsswl(rBxW2dH^aO!rlnG)_gUsca^2Z zFp05H5XoV}u%ud}DppK6h`LS=NDieBQq(R~v0%eHZi(SvvwDk5-eD)?8bhR1q}0yr zQC+f@2U;_dH4aX*_AI+P&Gi>?t-V+b8ArvOR&v^M=Q1Zf+f^OEYADE4QJ!ojg=yNv za`4GW0+V`-p)WHGjf?s-R(}nxY+!$x^{ES0+5l3T_fssYtR*@jcRVRBXN}!$UWY7paY9b@Jj}$ke>wDO)BR#<)SQ?x~|La zg6RUIXexH<7h6}eU&3J*&$u_}Cg0WmBunF=WNM4^G{=vD|C(@%oN{iq$;A{53BlzfF^6_Ge-$NYzfQ)Nb9$Lb*^{74r{SvU>r# zOsPHF2cbKwdQcR=(pY+~+>jft{7+H&sN0wV(`(HGITz2`3_`LZA#L6#S%~J#6|Gmi zgxrJKuN2L?+ZFln2H1NhsQ@J5OGzehL?fO9Q)5?~ z6@m?|0m%q}4hd4nslgpP*I=mNR4fYIE8vXe03#0O%BN-R#WXnMv-I09yc(^ zEP+h}1~cqLfIb;>U*;1-(u+gji%Btlg*mA>XjbAdR*F4BQ#P${MeH7x*h;VgYMuAM zkSZUA{g!^$9_V00xQ?tPg!t}8MsN+Xdh(-;K>aE~FOXL+awURWB214n?w3=q0VmHhpiZKa!LSyg!95f%&8!kc?AC zYxY{Cfq^@{4?y378Xn%jHs{NZK5x*gmjY41o*sGi>ThSaTvzTj;(#k)jPydN!Y|qL zwm4(3soJnmOrwRB=A$$=QQDO)H#Xm8g9_0Xhp^4Y?JNd;+$3efP9n zqkX9wtiM=FvS8r<^dvMi2ndKU$kr&MGt<8n+rNhlBsqSYBALM*4SzY1bt)Pa4pt@F zEt(BAT16EYCG#M|>Z)qr0g`~5JiiUzY~wzK0)F~x-IvT0t_0BKZeUQVBL0m+C&H8x z1g)j?<5-6pI%%)3RR2O`gJMhE7b1U9vtKM&#^i7LU1p5)tV7_cN*gxnch1ywj$7a@6-{(gqGk-Zc6>#aji6b^xeMp_ z)*z~7)FtXpHGCe7Kru5r%)sF6YNtuf_ytcAc+xMO+1kl4+GmJD$$4`i_w%A!jP%NQ z_7vX*gcRg%Oc~9nn8NG{MiZ{v5jHmDG5jq7H=k%GY1YG2hk$}%u- zS8uOb!VYsGuIVD`&oJiFlord79ad$IcAVs3`Nw>Hcz^*<+u7ON>+#raDo+X{G>vv# z;p4e27CNE3gzMzk{zBD>-*}xro!%*q!@)f2LcSjRz~s~vTEjI?SZCEUriHaK>d~5^ z`3%MLSQG$)<$GJ4d02^*oNO+t~RSZVs=V@ja~VKKw(dq$AIZf zud+)Eb9E)%^9O&j5qPGi+wH3U)MK;Y&%Ns?{gnr)XXW&LVM1ytEY9~ipMAPE_t$@+ z&gwW!){SXiG0|hG#dGNrE>vg`16J~R-(S%OOUKF%otHBjmlKLfwkXxCwuH<_ZxEwj zM;Wk7f-~fPZ&BP^j1?08XJH-+C^&%j7K4k`Tj)XZP7nxo+sbTxE@DUY zHSkj;p?H;vxeL}zwFHBEJ1UPr(vYrTT}~&F&i^Q?IJ-Zy6;}H$T0LVs@*`FUTL38c zz};bAVyS^A?J)1VM7CcSaJ#k;$qh;JThp1Gm{8Zbh|$2pImSmXqnPI`@eAa?rxWOI zl%Kp4B@bw;AQsdF52SMnh$0;oyCosVke`=OHl)8&R;=@}@S*kx?~7(4SC(eK1A8ru zX$3fOnOQ5zoWjc@1EhN8-MO5(kJj_G16-S2LZU_gDo! zM>BM9;Wv{O|CS}2R!mVpxWk$rw+(YY%nvw1MBhKFIarR``F4|@nBRnztDwjw7;$4NtU z8oOIRD?nD9J zC;93pTeM-qI;Hv$3T`~HFkgOgbWgy50jXr$R~g?uHzat{AnW@-XhG+eYI|Ep#se5M zqU?J)tfE`Bu3yV?J6B~g;Llvm&b6UWk4V8^PIx=2I;qr77?AP&ykY0gkYli2-`k_r;o z+4$aZKJcHDFmDb6^9i~~Gts2S~?zzj0dBnx~2+AeGS4ypLJh_d`@XOOdwK^VOk@m}S zaq&2iJFOSdi1tmb0+~KF{>5dqRoV&|c2mq#8wwLCT#txbAp`X~=EC~n0`DFilKE0D zSl2)7AMCf(>f?~$k=m3}9pAYbv-Jpsu?trL9ye8rIq_4& z7(@7?E$Ke+E`_TN?2|Z&2}1GR4s~k2j=Gc2Op|G+j$!72Y z(i&1s2b4x;Cf$N8GswJhtG;KAbsPAGG)l^-gdAZIa|xVr&{n{Yzy{e7ldnFEa08GI zM$NpcwBLxQ6~P_JeICEeexLoaI{?bS;|Zvj7!ak_*kl>hOD?fJ0vVrf^UC!M5w1 zsI(_3S?Z})+Lu8O`*(5#Dc6OJo}qn0-O{$-a9-&j-_f1J(lw+aJxj=~di3b1BiS1M zuVM^PLt8Cf@;*W{GW1_$zKvccNMASBH$!~vd6<@Vgno8Egwsa5$nnb97RRUo8)3~w zIx?(bNKTE`PfSja3pLlGw4-QtNPgLkM0-AgU&FFaME;`0WU*3xrmGnF?}+<;<0IVm z!7PiKc{ip7*n$k7F>K<1rd!ZL0r;=ZcqbMf(w@a%7aeE`0q=wz;JTz4nk-ih9~#a|L&MB0M`a5V|~_0 zn2Cmed5R2;k5`{vnNyiX*<6aNgf5s;v`CBBOscIr&`9fCO0%0V|1pGjPXG|k`WCl# zGE1VVl7DE%>P6)j7K`~JzV#G(G4(Sq%Aufy&(52Qtj=qX2D199Q=}FD`XdO|z>S}y zB|HiV-}r^V8tplx3vB0!L9X|70UjXB0$*zE>sZTF!zsCfRo}7Z{h)Mt5ti-B!#yz+zD|2R)ZQ40 zQ}o)C7H&$5MYvb+V;As@>O&r3d`v42ululLq7}D05x7R!nTOgbGz2)uaFpmv7@^5B zb|n<(FkZxxP=B1EFt*A+HCvb{8>cRt%s2KeiVdW%wazs=0V?>=ff~VhN6G18W6_CF z(fpxXVI$AFAWGE9KlO7F*$^8~S%dZnWd8_^5w;6MX-X@e%uv74P=@9^c24$yoH5$R zbU>TaVPD{(PxDduyfGR6%%9f}GHKI$3sXz=x0F)+A|=IZoeIR)ikHq)VK;$VUqM7C z?RQ&6zcvoOMq7u!duhZNg#x0?IwtH;oHvDa;pXYS!u%I*y2U=x>;5s zdJ-Jrd~nfGQoBUuuv%Xk@p(f?G)yvt@rqIXzW{4lCv!Cu<{%Or7Q5s|0?&sT0al0p zo)|Af@E5kDK-TRDC~t46!6DyIXhR{Lu(8{Kkg?217#KwvFPWc>ka|O`UHV(h$*6fG zeR{-~ zvH)Z)H&~VYu!gCF!+w79yoh0N5mFjwy_ppXe%a+-*d2IeEBcxXa9<5~CE3!A$@@5l z&4Sewk61O;@O_}0=B7Ql{EYn8u-9G6Me3KQ3|q2%;10vIL#Z*YLv(-dCeE+ghH_Xw z3yaaUC+LvP8gOk-$YQtB53aLk`OPwPVE`>}4KVF|!7jKD%xL_IZCpLoR^E2}u{G=H zQx=XcAwPwmO4p(~SMKGajLym3zRu^u0(Uba?N~E$O2G(|WVGGG1}xCMnFllL%HwQH zPet6w`YOhJR^j~mpRj5#0k&Oh%yAdtOPyVoqB0#*yE3#Zwy!~y7QuV%Py7BU0Vpc8 z1lJ_o=7gM3bQ9k`d<&hxnle4yH(70|7^K}TPEWZQXgCol1cUK(Z^>*qf9eE->1GBm zjh_|lxzrq)hxc#aojbJJ+w-M!8}?M}ndlV}M@c})YgHNHWMR;ciNn?n=>)D%tW1y( zRM|TVM4aF6b&`m`RP9o%WKk%@0`?EkL)05<2}5mSbjP*A z{_fX-afH=Vo8QU}J5*wPdlR9Asx>k&;J)~a>3r3sAgK)DXxbhk0Q-DE0D!nLNe}Y# zQXKG)6O*;%J;qft)n1L`E!lc+$t3FkfJJP2BHA00Hh7s5A0Y8~m3-A2qyn`mJWzJR zmIP-MoXMk}=by336Jw`Bsxg+Z7cTIp3wJngk*&Zczd z<=Chxgw2~q=cG*}|5MPtw>6VEt5kTIj|t8(UD}kPMO0qj>dULgfL@U5rp8J3bQqRs z><#?79I>1UdlTX(Ys5Y(sgg5$%hlE6G6+6_L~H;%HMvKteJRu`UXFz;rSr{drqL=n zNaFghK8}pq7K(CM-BCjUr86u^g3k;ZVTgEcUiI6Q2M=7g-wMsMTh@p{=aIAGKzL&v zYhTnO*r3Y-A|Z+vfTrY#GP-ztA@GG-gp4|JR026}HU4K5XACiFu44Zp8DTu84weY$ zur4)PWvYv4B1(zIO@%zcgBmZJd1xdZ4O<*kB8Ui9uxEa}og|3Bau>1KBB-jDY;N{K z3KQ#VusNng!~N9^?o@yy%C&oFbiOwRZgvLT-1w^?CUI>+TS&qqdEMKM+i;JM{yd5! z<{${{`|G$_n_q&5BhJ$Uvc5AKgLFCT-%IJSMdxw=XYw)fu1Vc1BFmkC_!HDNjsI}M zD-7Sr95!a(Jlz?WFGbuT)E%EcD!}V2DoN2p<+q1QPV^mycU?4V>cfNA-Vi2#ygN{E zJO~1MOyM^9A`Fc>)#-4zg7?P=Uyd#!ZA*5LlRafwid{l+<9pa!VKK5~8Ms|~`OoP1 zRCyi)94;tvvSKP~T%3#GqD1FtO?Kb&ky`R%XgvPj_QC;IQEV3Oit-PP7DMcVK3e>i zMNh6~rg)_!KB?eu1m{~fsV}7ern+i@9|tA>l-1+EbjSa{%8Dt60HCk9WQ0EUZHc$D zih)BLQ7r8)HXS#P)uT0Ya`9} zh$jT9eL(HWVEy(2^YCT>63QWBvQg= zW;x&E;61ZUJ$+k@XG>$p}LoHd8 zwA;4wNY9r{#5U6B#v;b0kh?=5@}qkBnM0N$eJjO~q+OXB8$3L5HDf!%DYv;1A{peL zMx#AaMjT-90)QM7#V(D~2sKW7;<~ z$7sTqq7CL!#c_96iMU+@YybMY-e4>AeFVdy@zC>6zVM_C zo9c!hW1d6d!&El{uAnGN&^i7!_!yFbp~`-#3MMTGQKj8_*t!P6GLVgBq5r};+yK-# z`A5DAG0^z{NS?x}H(8oef>mz6_>-o`i3UR)qmURvoYpaWIN3Fy0np!reIR8!pP1Oi z5=(f9OUYb0@Ka+X1rmde)&Jc)?at21#(IP&Da#W`XV+*KzH4DF|1>d8FS zPnQ2`GfAT8_Jbdq7rVm%%(x;rthrJsYI*os-}8uOM0d&o>7*E(FA>HBi6e-lpZ%FS z_hRD9HWahZS514bG^OZx7?e5I=&egS73QSG31GUTr%!9Ck5SDZtScib-*;t0!p@)N?%T!6tmR=H1Ed-ZK| zY^1!0M?0Um;xwQ6dW5@EDDDGfEjw5kq3YuabGdgb359S<_YN-h3T4}N_ zIE#jQXdKlpXu$nk(5y<1c|ju!`8s#lczO|bZs8OE{BP-4WM_l^hPiI3HZk#-ODt+4 z#5YoOs40(IF+^`tV1z8XB`^jh-xA8tUTxqthFSRuurS$6a*tRkP?5cugfhwhP8caL z%;~54vF@*1St$*fCYdthaf*rP4%d6FzF7@zPFShdjV!SxZ8*fEgCIkuFxM;-VFFo4 zegAE0l!eTq^FZ!WtRH>q_+L!IHzgZ%{iE2be-z90uTbLXV##FbVr*uY+~bduTyQ{; zEM3F}(Kl9+AJZIK6bp)wN#A!^{sRQ07z_l2`~TwPf&+wP=~7`ZWIvqd*wUaM2t4#u zmzDqi*#_~i`0~FYUWf32RJCsfG-2eg=U-Q;hgP;I$l~Jki-Zi4D1acV8WtAPi~{Vx zj@C@ax4+i52_%R{sBR6Vz)|IWL5L=~yBMHbqzk1jEiEj2-z+S)gaCjqNak=$KkR_Y z&*C_fC?tTTx1${*?q2aZr&hou+E(=r|7SZyUWzv(K3SW!|XZ++BfyJOVX6!Z6Mqohg5%_VXY09c}9F1l0#zxZBl{sE*#qo5I&- zf*c7|TMKYhcAb9#7qE^>xl?ZbH{j|YrDSQ$+kwnvD)4$Dqdzf& zMff=rB*HS%Wgsyd#+h9(U*6&lS15v$JGauzrV2Wh+&1e7chsE(q}LQ7n4tB%C1%-9NGgAHM=Zv&%BsOvCH!f zqx?HzW0NhUbu?l!W`U0gL4>s0cxEG~a@pY#nM)r^GY5j}d2$3v*0lX<74&g}X+NW^ zu_?d3=n%*NKv*gH6k`>%Q1nG>yernimO$|bsAapqAd!yP9}xD3$kN8oiTMl8AfJ2^ z@r>_^`j;yHaxOB^^kuSy5^(J^hrE6q)X6s-@1`aP`YkY8Zc+U}ZxGWLU>By!JGe@B zXbMX1t_@YaioQB=_R@$bwU6Z@$ODhb`_c0c zI5AllI{qst!bT_#wbyS9&BxIKW31V6_NdQFK%b@!wtc9y{Ju_=P+?mM6pmb)Wu)Oo zW`g~;X>9GY8FX8>(depFiVwp6k!R9K~MnpYQ|B8cbH}-w7~oW*V!4uF1mFaxe;iZd~ypqkJbe8eun`E4SE6!6m)3;+>OU{KkY?}44X%%>u2OB3CRKhZbmRd!SCexE; zX9z$6BoW8{QFZ2#wAnb92}qrB3Vrgu8xkN)#M`La4N}|>Ox_PpeIw(8Lr2+_YJO~4 zmE4QUlXeGV|c|w?L!ezz&!1{{@45!)DbP^goxg%~`0xEdq+|!Vv zvxxi-39*E<$|9A){zm*Sq1V8V;zJ~V*9Zah9T$zz{S|1?;aq)z@+V`+&cTh!JGlc^ zqzl6#cCyS}>pO7lHL~8ezda-1KJv_Y&w6j|0{p)~ zodVKg*{e8ND=hAYB@h%DF10GqSeXRQ#Ot9ee;tMxc?1>8YF+(W6zIl&(SH(t^qU2w zbPoJ{r4sSx%_E;VorZ(yFfA0(d?H10X8ksh(RBAk31jTDa|h#ak&uD+Tf=$HTY?!i zB?<2oRng3*bwqA?K7nIB<)0!ILeLuu<} zo`C+>(YPPk`8c^HtS*-)Xdqka{bm%@qhb_oFw)u8@bWwKrM*Lce+65Rm1!#y!^mHz zIztVI*aGp;c84fWU|#B%ISw;!vM1ZhQj|rs=TnfVLYdZalN&9frc{3|YW`aE3aJIw zpdJz(a&F0&yv}fH$Ys;DH4czI2Zmua0e<`!9$Ts3fjj?lvn><|h|vFDsQ~qwFtvDw za9j@Cr&!Iq^_8G7Men`WhNvJQXUU08OaN~qwUv%ang-oYLr1- zOb!`PT<{@Mg`{k=ab`3NN|Eh~Aot3V)!HC;n%c598wid7<#XE$72E1I!P;I8!>t!z zSxxHajDFh&2)ke{HzFSVdvUtSN4T zRXn*eOKyopQ(;Y+VhO{%kW!o%a}idhrdVf2O(v4ElsAnw%yELeK4pPcrOtx3n^pA6 zB}~(z>RC>IHc7imvvOjiLyM-lhgC9}b|tskBljfrP3958K)d2MmbFT)DS*Ly$^_q1 zF>M}Brn!|>A-U8*yG)Hwa?ISN?)+acu6exc~9DP*SVG|2EW(#pier%YvvEl)zi=^>CaX;CSvoZ1GyaEJHHtU| zzif$ZEH1l*6S7&HXt?Cs(~Op*Qmbt=mhEe*>-5{4&7Z2&rx>fy0IzBPTtLE#(-gg0vt0HgxP=!P zq3UJ0fbKUY`N>2+Nu9kN?8UW`z`#c61?0Kza(-}Y4Noi*&k^TSj|u@4C#c^>8zmbK zFCIiQ+){b2mAX*wm4o5|jD%3Dfhu5?dn`w3&pwbvwEGe-J{+zfM9Hx$Q z0Y?u{`ZAY5315P*a*+D;l&L#dG6#LRHZ-z^j!3UZZe-L9QcP2ll{$z18u@@WAUsi9 zl~1)_r)t7H=vdT1keo{4Toy${j5NSxWxqEhQ-?bsQT6p4vlwH#FRgI20WCXG{*&#k zd5J3Vs{CAA&KxQV{Ll`Ix?)3tB0ckMq2vZz{&Y7ZNr(_i$FZR3# zDhgwa6r@{RoLKgZ6|3c?GKHSxvR$KI0{(<7nu9lU+0l#xS62jTI+RH6_OgB0q4HDj zv;!O~iE9-{jfObyAuT2Wh1uX;z`EVO8)dX>(ohKwAdiGS)HU1+t!*P4YztiVeQSXw zsr*^>l-$fUciCwWFt*YRDa3`pYQ0z?olFVtvsNaR@Pn(KId&mQirS1*`^O&o5+d<2 zWIoW#DsJvOx?QB*|sqZ2H>7Cr7Oa?*G)KWIuWfu4YkD<+C*@NLcV2(fV1ACTE zBTf4hP?MO2U!;=HSE`y>36+&Ktz~y!gTn^C1Pdh@NJ_Y%y=C!On^UWi$CHu@HW@`8 z%@Y;sNtnJ--eEv=7Q@-dsS?3&0aETVGS*+aZoWskZMQd7^`gEl5puqD(EYO}cFW2H zcagLf@_N({^xLR`F92$3G=USw1!|*&@G58$ARLc3cCJv1Xx+4t&>#kXmcS4uCQc#) zIKe?pSHSK3*R?WbUR_`}Pp|~1*ox9@NJppZdG;59BFs&?oJ4axHB65}6VD~qj>-+4 zF$RNz`cFC*-aSNz28zDrIO2x1SIeK3maGxYhG=y;lNviwZ|rLyjZPk*dC3 z$l)fE_9+7T8hj&_kx^G1e|-!N`FC}vQRJzj-ir{{tK4(vbP~hlu4Ea?2DWVnPAbrN zZ62{dpG+)amuUhxRceyOfsCQk@R2HgfI!0od(rE}Y;fkI3zzzWm8yAB7C`sR;{)`a zq|V>i7LRUv>}M*4U6=2UG-m&5x2QMM+*lS@St=wt4%BtKMGY1J@t^n*QT;D3;?-G) zsLBO)039}= zN%!bq|JMElhtz4)9A))nN4ocLZJ!bhU9p+fpAjQACl<6Rv?bbN8xqfo`Iy<)NGg3w z%kb=;Z`s~e;WK|+C`LR}MEh*V$!O22(!dAzrM8Kz926?->r0J6rFGx4?XtA^kz+sF zArI}p&disl5ViyGIJ}n=#*Tcl0Q_~Rj?qRt$UK{2j!_|pKYlSFpIgC&R4TBqA353- zhiB14El_Mv*>EZ_eNQhp49O+?gF70Ph*r2Mu?)1m)6=VPNLg96aQf{8rYdZhGrv-Cy}jUd|E{TYs^nFt zZiv|CcF%n-neYe>V*`nOYN%;=$g{g#_BW$=wneN46MdB%b81M9yS%btRX7xI9CY~# zVeVzZW^Q&NI~hZ=z5d}>Gas~X;hNpq*;Szu>tY$(Y|{xLsDTTM3E=Da^KhM;%^hok zRRLE=Wn@n6Je2q%Jt@$1H%eA%>rq&II;1=iYS=n+aRaPr|m1>)1S)0y!QV zazVOZ0`@>ZS%N#2;=54`tE1ovn*TzLdEkv5$DVnK%)GtV>6mnG;n0uhWNx{+!j13${tj{ zbJ>LK?X_!W{}X4Z_8`{cMQ4|T9+#=z&>nDg+h8jh{@GJ>6P0(pFAT!-nLX-NK718T z;VE0ewwlJz{f-k)wTCRq<}955&|x+cqb+$`9PVCCAG48|0`L$Te|^jgs;zMMyEbhJ zDK+R(Pv^?S;cQA@+l>Ch>r^LykC!+q|5+denV-0X z2*BTCZz(1Wz$>q)O7Ed}#|z(+)%eEz@?vR!Z?EEX%;^-p_rtdK(mi?b=w661K z2%_oCw2c6@2k?f~{-{ayysd18uowv40456zm5u4Y;%?b`H}1;Aj@Sz7if60pxghHx zb;p-yc-4qc(LGh0TpN9Tc`b*w~ zNTTTkGWr9)`fB7h>_CMl0L7gmJYftkWa@-BBr#~`9uReY43{@lLF<`0w-db3b*!Av z*H-{#>OoeIWs0}F=ASvUvH~8`1a|=9PfN-95QSOUV!(%y0E9@k$EndWH*e5*&19UxG3pkTkTh2U=d#tn=9#3E=Z|nJf2?Ek ziN;3CwBYmep0UL?_ZwJm@C_?h`M4wX37~koZ!GD|LD?@};5g8GoG%U)fd|*aYTXmP znLXw{pTqA`8NJC9p2OGXlvgG)&S(Hd;*g4);R2ffvdp{#LG5D$5ai#uZ-ps|u3|M( zSDv8%*GmX}Q+(f1UX+BPFwANBUxSO0GoSYMe*b|Ee<)#gD6q0?wDAen5z)xP9w4p1 zu`i?PQQFn7zqdK1#^^LxbDu)#dOuB%Kd7y()x0zV1hhXQTfm%8+940FH-ST{)0)v< z4Q%XC{bst3KAw0)wZdYuXH5ih1=~~jd6Jl#BKY;B^=jVdiBC|G3hmFpz3~ME9|`lQ zYIWuXQPV%MqUA|2aVWgc6MBb{0c=iaoN|q*f3dgNdT9(93x@uA#a4~4TJV!(&Wr9< ztYkdEJR^Q6ooXU?1UK(iEwkr(W?upo<_9+=$3n^AONEdih1M2 z9kg7OJAAs6rP8s}XzwvH-1qliSEK{}YY`M1^ewCe+q>Uj4Bx(pKwlogPh`e%jYl@$ zy1zkxkDuv^MRPe@WdIbFlHy?$XJRX|QdJm0jB#~*G5yl)#-g+uY+wmlt66E&IEkJW zLpwN6EBiO;T62ZtxCUA^IN(VDm@zp+BY)nOWrKmtvvy=?LU%e#?C3gG7SY%I#pyV}N@%qvPJmz3kt? zs3(1Fg)iC26)}P9cV_>7t5x95)hmhru$4cN>K(tOD{vRQ+i0P-1bruEbjS}Lj)mje zg~nhwUR`i%Yl7eS#{6$cVJ=zyKE~e^?fvAFTej=O*v!RP&B!%InZNXRUQ7?yjbdLL zN!@u#M<>JFKH-K=(||vBa#3S=Efte(cr1Uxt57$dZ*VPiVO}Vg;D=Al=b;p=Dq7wh zwZ$Y^@-u#cm!k^D8+V7+7dI)+OI`cR{+NwU?Vmm}#uRkDnk368o!8VPsz*ECl(ZAD zo4Xm&37hHESqn>YDt3=x zZ?1=1AH&j9$6`4;&$ZK>@`Yp@J}mpEzdjS*U2$oJ?3%FnOMZUSg*ukjAnpzTD%he< z-s*v%8aMjyD#q(3b)Sl|@#04fbe}Ozr+_I9X%@sfQ|wCPTKrH8oIr`{?+};u^4Xh+ zm!0jk(GOz3asa(Eu%zwrHt14DGthR>z>a~zX{N?Sm-t2@MYY#JZ#98PswBVbix?NF zAW}RlDErS*_XfaUNGGLL$7jBcN*L`@v3Y5v`LN7DjkDtX4TN(gO@}$dKShpS_DAGY z(v*7EaM4T6IrUbPKSi|fo+5Uv(1Y6>9NY_%bw^(lCEy)T=hjniq~qci=E^g~`-^7b zgu3`yYZ9(_!6KK{VHUCZUMiP0T(x|9f0(8@x>xNjjbF9p@Ky(ELRpRZCl$d!O8mZH z7${v9PLk?<_p4)Bou7(kgL<4Q<*a|nuD-FxCeGF9cSUZ;E8q@*7~TAC$ex4s-FG<4 z8j_3S4d_$U&&1Sb3t;N&fg&M$4&`&i0x}27OR$ULO8~n1~3EiTwYpQkgTkyII>Y zf&H7@0pR?9Y*;(EnY%a`|4+n!eX#B>3@iVCB`oa!>7@Jr`%uT)N!8BUiP6-~*wr;u zP1bWs0{x4!iEKo}3tDBcxDuC88a+XWIFuZ~4k2P?E$@{PLRk_W$;K^eK9M?Fa#oi8 z75R$fHdN$h?6Rrac@uwrMz8^nH7y*S*%9Bd>q%4$`1(Ag2zYp{3*Zj|jXOj`%h%y{ zJP`ST#iAY%IQMv#6gu@Qzm2*0(}F>7;r>J?qxm*8v|6XvV!t!g8;*;f9^DDe5OEJc zx4l?a&){oXG&~Owmr$8u!9GNrg70|q5@o(*nvkOBw7DSl?q3sKgkF?go% zw9=@CEvV$E1=|dAiJ-8jz=Pq?B#QCFYnb=oPrlO+1*QmLk~50YZWtVKboyI#KZXb$ z3y&AuC}~8-R5hbHRzp)w{>?RL8)yO?q~16_{0d((&SVsKG(~tC)G_Bah$I^^Px+k? zSy7?&A(X0KXNJ!LKmJ%4!+B84PKW%4HR()N8Ii4Gx{>?nORzZ#<7;J#kEWKmrqw>E zq~`6#P|0aSssbmZA=X3S>vrkJ`)6`F*5uel)71lzt_9 z$;kf?SMR`F2^($ec5K^rR?LoV+qUhjPCChoZQHgxPRCZqw(a|!UANAyyQ|)x@YbwR zV~+7msAl$>N5fwNWnv_Pg)KsA$b%(Ij(p1DD*s@d(aV0OX&GC#qB-FN0QqpgFh(Mi zCO$(yB6m}~=Dxv?wzr%POw3iD=a4%c_Bd2<&m#pzGufNtEP5{>`)B`p83$6U{<9gk z>$f$XcZ1fYAmUTGafTlt-g(lX4RU^`=joMX-N zT;WdsaIOuTS4PPQDKxpHSW^qf0YQ_@e&*Q_kjvs*-bcJJdd`{ZFvrTJ1b5kk86!{G z1<~_a=91;GeNf}~hl^jX`setn=F<$G;5-Ux@|* zgdVYIm6B!#m)S*+`pc%@LQO3k#RjwM+#p(b!fa z(7^n1NS37y*UX{nP-r#qbZH9uLJGL4U=En0 zDP!(+|Id+;e=lYKwEK80WcTEMMh|p{=OIcO>)?LgaO=J9I=Zibxc(v{X`|pYZ9}N0niC zNGlwZY$5h-x`)JKT62$;Yn4`-_PGYnlS>*`7E$vVX0TfAQ&puic+^R~0 zFf(Gu!Fyr~+p_E`^|jKe1G(WOKMBBXf0mdwU~Wk7HRKhruvLJ!UDV!GoVEcyd;sWY z>rArJ{(*lo;sJ5V|a7 z)kevmau%)q=l-zP5w@?#;J-qLn#SR8x}&ziaf9b*`;txeEL5pj_6qN8|#e!!KMIY+trCkF(sL3y=Z6{0bJWr%s7UA;$@oHEM#?Mo@IE=Jyfr_U6$|a zw{lWBTnPOPS_cx)()uW~?WO1PV3wH)wY+8?*UEw7vDe)u(+la6vF>Y63PeR7;u}p> z);eQ(*dOK{(jjj{gF{YmJHOBiZLAT)dka{lzZ|nE{UW~r3!S27Y)x>fcpJi`?9E_Q zMfRjG(vpwy4j|blt;kO#zzY%Uhln%~o%n=ie-5;82kcak)u*-s=M{E4o1)%ogPp^{ zy>{sM=8U!I1>GS$$UFHtW6-a?qo9@y3zI)UG)wG$;kmFPCf>6Skilw3ntQLff3eb`HuS*4tK%cWa-TqX0W zljJH`3$*fmT$y@$-3u$2|Lz- z>~_=fRE(U!j<0y}IMZ6Dn_>>USYEt0YUMC1jfn(Prr?YD|1S|FZB_mj{wEOv|C=xz z|98UBuiyexG#uR2BrpS?s2`}?2=Gly)T`Aa(u*Au$$MwXl~t8l0veo@b%QRa6n$@f zow_?39#CHKh!j*T358A(fxqxzlt)p%z=U2Rb}tN8=D0s+Zysk09P?T|3;KR7 z%=}O+GT)(jq?n52*heBfI z&@jo<7hTqbQEG9ecgzkiF^IH0^vzE0R%&e7W>}Qndt7TTA`$^^1i9tv#c5gY+=S~` zB`)B(O@tFdGc4(6;quI^Aqb8#Y!8?KDaDmu{iLm6?Is&46?X*_X1EzuUo+Nf(fl5r znI2#pugd*O$-Z9cjX_+Hkq6-^mc2@i?7#sZQO?Hsue~c~IbiA}w|?DXvsDN3;Fx-+ zx0XM^HTJ>n{r6w7>cWpJ`zlIs0zIC?jqYn5#SCFI@YmYYe~5EG{%EIQ0`0eOj&Rfp z(GM#3)xr>RUeD8at%d76nd`z-!Z2X+@uGn~ZATe*ktOM|m-ZGK(1dlnJfo}+3>AM_ zL-B~32=h#0&4>|xV)Ldt8;l~wT5On~*v)*XPBqHS?`!wd6Qyn{JK&<<7RU&L+r&Da)#mlsRT587-NoL}LTF z7$O(B1eP|`0U%1fc=haqi5*-|!<(H&g%hFZ5M=_D31fQA!3e_s(x`>1PVoui<|eG zIi-;;JSCMXs^a=hgBdMd^ACE2T&cith;2YMg44kL5Ve4O#e-~6W?-JPCj2QC;ymAm zloMyv=n{aO4pPB@{_J2yhEFV0vMB-Y4NTV(p}<$_JHIY*7O2V z^@8DbgCqYD1OEk=IHufJbuzqejz#_e=oUk#$Z{!`Rt;IiuP8nJhHXA(@p!QR{(^}6 z%|mF*!Xg7rHmqf7jbQ&@=u4yx4XI2Pztpx~7+-{|T$HJPa$kY=nyhjIcg+Tq>2sI@ z(~zW;`RUL9()^%$^|#IcR@%SllJe1LfK$3~{{LsI-8<>(M9ocxN6He;LNE6OOKuFV zf{qSr-Y*Xht=>(^J=SMVJ-uP#QiI^AQMI&OQ@b?3Tw-kjE;-Cp*iy4Mub}t-)VuPe zv;FmE=IEh+7Ky7KdXFG&76%H}C$-cC6X*|x$AJrn@Vet*#f&Nt zH>m^3`gCefq7KT7@C`-1i;VS&hiV64Z6LWqYxeYn5Hw&ewgwMi#hmL4wTV# z_lZUM6o9aA$x(U+qlP^*aK|-(wKubR`gCFRtm;t(l87zDzFA7oH|U1+VeFWOrFR*` zy3-Q|lwVjFSU2#7rv==vj49{*`ZHBS1xw(Ky1US!Gf&DCeCmQy{wv`HDu>j!234+2 zFWBJ(dYFbZs|L(rnymJygOaSx5d{W_MDSkp-7<%60*iwH(O*m{5XAVvBgYg!^{whV zY%l>O>*f`)&u)!F2jVxXyt+Fmcq3Zjb&XzW3xk3*%&pyB!7HsbR4+tY{_>mnagh|S z%5J&C`0>Hu(fWL16(8}#D2>=kLbWw@-r74y7w5PEKdi0M;+C*M$!5CZQB%oin=f56 z;W*G_OM<|zviS8jW(*=wGDf=^fXj|V%H{+1ugghcgOF{&vR;Xs;)mk->FVlSM~T_{ z(NV3iofXXNKhLwS$A9s}#MMaYbH?8FxfS(v=&>2Ts~gpzy|D2#7J&BpMq_DNjh~;C z+jHu4ZOnR?-g*|FUuRoeTWd=TbY|9nCO>p~`;tg=dwNBFLs<#1q{GfH-@}ew*kVY% zxuVL=K+BD^zQ;!3#Tk}?y+bUaU!?y=#v$Rv_|jPY8U?S#ukh_}I9iQEQkJnyf2SA; z7b;S^16N^#G3BH>Kby?Wnf|kXcCNFVi#^HF;3`-l9_GIsOFAk?Xt9>dH`w?)i2nY1 z$B`oAoym($jU-MW58nJQzQ}>F4jS~$B_cvDauy5K5ebNHcZnpM^|sO2LLw*ve65>^ks@1=+%v^Qqz#-W{nrNP-ZJzA=V(J)r#8md10VDWj5?E zoF-apz{AYnJ~&&MdY{8QH6=D!;`a%y4L1f2Ig!6E5fe zI<^QH)I1FX`=uS^Sj?q28GM0%P=C;|5MWspZii>|*9Um1Jn2BX-ERq+iQDfvyChoL zt#TBa2!ue~rlT3KTd$_TyYx^9vXE8^Z?#IIB9DT)5c|!8@K_&}v(Sh+Kx~d|Z%L$E zzZNqXsTC61NrM>S41U$EW!SeB@Pwfq8^aT_h36&$w~)o(Jn>3%cGV^!X2{qBZDjkV z+j-Hs3w*>#Mm!B!vSn>n9gwUX=~)B%P9nmnPwHw6cNs8yRd{8I+B82lCBdH-@a z1)Gf1(0?B=3L6`(E)Lsf2De&* zh}lzt%y!7nV|y*-j8YC0O9hxH_@y4S{~XiB(9*pXp$!*tVS{vQT44OA;{VF%59>-c zy_4_(#iqbLEv=e$;=+Q#?JS`+OT#GlA`!*g9_ZyQEwbYc^s+X7jn!Jvi;SH{f!r5P zWNh{pHHR8yG#NIj#X)_Umf;W>m8-_*abp z^LE$MOOI)f@wcbjY(8|}l=u0DE)>5A@#pAsWBVZXmN0^HJ1G4P93xz$Wv`ga%czKlXJ>amN^Lj74gXVqTlA4G zc|Grk{~0D25(723G|$ZWg-$a2GVy^G^M^ic^c5~9@1Tt13!g;&#U>_ix6bZ^aXXVE zLmHG*k#BZK75Z>JXZDpZy*T$0Zu77Ldj`T(wB{cNaS9I1uqF(c;S0?;N8^M5J&%E+ zm68XF(U~LOER<%Qlrnh2-@+Vh7b`Ckg7jf&sG(nAL_bgK?z5J2zStV>kq+i|wL5k$ zv;K-@u-4wTg&YUyAu=Oi5n?oH4c*W~XTKnKyF?Pk6sK9XB5)#vuyd8QNr!z{4ha=X znS~k678}jg%}L04&A)JdF)e%n0d}1~b@`TG{Y*t0A2&C%KG^o(o78%Rf~mLaKm`x! zb0Fycyy>%G>Bh=8m%o1$eM|q4^b#Xog(GC+f0xDw`7_532P;9!@X(&_uF0^)`9FZ zOwu{Djgg{WLu_3NG))||AF(4sJ0zk&fla^?1LqgoHxB}{kGpTIZ~p<#Y!9a&NQ{#& zc=s!_rL!W-+m%CShT_zWqP?$K+fLk!GWREr`I`Z9&&Y>g@X;)$C&I|bZun{3u#_Y@ z>B2RPJ;_}yaPY|UWYZ34k$}$^rF~k$LC`){%Qa8}EWGU^ueEqBUvt30m@-_GFwJZs zR1`~=t{$m0Af9aO>tv*%0Yon`MZZp9kDMI>5AoDuw)gYM`3|LhYwXl|PR7&@hp0}L zm0jSQ;K7QO>hw&&&vB^OT?OV7ckQhHP%ei!B3Djq!dg%_J3|9~EfuXNTyw4ptj*&d z33D=gN-fxs8gB9W6M&KKhOA3yeHoD zvv~ApbWf>2oxKDCQEJqMC*_h&NB_AbgkWuEMZFqW)4I=S_(`T#s)FE8yu|yj7+|AR z+KDOAZ&8FO*ZL%2(#x8fm$;e(lQubAsesJ@^Bq6yU~?D3kb>lSdMM3x{8hdbSaWtO zk4?k_`-DJul@M-dRr_T4LqDeCT?yHT+1=oDT9`$T)sP|D24nW=ha2@q#$)uIXfD(K z9*^Pg*56lh->xS~z-Nu%{ilG6?ONW61GiS`t?K8p9f?_>l;+3OxU%%qpjo7<%GXu`U!|YPtJ&Ir{Ki`d#{f|Dy{M=)lTmy zqq+CvOpbqK(a)G9smCGL0tLsS|Z6#b^dV${K=zTRk2wW6xsJx09Ga}--bro^{ z>#%jx#<^Woc5-uY6}`=nqMl0>G`v1+)w8fjoQV?43X zcjCS&66C~|`6(Zo81lWk0{DR3r-p+arp{YEKHvd1Im~BF>QM{)vjT1qC!--=$6f?~ zPLCHvJE(PO(hDJka$d0?GDo_}&<2Kbv{l>C|G_X!2psH!A)hnZ%tDz8mDZpUovlH z-$em&xNd})Z?6aNXOt(SesqDZhP_EN*Z(Rg`j=^_yJ|d|jPuFIs<;XQPy)b;V2ldc zo$c{9<8(qp6;Wn?-_`jnP!cd&1t;+HVpQ``7J%$Ue;3EUyc@|t7i3JUxGGS#1V`GG zP_|6|j3?Jf{GVr>c1*wb$yOaoYzbseS|$^Z_y$mi`#MH zrG}DcPFPma2?&ADKvxeL<0#d->E##BZ29p3i^XR>5p=6XkFuv&cF>`Ir>#U^QyAKu z$)yV6F}s!s6e?#fmUxixm3M8A=oN>#(XbH+(D`@7a2Li;a9-8d_;fX}wZCcyBtMoq z&^Un$%_S^zb)|nuZc&vk;^!qI9xb>)0<{Ev2Sy1@fb~+>xXh{|8bH~#1Ddon-m?X@ z#`a9tv11YSIQlQ_N*Ix_eHEJ!fD>}OzbFoWq!z(4yUWOM5_~KEJ#Jj{)opx?o?5p1 z#jxjeh*fk@Q_e5Gz)*=Y7Y&~Wyhoj?zUe?l(~4HHak5yVo%$)>#9-Npl7RBpZ9bd1 z&_5cG-;3Pz7@wbFISV~BBdIIzpssM)49SJATGLiuQmmVqXlo-|SwdHlT3W1YD@SEH zaK3?e2bfB{K7a8^MP3sz-f8ZmtMg7>^_xe_%z|NNNT^CWJi}EWDDvq6V)4t~qa+Cd zG!x8s^)uzb=!=NQdL`;NEWce&lYQI}q`l9}YvzevwW%v)XX)U6ddP(;Z`qudH!%sK z#Lh>(E+-#b>bMF;;>hcFNF!cd{4Ufo~)0!tbxXN4J0%W6L+oYt0fN4lTghI7^`re@E=V9DA1Eg1eC4K6PthfLSW+qM~4Q04*={qxK zBrVX1-MRTChL)njaiNm;-7$GJp$-vF3BuF;ozY93OpkzILR}|%`Px5f!%i$F;mJn5 zP!a0-Mg@y?eX$rMUNq(@SepdU*(WIO+reJlWLSJM~FJW6KS0&gl?&xID~E`WVnT|Mo5!L(aMJoqn|g=yRg(942h8ugQ9h^ z%$*uU?IeMiZ~Z*z>Ml%>?j-ilzc^j_LFAFdty<)01X!8Kgr`8V4TgIx4Eh)_qcf zdr)}ZnM&tuBoQUZT`-$Q?HMBfk#zALmB%Nr$x)>5%TipiPtdy3lfh= zV+#n@KofcfJC z{Z$V|o7yOeuaKTQ;|GOuB$G2yJ0Ggv)L8m_JkeJb6%D4ZL0>MA{EGlr4uNnnLmIGnT8;DC*wfECvOau2W;8Vb$2jy`A(hA!R5ak z(qGK;1NZS8-x{{X_)sU?#gdu}mTz8bj4ef2?>G?rJUDPK`v>sIIVE8N+CY6s5u3TWw@dUNN2FaU{y)iHh9iR$K0MQd4s40u zq0x@riJ9g7^#rY(c`$l1}bVG1_rug|IYI5*EM{GdJ@G_wl@>(sbgYULY^n!Kt$vq71P ziW7!Q%RV{l4^3&{vLKs8Z!v{3PF+LJ-!yVrV>Un`nL&S>aYPnZ) z<^o(?okiw6Y?OXU4pBoVCbgqcP?F;TIQs~uJ4$*zq1du63zSJIrB^03$ZV#bLP9|+ zdknF))?_F)#{~a98(*b(*2d#2^${6AE{zz2RpGiq^MqPYpwbDg!~{m0r1cG`hC5E3Na{CXMej!kgPc?E{+T1u zy&%@L?Ki;_kAwuz+}`+Xy@U6b@5sG02G{LWq4$>Vob#0J5WJLzNMZUT#L>TSQB(R$ z^?Th41PQCjwh%#WkD~l7=l>z?CBX1e5JE!t!s_-3DU@=<4ka|ojF~;kjP(H@M+bc2 z3@qAdtN!+qMz#FMDdv`*b0dY;c40<&;__iQK!W*!rbPRK@m0OU{K9~i21ZR*IW>-Z z>$~83#(q@eTbT=AzSUrjt^i)(sGy){$sqGd!1v+xA=aOij#{1-Y3McL{!q+Cmux1% zba6n+oLiwS<40!+S(}Z<0ls2j8UZ{?%C$3CY>k=-%A8)51Qn_5H(vNB>qo&Cs(ZPV zg#1gdsfg9<0tZ;|Ijj-$)(E|lZ%|gDXEw=M)b;#~x85wFZkeag?nKgVm2oI$RV}?# zTp#bx#ubY_bb!;xpj&y8d-p2Ib)12i;!JzfR95oyoTlJwdnf}?>|1xKTLFK87mb+e zW-?W#xNA^Z20&qTu|`d2tF2GkYE2fmrYH4`-Pf9K-E}a|=w(iTn?DoE@!=aB3Qn~m zFozo)WQcv)*f#c^o$K6}>ew-b4Slfvgq5K)#0xowI*b>ELmufUP-eqPQ4#VmlJVEN zZ1Cuep%_L9u_$pW>ej2u;{@g(x^37{p)+Ym_2qOKTY`JN6b{g0gr%gjIUr@Vx;&M7 z96&Vgk_R}_8yCDB94}F!F+d~Jkcqb_J@&2_rdi17V2iq6Hf0$M@PFQ-&eISWITYXU zZ=*&?;wvv0{8Kq|2wcdygRVA%z_&8%`?|aA()W6JliBqv-;v=aG6bHlrDiepr-}|4 z$xci#Serl*d|g{LGei>p6KPz8#L_-gPs((_e2u-S&1P~)Z&OzKVqcM|>Z5{7Cl zfW>Krc4p^|ilsBji_qbOn6f21R#<7tgrqY`P~^pY`PSHO4|$$urNSO;`46@GlxUJ< z^pH<8+N>W|lw&FPXG|!EQG!JJ$+@pU0q3*mUEZ(lwut2~7e?TjE`M+Vo`N>NjcAq7 z#Y_bHemk3#t{WB_houWAnuW?WksxRo^l4Q;1ghUo@>Foz+H+xa@KDNQn8k;MncmS2 zLbKniJBJit7OUw;r$SRjM^4Pj<>Zyv-Qh`n%=NY;r{Rs45W}9(A^BX`joc6kEo;4n zL~enW6_=7JwgQqN)ajf80y-?*5(dCqHSE#ox&sqQ3IDF|7I`z4h19z_%BHq2&wE|+ zXsQV=AKa;|oIB?JjA{w^@EFEjRuB&VO-`r!P_%VzdhWgP(5V>VWjIc6RhcxbZ5k$=3U?K-}!A;S?J^Gc; ze^3H>pXRmVi-|~c-eLDcZuE)7hcHfx)PhW0NS4;X_5_%&`lr-$1o^4XoDq-{lkc@J zWs!FzhJYogJzk#Sg$479><0*|G-Q+o?>3V)og(_bC@3F{`oQ5>;cm<3xM&Mbx7%NZ zw58N%!>5#jk+qx(P^r4n#TNlG$^upmU+QbJn*71pFiED?gwCfPh@JPSh|i30Nq0XM zXAKd$|2Y1;4Ep$A;FKUdp$9m>|5~ef|E2W+|5ueD3X=nCwqG<#r1jwS;4@K&ab?1( zC75k9cQ)%0Elh029IL)4oZ4r_3+IO9m_JlT*qhc-WRW-&W+vBio_Vj=GB*E%NPK`R z_nSeuU|OUrDbtSClP*XQS@1I9N#_@uW%OHn`;THV>w$tz8arpU-6m{w2x1v>XG0CH z+KH6x;kSXuNV*Adn(f_d^|lT(HeA*z#I>d@Mx38qUYm~sCM4y}blo0l@4Yv8%Xd~w zp|%rt+CgyVzenR@L##rRH%Md7tj}wR=-D(pGWR@=o%Ot(UR$g5TkNlvJC6VIcbAiR zDx3$7w$hnsPvu=XXP@1cDK6LunWf_eM_X4aTCM~AkTnR)f_Gm$c6qyES53l?5Uz1m zTe#X#xL#FOllwJjI!atK3X=b@=r@v+Oaur^fNPHaKqQG_vepS+9)Zpg6d7~WtE@9D zmB(+<9Bm3^To*EqLO0#RugyvyqQan7rADMw*cc%qVnB4maW~(Cb{xM6C)<2}vh*`r zbqE8Vew!^)FC~<4(~B@0LgJnNvc$6ijX)kAAiImw=>@521 z=pp}W>LVr`xTWUy2~^_WRLA2`tT*fzHVqc+d;PTK~)1dnFQ+lKm}2u_+^#gOK;B80{c$UiLc5e4&ca9qLWmB=9?ls8!> z?N*sr=8}+Cdwe=SIorOfWY!_RoMx+kwC^gkIfaEk^ROUZ`>-IuFDGd^u|VjPS#`@V z1m)A1cYF^nv~(ltV?a4&Y9l&$LaO!Z9Whci_Mj>hkeD{MGwQDo_;eLoxq&BH8K0C* z_;S|)wvQ|AcTA5~l?aLL`w9ULIPtk(Y*<%KoNGAFi+R;fs?%y>1h+^j2spQjZ!JlH z8>0$r^}|XNK2;ofHwy^u~0u#Xg|A*%#TEI@9@cQ<;fyaoeAhsSanHmejW7l595A8Q=`ITAEJP+s@OGog+xV z^Yc`v4du4h-E5B~0!>z|#XKu}Zh8vI>Ym0q*$}f!4f#R4JyB*$2R3rLg;6bbABx*2 zPxgL}3c+0KI(sGD8nh-?sezLV4vdsXTXepFnp>g<=?!a(%(LctM+slUc6WSDDNgZd zE~$_^Iz@uz!`lAR2um$F$`nK=ZmlpNg{6mFREB<7%wb&3uIDr3&}2Y%due?ABDa z9D@%s-+*@NQS2^rjHE8=EnBvjYLwB*F!km&d3zQXI^ys)+yn(la>naZk+vnYuw!aI z*VX`}(@y~0Lj5GxZt-yQs><&vPZUsV=(-x*ApEGA2370A;H_*!M0+8XS8fIHpj}3! zNV8rkBunkCmle$f-y|t6L-TOt(L-A`y{oukFr6JJVn#pC@sqr=?r+B8iyLk&3I8O= zb-HFQmpKlP-GGk-PXbn@k(B}KHu_bv*D5*>E1!j)>i*0iAl+U@!gz}?NQsVEx@3sX z7kAH#X=nBw2nF0O&e?;sX%JeT8wT|)#Zvb3uj#m+mwmoEiT4;GtpU+rhNp2TmK`2ZhqDOcATj_a&E?29Y~EKl z@fC&FRk#8srRdKG#}Naw9xA;t!w9%pj63<*MKB1PUB-kP(ll*8N&-0-cB_VDBi)X1 zP&6*mBPkkN$}c&1`mGyOCB;Rkgz%K8bmg&72PbP4n}*r+mZF(W&Cn~Mb8eHXI;CcV zODNiq^(*JhjOTG3sZ?B~xNm1N z@9gTv6K00k*%>LZ*hf6pmMsIIv=t54b*6<$D}h|z)%w^7~w z$}4n?Q*s(G%e^; z2^RtDyd(ZF(c9rQj)|<)v7~=T7R^-heOMYl(Jdv>pO>HZTV5^D^-|Th?<0Xu{NZ*CWx0 z%w7yM5T7DXPuHI3M$nfSwYN+in`j?@`U@ZXbtoN^+#1MR5ov^TTvKWf{ho`X4w`ByZ#2k^mpVs{@AZ3U zEi40#v%rp9bM-M9B00e}V(khgz3K;79ig)n*s+_Vt;;ac`iV@c%q&&p0}Ln&MXCnt zXVd%fVz-gmgL5KyxO6u~PUZmJ_TievWIx#jP<}&~1jC0VDf5vn|DMpZkeihjZgwYz z?8NMPcrw-_Ck_p$7N@7nt&i%+DAZM zgR}M9YA_;a%)X-af<3AT^)8p0YVz1xp5wxGN~vf(SZ$w)51gc8R^OXF6i&z8WBe4N zQ&Q>=a@RH;*d~lE+1G~UkD=eQ##;U1Zq-ZA4W4c)j=dqBBKO&z*a<`ltp@ z3*q#|xz!?OfFpz21j_E2U(!TRwB|p2f2ppGs~m5KOKg}bMKo?(rXp4U@CZ~)RrtIi zSKdGN_#*m~e{V9Dvky8-Z-$e#+pCbSiX==P3-6cDA-uR?PX6t1D^5%MdMptGth*-} zV%6p6We** z--ki|?Fs0|LiBz$NIB4|l&W0CX`1&o?k)N{Wunp0BJ+WaWWK|D48Y(<>u>^;9`ahv zOQ3+(% z=~*RaqURlXd649*>M}L^xQ0-ES7dkGykRxW?rObj?$Eq2uu|8ikb?+3L$HNys1XBP z&<3`8<^fvXZDLJ{zP)n!@OpnNd-NxXF{VB?sjV`{jRHb`&2>OS=@eN|GT!z{)4iX? zs3Q)Gn6?BZR7|(=eX1lbSt-7)yUyP2uMFGe#&wFj>Norb9EuM0!Zb9gdAP9=%jlF2 zz2^fuZcRPc@Pv$*{%8XKY3UY$^^@6#Br&jYGF{j-gR7Jgzki`IN{Xc8Q((sxNXD$r zM>BY~d#lXFW6DLfcf_9kv6%P7H9>fUL59G};?3~)R}9AYCgAITO%#2JIn9&ZP8};( zDr_}0hyd`CQKEEcCt-F_yCGz#b?vIFMVNU1M6=j}Ax3zzPT2qkSin<1a{B29Wp*?o z5I%N_)*A0!LfDNvsxPVV-Pg$B+{rJ*MOkQsB?gF2^pkr<5)d>2C&x3&RoAKodm(H# z<3G}N6S_tcK+N|E)OY|Z5d~0!wRlLn^fEpE2AT#Xre!~Wykb7jiVXzwlB6f@80T$> z4gqV?4YD>;R0hyM^Ub3^tIO5DLS1qoPVCXnQsgzPD#C{ zaIzT|#Bb1YdEx&TaUj7*z{g<2I2DBGWaV5NM9ft8Y^biY(6EOw=$M1;zW_+Fs43 zkKiUbP2&NmYJw8u_iJRES9)4v)`gW;OJtdfBSNuA%{n8|EoBP8aIE^qW&jgm{;73R zmA?nlH2R&Qzp5wObqa0G1?RgZ&26$o$C2SUl9cAy@5t@B6)Esi+V;5H#ddiD zq-Lx@qF_&dULu233w1SDbRyLoUpAZL#ja2b>+K7)z619vcrT@JN`c|86{Y31m`O-u z@U=c8o)A8qN=J3(r{{u5&vQ%V8mRp50M`hJ=mk3H*q zqW%ovVBEputOAufAbXpaTi;U|Zn+)Um~ek8!t$bz=s<&|skEFKjN50Gqh5NJw?Hs- zg5>}acJ^}I(E}1KXUod7nTC1|SdNBQTcT{|>VnM(cK%H{+p!a_!?k-rIOBlXY8hYP zKjxJqQ3LW%ACTUtqcMI=In5ELH)70ehZS)C14j*jY1->NZ>;Tz?L`oys1`o-a|b1U zs=f(+{$S&tJ$(>UcKQ9T+L`neyk)S;9@h+{AU$8QE|c8n?ELRwfOb(ph*NC^1LxUo zfN;@mtxEiM(JB5?x7;91dm~PKGsGdRwXgV>s`u=Ivdb?^mK&DV{M7)G6poH@f@ihU zK>98W_QStg+-8-gH_7KerY0$b)n~i&=t=qub$(-y4PEf1Anrw-CdWPLo_tzZ`x8L{ z!6sCN8zXy>Yn<|1e(iPq*yWTpqB2>wbbv3d&PDEP^~rZ-+LQ~ZUpuY;h+{b~eP^j- zm>A#qOvhM{pypRVta@1q5dFie_;k~ zC#nCg`>u+`WF=q?4-w!$&6;OjQ1kEkuPTvT7f|D^k`u%4h0MNLGtdSW0(Yv3f9e^B z*Mg=`3*4SXvqX!lCv1w&>LkwUP1-|83IZ^f0}-G6z>g1;v~M{`U)12|*y~Sb>i&e4>|K*EM{z(0*c{Uh#S#vXl$NDSr})K0xi9N&I+w{&gldUX=6e+%gl+cBVX@ z0ldemzvg%8>+eP6A{=SqbByyyc{Iq`o0NQkToP7FW75m(+ zq;c%RC zBzh0M-5vc~6I(~R7m%l8s|-BmYd~!aDuMsCO0^T(8}&Mnv7FPj&SDmr_@_$ko6|v7UG>!=DLCl+@EOZzR`6kfJnem^V*}OL7(Qnw4ju zOR$+?*U=qx5-n$0eyC9^*W2S3e_~Oj1|%(-z_EFZT{U(gwNn%r@3^-8BBE?VHq_ zbXVqAg>!D)9&q32!Ks}t>G2}xyI;{ofzt++=(c2T{d$2srm*i3jb#Q2Dx=wo!xyAX z@z5>y`dz}WL1`Wr9%KB>^vN9{z7ftEWHV>YE!z>sw(9A+0RFsUBkmJQzr(cZcHr^L zg&ti=>zlRAR+Wj8St);xFn^36?{E||b41Mz`L?in1SnDeg_1lX0XxUNMCOR2z=R*< z_8Ooey;i7-6kPjt?KJ9`4pBIWX1J+EK$^Uvb!o7y6jwIZ#n9Lx4v zha1gW0uI}qga3T}RSiM(OhhAXg}dWOVM>I^naq<+>KAvo`Qu&cnZ+8`0GBeu@mq-r@~wDB2t5it*00h1YcW;9 zWLk*{JoZK@p}?Pqhw*HIbhS4((F_$X+^3WtYo@qtEdsh_<{deIc}#DR3#H39M#ONe^rFN|!N4L{F?Dq4u6!ex-8viXVkP;x z8Qq%}=tzG+csG*dTci0qT#gz@K~T#dnxU4zpT1VGKafRzW|pwgqcPDM32_e{B3b8T z78V7JdVW76;z2aZlw@#|6U@gz4vWNO6Ns#objn5ax z?-Nk@o0M)o<@JBnq4IZI`5^!6HZmaphd}rr@0wqq5D1`{*oPXHsc*m-hHGFP!f&Hm?!NW>?(Wtd|cbvq_;`%5>1lx%FdM}>1+a@5MUV?$=ffvDC*nRZCzU3rx z$D8{KmbY6Dl$mgE%^^23ev*+Nfw%*4tFKVRh6lnzmgh9&hVZ)B+7O$1r^kcfLoc~3W_sV}gu^O-gbegaa6Kh~kc!c=e98K+ed#j49|Arbz#Sx`B}R6)mdx)=!GBv)1}+knBCt@pIegkeLyj6Q%% z057vuIW7J%9b=jqPN!g&PINGhhmE^A%{kRnCLi=AJ7TIaH7#7&cwr7 z+;N0!S*bL%%C!*MwO*S%%ZOH&ORUGQdAXmsp~dqw+Ip$Kg|9k?L)Tqd0mAu&|4&)x0S(vowsA%mV)Ql` zy%W9nZi46~M507*F*+G!kVJ{xBT6tjiRe9g4-!I%(L0eKxOzx@6F14d{@+>ati8`Z zzw_=>X4X1ozt3~z`AD`h*_;ohs4^M>Masa2Z2JeO%Xwr;3rcKFxyA)F#F!?(KhH~6 z{z6FqVfYoUc^9@YrCz1cmFhGbj@v;9+Bl<@IjI)jz^DP`?wg=aDk;5~HAr9kSCB+2 zwNjRdN5-fJd-)C<^|!hzhD9a{&D}a=`H~_Lw@=o?EM4toCL-G1hsYQa!8;UWBlT?( z&B&}p8zRwrV|e~XI@7K%v*{@A4=J{_3+tpMA#HcvsoIiv80+rG2H-)?|9Hg!axpl@u>DaP*khhWmea`$vC=Y#9I8>4F0TGve=b+`f;P5SZAeDc1?f7qhch3 zr_M2Vk&S)B`lkZDOiw4TXFIHth!+;kg*p-gtKOozoV2EcFG_7j_T3U>&vR!<*TiHz zQ>T;HnwJiH8u+i?@7PXJ?T+@RSw}I&3vZ2C2b8$vY{S=*c*&wa5QC!IhMl%eX{AJ9 z^Y2!?N9WoEhjUL!4H>b&dJ>p`K8G8^}$ci8E*KLaoR(2viX9X1QM_<-^6o?M31iEJ(&w z{Tb|;-xU#puX(ehUUb^3K^YCk!5Il;+^%4QZ&F~ZCG%1Vq||Vo3F3iz^&y`uTc-H_ zvCE6xSJBh@_>y7l)1gQ1dwwMj_Qu{gvoM7S)NRrb^v3H0Htl8TDmDAK+Jr%F!c(N1 zwycbPW5$NeT)Q+EPo7yLN>Q7wepranssHbhInk3WXl8GpoEkAMPXf}{0k0qU!GogV z8nhVgkS5=s?`-8eM4-J-;q0_2KPq5tDpJ=C7KA_wYs0ZkcfRc7Gx0^*ix?IL$Q43@ z1<&5^5H@R_27c1c|3nj9jT`s|{@3s~$>FvYZX*I$=?TC5$LqoVI5@DUU2rokMISbn z-08nrAEOhuu^HYE+bG5h>4t?Tl2JPK5oF1IMX;PITtO8OxrL)6+Q>XeDarz*MA<;iN8ULq@hChR~6C ztESDcY}T*FGqg*EvfrMFmaxC;JK5Cdx^8o9vLm9YevuHJA`8iWE#G~(QUo^|`2GWC zN+L0QWZ0VxLEt21snh!#>XQ%T_*k|0;kN`*Jt5b`3aQ+r0@PV(eRpN7tys@!$;nev zuiD7T%f2_JqxovWri%Q_iUA1H7WDjw@p(u6w=>8vqkhm|}t* z_XiKth&}vv3Z(PYIzx8RxDQSCP#Rt{dKzh*LEO5OM8y3apLcNC`8!K>v^_H(F>OSO zB@IRd;07U3<}8j#vN0Z2riy;A>M}*K9&kcMPBoRSu5trAc9^Er%-gD+-YYe!m%^Qr zOUJQyekE#X!l(BNB3x-;*PSAJ*3+q3#;N*K^S0vT=>Ge8+cxamc!#RsXl{@^X?ry^ zt}_}d9eX18?I&>umkF8$yg@nf%@2u`P+{eWaEnoa>9Jyj;7u_&D6hbGwVG|CSdise zY&tGm=7CL*5mEOHDmoXvN<6+VJAI^I`JRwNuAa|nU|=e^;3Ee#bVSISyYXI#)^0bf z>kR%v!Zgp35~>0EA*hGm6o}}6ucd|yJB{6-5^`y5oOd-d-MjHp5&~T|ae?p`YG$?Q zWrY=5;))_TyGq{r8NZY;Ir(5BBxGWIe|s$>2Ql_V&^Raj`08S#>!+qN_P>TIXesI@ z3Y{C9Vyb%S`H#XV&HZNsXzK$9`{7=~@26Un$Gc{koPy;FuX!E_M%_4Abk$^iwbxPF zuy(5_?4g}wRvK#pX%Ss*Fn=rjX{+uFyP9{#eg=6si1Y*&hKfV~)_Ts*YFWPwdXp7@`tp)AV5f;>Sxc}T%dh(*RzbkM z&bKBLHOKCD2)=4L@v^w0sZZAJR{rm}Y(4nX3xM4ZNx~u7g^~wGaFGwui`xlr z1>=B)H>p@GD00M?0fwN?H3m@gF|)o(9k{S z(jGH6vd=n#hjpc6G&kK@a|(6I9+$RkpSEtF7S?WuiER#f;=|4-P7;n<=bPq_wt(}X zeIG3TT`zj!r@J4(w+gd~g;RXWBcWG$5_gCZb3p}h=21JY^yP-$w6|Vm(-2U<>%gWj zH@-$o;QDA!@kcXCDdu?L`&kf<@jP8;dSIy7c5{@I7wMvhykdJ+eay~P=K1~=f*^ls zWWeq*7%|7x84@VpnH8fJDEwsGelAZwM7{S^?HKD7JWlEfzeN?Gd|@mRWe@mSutNFEd6Wwmpw1sbWoU2|~F zR(kE@4V|18lb@K!IN8ikqbHTFaj?$#RoC5gd+$8Cdzj=wKe<~=@kKIivbof8%fqTL z8)jjFU*V%xJo?~<{^sxk8d| z18OK@tL3}YRWgnyxex*Zc@9peG@02dQU&3h$x6(~*tEmeB}Gd!+odTxhrSw&%Gb5& zAU%kLie zS+VYBb;GY_j>PK!AnkwG95GgXH+1p`e{)HBkwgtU$&!m1GMoSdE&>d!;T*u1RP#8C2(F7&c{Y3 zP~l}`hui|UJkSS7jVK&y@0u5esv)Ge9PqJ87noAf*=oLz4Hdo zkdN%M&P4({6iXxL+%N~5!0_vn^s_QeU(XW%vW`iM6ZXh+naa;f-WZ<{8m#g|z`A*} zEtefU@}7_wb$UZNYZNn;iS{%@citP)v=-k8ht!p>aZJj`%zVOs;JRG3?!KCQ^7&OG zbyK;PebPt8PeZgWCM=`GY1MkAlLWl41$12W+uVj=@2=X)u)^93lx=P&W&3Lre!Uwc zs;H%-W$9(*U;p$yrCUF}u5NyVG2+D@-+&`tY+}UlvFeo&!~C zx!oX&E%R^JPeBMzqg{>AM-2D-*M6vuOL869G3jbI$ZF^=JQXqGs@nf>_Mz{E&f5|f zt^F1gEB+USx)v?$GrcCE^tN|gYzv0qsDgr}v~}~5cxLLBbz_l^Mprt+T?s^JCNeJg zOJH6y+B6hr-o%IB(|lJj*`#-*Xp)kzbr3Rt#|}tIeLlR752NNX4MlfMlrM$6E1Yv( zF`~koeYz7?h_Oaoz&cMPQnFK3sI5{ctaS^wcBbjuXIbWs3ygW3wd7>du2`BZ-`p?Y zpczH`x=rKoSb9d3JF3gPf*XM-v&#tEy+U&aJJanc4?1LwyOF{uxY15egGhheUR#qhYJ~AFX5e(MlIhtMCl2>@ z5%yd@wh)pKwkp{Cr+h8NqM~>aHI{ffOWUR)E(*sXwsQZMVU{1dEXa1IgZxw)HH>174h(X`S3;7nK)I9>aBAw?mD&pZ6|pjp|b$OI2{S zVJt@1*7(bdqn%G|`mN)sX&H)mpM{Q@o7m4ic})>X87x^|-IN_!Zy~KdkOkxqzhu%J zA}?BLxe~J!-bCdzqMU?4Rq0aPDoXO`dtZo~t8=Ki`I#|PL;<;B!HzVlNn zsp7lSOE#SMwnACBtKD}tTJG1&e|}aU<>%4r_1rRgAa^I^jdUh}Xnl>oF$KHp5tb2! zT(3r7y0fdQgcoliw8j}(IXKxgHraH2Q_$})cWAE4CDCaqrm{k-;qL3@=h97gPsGhK zLXqx|7g!;z08^2>3Qkx#^vd))*@5o6u4gOXt}Zn`Uuq>zv6omKm!OK1NbR=Td?O3# zu0{GNFrprjGD97w%`0NHotSrT3U5%Yun|E2r-_%>IY=qr-u|9);Pm!&PGuEl% z?nC*CIyqEor|dVI5ISA5CvP=wxapV~8-9&Ss>-$$j4DMrAbB&>ovpR)`6H^C$#bUd zr1pY(BM^7;gRXg3OR4KY*b<(^*+6oy>lx1Kd>dJQQ~Lgyc$FV3Hxt@?{OI8u99A?A z#r2h$y{)e&(5!NK_rR+Ju`ymbBtD_fSRX_}PLb^28zmxEcWDOVHduJsWU$jXSe$sb z^&%z~Gx*kA>4GLyb?NXnY3M0?nKuiniC?xnfzTj$WT7hXV5LU<65nbGjHnql--<4y zfB{YPV+yrE)OuMQMkcr>DzTU;AIb8;(*W$mzPouM{)!?spdnR?LTV4)vsB7m5y8+$ zpfwrOj)MkOenVL#Nw?KtD=;L!`*n+sL6-bM+i2ORlCX}$Y zQrg{g!0@FB6HHqN{9KiIi1XdGuN>%R@D*=b$Ir9sZEq;E>vDxWG;F4<*@OJKGgq(# zyJMY&p;hK82lPnC8R`w;S+m6v=lSZ$j1GtH?ux`;}VetV`A31=n32w-AuaJ*0=3Y?@GqYpn z-&1}>+!f%-i~;EE3a~I^3eq3}q-Nzna1vm_PYeLhfr0v22GIFc%pyIoIjaw;fF0V*nM>Tr^#smjrD>FjaQIWu6wYe(cf)!#vitVQx|7e)o(i z^CQqq7f{zE`fU+en+Lep&0r4!Fiy;4N`1m_cenf%vAGmZ0iu=>( z5g5S)sg?WFu2VpM5#*A?hEpXcGTRjsJg2_>V&I4H!0v3J3EyA;hF#VxFM>SE2Y< zZpQT!@^>{1inQ40ttaw^fge=Uw;;- z(8{1%;9~JD8x(jx1qSX+Fo2q$W11L&)Cmqu)+z%iJoAFkm}G%mN?g6vi7V;?`X`T- z4)QaZ7*erk_oaf8Rf0GG8uj`}X|>s6Rsh3^`K* lq%&a1rHAfIu)ydqn3f6;lSA{<>s1Pioe~R6Z}#WX{{gLDFVp}4 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 03ca076c8..02292eac4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ +#Thu May 11 18:05:55 GST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=7ba68c54029790ab444b39d7e293d3236b2632631fb5f2e012bb28b4ff669e4b -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 65dcd68d6..4f906e0c8 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/bin/sh +#!/usr/bin/env sh # -# Copyright Ā© 2015-2021 the original authors. +# Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,101 +17,67 @@ # ############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions Ā«$varĀ», Ā«${var}Ā», Ā«${var:-default}Ā», Ā«${var+SET}Ā», -# Ā«${var#prefix}Ā», Ā«${var%suffix}Ā», and Ā«$( cmd )Ā»; -# * compound commands having a testable exit status, especially Ā«caseĀ»; -# * various built-in commands including Ā«commandĀ», Ā«setĀ», and Ā«ulimitĀ». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# +## +## Gradle start up script for UN*X +## ############################################################################## # Attempt to set APP_HOME - # Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum +MAX_FD="maximum" warn () { echo "$*" -} >&2 +} die () { echo echo "$*" echo exit 1 -} >&2 +} # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -121,9 +87,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java + JAVACMD="$JAVA_HOME/jre/sh/java" else - JAVACMD=$JAVA_HOME/bin/java + JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -132,7 +98,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD=java + JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -140,105 +106,80 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi fi -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi # For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg + i=`expr $i + 1` done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f1..ac1b06f93 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,92 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle index a42edcd64..93790875b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,19 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + google() + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + maven { url "https://artifacts.consensys.net/public/maven/maven/" } + google() + } +} + rootProject.name = 'jvm-libp2p-minimal' include ':libp2p' @@ -6,3 +22,27 @@ include ':tools:simulator' include ':examples:chatter' include ':examples:cli-chatter' include ':examples:pinger' + +def getAndroidSdkDir() { + def localPropertiesSdkDir = null + if (file('local.properties').canRead()) { + def properties = new Properties() + properties.load(file('local.properties').newDataInputStream()) + localPropertiesSdkDir = properties.getProperty('sdk.dir') + } + def androidHomeEnv = System.getenv("ANDROID_HOME") + if (localPropertiesSdkDir != null) { + return localPropertiesSdkDir + } else { + return androidHomeEnv + } +} + +if (getAndroidSdkDir() != null) { + println "Build configured with Android submodules using Android SDK: ${getAndroidSdkDir()}" + include ':examples:android-chatter' +} else { + println "Build configured without Android submodules." + println " To include Android submodules define a valid SDK location with an ANDROID_HOME environment variable " + println " or by setting the sdk.dir path in your project's local properties file local.properties." +} From e7d633e481520017570f577fa212ed52ca095509 Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Wed, 17 May 2023 16:29:17 +0300 Subject: [PATCH 02/16] Finer grained netty dependencies (#276) --- libp2p/build.gradle.kts | 9 ++++++++- tools/simulator/build.gradle | 2 ++ versions.gradle | 9 ++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/libp2p/build.gradle.kts b/libp2p/build.gradle.kts index 246b41836..497726425 100644 --- a/libp2p/build.gradle.kts +++ b/libp2p/build.gradle.kts @@ -5,7 +5,12 @@ plugins { } dependencies { - api("io.netty:netty-all") + api("io.netty:netty-common") + api("io.netty:netty-buffer") + api("io.netty:netty-transport") + implementation("io.netty:netty-handler") + implementation("io.netty:netty-codec-http") + api("com.google.protobuf:protobuf-java") implementation("commons-codec:commons-codec") @@ -16,6 +21,8 @@ dependencies { testImplementation(project(":tools:schedulers")) + testFixturesImplementation("io.netty:netty-transport-classes-epoll") + jmhImplementation(project(":tools:schedulers")) jmhImplementation("org.openjdk.jmh:jmh-core") jmhAnnotationProcessor("org.openjdk.jmh:jmh-generator-annprocess") diff --git a/tools/simulator/build.gradle b/tools/simulator/build.gradle index 2529f783a..4798a1dbf 100644 --- a/tools/simulator/build.gradle +++ b/tools/simulator/build.gradle @@ -5,6 +5,8 @@ dependencies { api project(':libp2p') api project(':tools:schedulers') + implementation("io.netty:netty-handler") + implementation("org.jgrapht:jgrapht-core:1.3.1") api("org.apache.commons:commons-math3:3.6.1") implementation("org.jetbrains.kotlin:kotlin-reflect:1.3.0") diff --git a/versions.gradle b/versions.gradle index d7c299f79..cdf96f735 100644 --- a/versions.gradle +++ b/versions.gradle @@ -27,7 +27,14 @@ dependencyManagement { entry 'protobuf-java' entry 'protoc' } - dependency "io.netty:netty-all:4.1.87.Final" + dependencySet(group: "io.netty", version: "4.1.87.Final") { + entry 'netty-common' + entry 'netty-handler' + entry 'netty-transport' + entry 'netty-buffer' + entry 'netty-codec-http' + entry 'netty-transport-classes-epoll' + } dependency "commons-codec:commons-codec:1.15" dependency "tech.pegasys:noise-java:22.1.0" dependencySet(group: "org.bouncycastle", version: "1.70") { From c6fb638cbd4c3c688398699a5feaa15f53079d7c Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Wed, 17 May 2023 16:32:13 +0300 Subject: [PATCH 03/16] Remove Apache commons-codec dependency (#277) * Get rid of Apache commons-codec dependency --- libp2p/build.gradle.kts | 1 - .../main/kotlin/io/libp2p/core/multiformats/Protocol.kt | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/libp2p/build.gradle.kts b/libp2p/build.gradle.kts index 497726425..b2942bd0c 100644 --- a/libp2p/build.gradle.kts +++ b/libp2p/build.gradle.kts @@ -13,7 +13,6 @@ dependencies { api("com.google.protobuf:protobuf-java") - implementation("commons-codec:commons-codec") implementation("tech.pegasys:noise-java") implementation("org.bouncycastle:bcprov-jdk15on") diff --git a/libp2p/src/main/kotlin/io/libp2p/core/multiformats/Protocol.kt b/libp2p/src/main/kotlin/io/libp2p/core/multiformats/Protocol.kt index c190823a3..3a6a513f2 100644 --- a/libp2p/src/main/kotlin/io/libp2p/core/multiformats/Protocol.kt +++ b/libp2p/src/main/kotlin/io/libp2p/core/multiformats/Protocol.kt @@ -9,7 +9,7 @@ import io.libp2p.etc.types.toByteArray import io.libp2p.etc.types.toByteBuf import io.libp2p.etc.types.writeUvarint import io.netty.buffer.ByteBuf -import org.apache.commons.codec.binary.Base32 +import org.bouncycastle.util.encoders.Base32 import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress @@ -186,10 +186,8 @@ private val ONION_PARSER: (Protocol, String) -> ByteArray = { _, addr -> // onion address without the ".onion" substring if (split[0].length != 16) throw IllegalArgumentException("failed to parse addr: $addr not a Tor onion address.") - val base32 = Base32() val base32Text = split[0].uppercase() - if (!base32.isInAlphabet(base32Text)) throw IllegalArgumentException("Invalid Base32 string in the Onion address: $base32Text") - val onionHostBytes = base32.decode(base32Text) + val onionHostBytes = Base32.decode(base32Text) val port = split[1].toInt() if (port > 65535) throw IllegalArgumentException("Port is > 65535: $port") if (port < 1) throw IllegalArgumentException("Port is < 1: $port") @@ -203,5 +201,5 @@ private val ONION_STRINGIFIER: (Protocol, ByteArray) -> String = { _, bytes -> val byteBuf = bytes.toByteBuf() val host = byteBuf.readBytes(10).toByteArray() val port = byteBuf.readUnsignedShort() - String(Base32().encode(host)).lowercase() + ":" + port + Base32.toBase32String(host).lowercase() + ":" + port } From 441e620e62662741426648a22b13ffec9d481b68 Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Thu, 18 May 2023 06:25:36 +0300 Subject: [PATCH 04/16] Migrate to slf4j (#279) * Replace log4j-api entries and dependencies with corresponding slf4j analogs * Add log4j-slf4j2-impl runtime dependencies for examples, tests and simulator module --- build.gradle.kts | 7 ++----- examples/cli-chatter/build.gradle | 2 +- examples/pinger/build.gradle | 2 +- libp2p/build.gradle.kts | 1 + .../io/libp2p/discovery/mdns/impl/DNSIncoming.java | 8 ++++---- .../io/libp2p/discovery/mdns/impl/DNSQuestion.java | 6 +++--- .../java/io/libp2p/discovery/mdns/impl/DNSRecord.java | 6 +++--- .../java/io/libp2p/discovery/mdns/impl/HostInfo.java | 6 +++--- .../java/io/libp2p/discovery/mdns/impl/JmDNSImpl.java | 6 +++--- .../io/libp2p/discovery/mdns/impl/ServiceInfoImpl.java | 6 +++--- .../io/libp2p/discovery/mdns/impl/SocketListener.java | 6 +++--- .../discovery/mdns/impl/constants/DNSRecordClass.java | 6 +++--- .../discovery/mdns/impl/constants/DNSRecordType.java | 6 +++--- .../io/libp2p/discovery/mdns/impl/tasks/Responder.java | 6 +++--- .../discovery/mdns/impl/tasks/ServiceResolver.java | 6 +++--- .../kotlin/io/libp2p/core/multiformats/MultiaddrDns.kt | 6 +++--- .../src/main/kotlin/io/libp2p/etc/util/P2PService.kt | 4 ++-- .../io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt | 4 ++-- .../src/main/kotlin/io/libp2p/pubsub/AbstractRouter.kt | 4 ++-- .../kotlin/io/libp2p/pubsub/gossip/GossipRouter.kt | 4 ++-- .../kotlin/io/libp2p/security/noise/NoiseXXCodec.kt | 4 ++-- .../io/libp2p/security/noise/NoiseXXSecureChannel.kt | 4 ++-- .../main/kotlin/io/libp2p/security/secio/SecIoCodec.kt | 4 ++-- .../io/libp2p/security/secio/SecIoSecureChannel.kt | 4 ++-- .../kotlin/io/libp2p/security/SecureChannelTestBase.kt | 4 ++-- .../kotlin/io/libp2p/security/secio/EchoSampleTest.kt | 4 ++-- libp2p/src/test/kotlin/io/libp2p/tools/TestChannel.kt | 4 ++-- libp2p/src/test/kotlin/io/libp2p/tools/TestHandler.kt | 4 ++-- .../test/kotlin/io/libp2p/transport/TransportTests.kt | 4 ++-- .../io/libp2p/tools/schedulers/DefaultSchedulers.java | 6 +++--- .../io/libp2p/tools/schedulers/LoggerMDCExecutor.java | 10 +++++----- tools/simulator/build.gradle | 3 +-- versions.gradle | 6 +++--- 33 files changed, 80 insertions(+), 83 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 03ebf0a9e..dfe2d0e01 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -56,16 +56,13 @@ configure( implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") implementation("com.google.guava:guava") - implementation("org.apache.logging.log4j:log4j-api") - - testFixturesImplementation("org.apache.logging.log4j:log4j-api") - testFixturesImplementation("com.google.guava:guava") + implementation("org.slf4j:slf4j-api") testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.jupiter:junit-jupiter-params") testImplementation("io.mockk:mockk") testImplementation("org.assertj:assertj-core") - testImplementation("org.apache.logging.log4j:log4j-core") + testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl") } java { diff --git a/examples/cli-chatter/build.gradle b/examples/cli-chatter/build.gradle index faa0bc75e..1d970e4b6 100644 --- a/examples/cli-chatter/build.gradle +++ b/examples/cli-chatter/build.gradle @@ -5,7 +5,7 @@ plugins { dependencies { implementation project(':examples:chatter') - runtimeOnly("org.apache.logging.log4j:log4j-core") + runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl") } application { diff --git a/examples/pinger/build.gradle b/examples/pinger/build.gradle index 953517813..b4ce18580 100644 --- a/examples/pinger/build.gradle +++ b/examples/pinger/build.gradle @@ -5,7 +5,7 @@ plugins { dependencies { implementation project(':libp2p') - runtimeOnly("org.apache.logging.log4j:log4j-core") + runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl") } application { diff --git a/libp2p/build.gradle.kts b/libp2p/build.gradle.kts index b2942bd0c..89c85a2d8 100644 --- a/libp2p/build.gradle.kts +++ b/libp2p/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation("org.bouncycastle:bcpkix-jdk15on") testImplementation(project(":tools:schedulers")) + testImplementation("org.apache.logging.log4j:log4j-core") testFixturesImplementation("io.netty:netty-transport-classes-epoll") diff --git a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/DNSIncoming.java b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/DNSIncoming.java index 351da96d5..f71bf1e29 100644 --- a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/DNSIncoming.java +++ b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/DNSIncoming.java @@ -14,8 +14,8 @@ import io.libp2p.discovery.mdns.impl.constants.DNSRecordClass; import io.libp2p.discovery.mdns.impl.constants.DNSRecordType; import io.libp2p.discovery.mdns.impl.constants.DNSResultCode; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.libp2p.discovery.mdns.impl.constants.DNSConstants; import io.libp2p.discovery.mdns.impl.constants.DNSLabel; @@ -27,14 +27,14 @@ * @author Arthur van Hoff, Werner Randelshofer, Pierre Frisch, Daniel Bobbert */ public final class DNSIncoming extends DNSMessage { - private static Logger logger = LogManager.getLogger(DNSIncoming.class.getName()); + private static Logger logger = LoggerFactory.getLogger(DNSIncoming.class.getName()); // This is a hack to handle a bug in the BonjourConformanceTest // It is sending out target strings that don't follow the "domain name" format. public static boolean USE_DOMAIN_NAME_FORMAT_FOR_SRV_TARGET = true; public static class MessageInputStream extends ByteArrayInputStream { - private static Logger logger1 = LogManager.getLogger(MessageInputStream.class.getName()); + private static Logger logger1 = LoggerFactory.getLogger(MessageInputStream.class.getName()); final Map _names; diff --git a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/DNSQuestion.java b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/DNSQuestion.java index eccba86c4..789aaf418 100644 --- a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/DNSQuestion.java +++ b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/DNSQuestion.java @@ -8,8 +8,8 @@ import io.libp2p.discovery.mdns.impl.constants.DNSRecordClass; import io.libp2p.discovery.mdns.impl.constants.DNSRecordType; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.libp2p.discovery.mdns.ServiceInfo; import io.libp2p.discovery.mdns.impl.constants.DNSConstants; @@ -20,7 +20,7 @@ * @author Arthur van Hoff, Pierre Frisch */ public class DNSQuestion extends DNSEntry { - private static Logger logger = LogManager.getLogger(DNSQuestion.class.getName()); + private static Logger logger = LoggerFactory.getLogger(DNSQuestion.class.getName()); /** * Pointer question. diff --git a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/DNSRecord.java b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/DNSRecord.java index bdc3e9ddd..6e75a6c47 100644 --- a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/DNSRecord.java +++ b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/DNSRecord.java @@ -8,8 +8,8 @@ import io.libp2p.discovery.mdns.impl.constants.DNSRecordClass; import io.libp2p.discovery.mdns.impl.constants.DNSRecordType; import io.libp2p.discovery.mdns.impl.util.ByteWrangler; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.DataOutputStream; import java.io.IOException; @@ -26,7 +26,7 @@ * @author Arthur van Hoff, Rick Blair, Werner Randelshofer, Pierre Frisch */ public abstract class DNSRecord extends DNSEntry { - private static Logger logger = LogManager.getLogger(DNSRecord.class.getName()); + private static Logger logger = LoggerFactory.getLogger(DNSRecord.class.getName()); private int _ttl; private long _created; diff --git a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/HostInfo.java b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/HostInfo.java index dd47bb6dd..c93d6d983 100644 --- a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/HostInfo.java +++ b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/HostInfo.java @@ -8,8 +8,8 @@ import java.net.*; import java.util.*; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * HostInfo information on the local host to be able to cope with change of addresses. @@ -17,7 +17,7 @@ * @author Pierre Frisch, Werner Randelshofer */ public class HostInfo { - private static Logger logger = LogManager.getLogger(HostInfo.class.getName()); + private static Logger logger = LoggerFactory.getLogger(HostInfo.class.getName()); protected String _name; diff --git a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/JmDNSImpl.java b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/JmDNSImpl.java index d8ad47121..d050053c4 100644 --- a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/JmDNSImpl.java +++ b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/JmDNSImpl.java @@ -12,8 +12,8 @@ import io.libp2p.discovery.mdns.impl.tasks.Responder; import io.libp2p.discovery.mdns.impl.tasks.ServiceResolver; import io.libp2p.discovery.mdns.impl.util.NamedThreadFactory; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.DatagramPacket; @@ -46,7 +46,7 @@ * @author Arthur van Hoff, Rick Blair, Jeff Sonstein, Werner Randelshofer, Pierre Frisch, Scott Lewis, Kai Kreuzer, Victor Toni */ public class JmDNSImpl extends JmDNS { - private static Logger logger = LogManager.getLogger(JmDNSImpl.class.getName()); + private static Logger logger = LoggerFactory.getLogger(JmDNSImpl.class.getName()); /** * This is the multicast group, we are listening to for multicast DNS messages. diff --git a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/ServiceInfoImpl.java b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/ServiceInfoImpl.java index 27ddfdd78..cc0c94dbf 100644 --- a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/ServiceInfoImpl.java +++ b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/ServiceInfoImpl.java @@ -7,8 +7,8 @@ import io.libp2p.discovery.mdns.ServiceInfo; import io.libp2p.discovery.mdns.impl.constants.DNSRecordClass; import io.libp2p.discovery.mdns.impl.util.ByteWrangler; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.Inet4Address; @@ -30,7 +30,7 @@ * @author Arthur van Hoff, Jeff Sonstein, Werner Randelshofer, Victor Toni */ public class ServiceInfoImpl extends ServiceInfo { - private static Logger logger = LogManager.getLogger(ServiceInfoImpl.class.getName()); + private static Logger logger = LoggerFactory.getLogger(ServiceInfoImpl.class.getName()); private String _domain; private String _protocol; diff --git a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/SocketListener.java b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/SocketListener.java index cf349253b..3edbf9acc 100644 --- a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/SocketListener.java +++ b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/SocketListener.java @@ -11,8 +11,8 @@ import java.util.concurrent.Future; import io.libp2p.discovery.mdns.impl.util.NamedThreadFactory; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.libp2p.discovery.mdns.impl.constants.DNSConstants; @@ -20,7 +20,7 @@ * Listen for multicast packets. */ class SocketListener implements Runnable { - static Logger logger = LogManager.getLogger(SocketListener.class.getName()); + static Logger logger = LoggerFactory.getLogger(SocketListener.class.getName()); private final JmDNSImpl _jmDNSImpl; private final String _name; diff --git a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/constants/DNSRecordClass.java b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/constants/DNSRecordClass.java index 41c8ff49d..359279f88 100644 --- a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/constants/DNSRecordClass.java +++ b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/constants/DNSRecordClass.java @@ -3,8 +3,8 @@ */ package io.libp2p.discovery.mdns.impl.constants; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * DNS Record Class @@ -41,7 +41,7 @@ public enum DNSRecordClass { */ CLASS_ANY("any", 255); - private static Logger logger = LogManager.getLogger(DNSRecordClass.class.getName()); + private static Logger logger = LoggerFactory.getLogger(DNSRecordClass.class.getName()); /** * Multicast DNS uses the bottom 15 bits to identify the record class...
diff --git a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/constants/DNSRecordType.java b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/constants/DNSRecordType.java index 3f2e36a26..0ad1e8e9f 100644 --- a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/constants/DNSRecordType.java +++ b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/constants/DNSRecordType.java @@ -3,8 +3,8 @@ */ package io.libp2p.discovery.mdns.impl.constants; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * DNS Record Type @@ -249,7 +249,7 @@ public enum DNSRecordType { */ TYPE_ANY("any", 255); - private static Logger logger = LogManager.getLogger(DNSRecordType.class.getName()); + private static Logger logger = LoggerFactory.getLogger(DNSRecordType.class.getName()); private final String _externalName; diff --git a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/tasks/Responder.java b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/tasks/Responder.java index 1a576384c..77dab738e 100644 --- a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/tasks/Responder.java +++ b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/tasks/Responder.java @@ -11,8 +11,8 @@ import java.util.Set; import java.util.concurrent.TimeUnit; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.libp2p.discovery.mdns.impl.DNSIncoming; import io.libp2p.discovery.mdns.impl.DNSOutgoing; @@ -25,7 +25,7 @@ * The Responder sends a single answer for the specified service infos and for the host name. */ public class Responder extends DNSTask { - static Logger logger = LogManager.getLogger(Responder.class.getName()); + static Logger logger = LoggerFactory.getLogger(Responder.class.getName()); private final DNSIncoming _in; diff --git a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/tasks/ServiceResolver.java b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/tasks/ServiceResolver.java index 895a7c71d..4b15a5544 100644 --- a/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/tasks/ServiceResolver.java +++ b/libp2p/src/main/java/io/libp2p/discovery/mdns/impl/tasks/ServiceResolver.java @@ -10,8 +10,8 @@ import io.libp2p.discovery.mdns.impl.constants.DNSConstants; import io.libp2p.discovery.mdns.impl.constants.DNSRecordClass; import io.libp2p.discovery.mdns.impl.constants.DNSRecordType; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.concurrent.Future; @@ -22,7 +22,7 @@ * The ServiceResolver queries three times consecutively for services of a given type, and then removes itself from the timer. */ public class ServiceResolver extends DNSTask { - private static Logger logger = LogManager.getLogger(ServiceResolver.class.getName()); + private static Logger logger = LoggerFactory.getLogger(ServiceResolver.class.getName()); private final String _type; private final int _queryInterval; diff --git a/libp2p/src/main/kotlin/io/libp2p/core/multiformats/MultiaddrDns.kt b/libp2p/src/main/kotlin/io/libp2p/core/multiformats/MultiaddrDns.kt index 61e0fcd4a..294fc5a8a 100644 --- a/libp2p/src/main/kotlin/io/libp2p/core/multiformats/MultiaddrDns.kt +++ b/libp2p/src/main/kotlin/io/libp2p/core/multiformats/MultiaddrDns.kt @@ -1,6 +1,6 @@ package io.libp2p.core.multiformats -import org.apache.logging.log4j.LogManager +import org.slf4j.LoggerFactory import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress @@ -13,7 +13,7 @@ class MultiaddrDns { } companion object { - private val log = LogManager.getLogger(MultiaddrDns::class.java) + private val log = LoggerFactory.getLogger(MultiaddrDns::class.java) private val dnsProtocols = arrayOf(Protocol.DNS4, Protocol.DNS6, Protocol.DNSADDR) fun resolve(addr: Multiaddr, resolver: Resolver = DefaultResolver): List { @@ -54,7 +54,7 @@ class MultiaddrDns { } } } catch (e: UnknownHostException) { - log.debug(e) + log.debug("Unknown error", e) return emptyList() // squash, as this might not be fatal, // and if it is we'll handle this higher up the call chain diff --git a/libp2p/src/main/kotlin/io/libp2p/etc/util/P2PService.kt b/libp2p/src/main/kotlin/io/libp2p/etc/util/P2PService.kt index f48f05b50..13c2d3f38 100644 --- a/libp2p/src/main/kotlin/io/libp2p/etc/util/P2PService.kt +++ b/libp2p/src/main/kotlin/io/libp2p/etc/util/P2PService.kt @@ -8,11 +8,11 @@ import io.libp2p.etc.types.toVoidCompletableFuture import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelInboundHandlerAdapter import io.netty.util.ReferenceCountUtil -import org.apache.logging.log4j.LogManager +import org.slf4j.LoggerFactory import java.util.concurrent.CompletableFuture import java.util.concurrent.ScheduledExecutorService -private val logger = LogManager.getLogger(P2PService::class.java) +private val logger = LoggerFactory.getLogger(P2PService::class.java) /** * Base class for a service which manages many streams from different peers diff --git a/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt b/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt index e6433d1a3..8dec78669 100644 --- a/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt +++ b/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt @@ -7,13 +7,13 @@ import io.libp2p.etc.types.completedExceptionally import io.libp2p.etc.types.hasCauseOfType import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelInboundHandlerAdapter -import org.apache.logging.log4j.LogManager +import org.slf4j.LoggerFactory import java.util.concurrent.CompletableFuture import java.util.function.Function typealias MuxChannelInitializer = (MuxChannel) -> Unit -private val log = LogManager.getLogger(AbstractMuxHandler::class.java) +private val log = LoggerFactory.getLogger(AbstractMuxHandler::class.java) abstract class AbstractMuxHandler() : ChannelInboundHandlerAdapter() { diff --git a/libp2p/src/main/kotlin/io/libp2p/pubsub/AbstractRouter.kt b/libp2p/src/main/kotlin/io/libp2p/pubsub/AbstractRouter.kt index 9d6dfbb27..1c9d78e50 100644 --- a/libp2p/src/main/kotlin/io/libp2p/pubsub/AbstractRouter.kt +++ b/libp2p/src/main/kotlin/io/libp2p/pubsub/AbstractRouter.kt @@ -11,7 +11,7 @@ import io.netty.channel.ChannelHandler import io.netty.handler.codec.protobuf.ProtobufDecoder import io.netty.handler.codec.protobuf.ProtobufEncoder import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender -import org.apache.logging.log4j.LogManager +import org.slf4j.LoggerFactory import pubsub.pb.Rpc import java.util.Collections.singletonList import java.util.Optional @@ -29,7 +29,7 @@ open class DefaultPubsubMessage(override val protobufMessage: Rpc.Message) : Abs override val messageId: MessageId = protobufMessage.from.toWBytes() + protobufMessage.seqno.toWBytes() } -private val logger = LogManager.getLogger(AbstractRouter::class.java) +private val logger = LoggerFactory.getLogger(AbstractRouter::class.java) /** * Implements common logic for pubsub routers diff --git a/libp2p/src/main/kotlin/io/libp2p/pubsub/gossip/GossipRouter.kt b/libp2p/src/main/kotlin/io/libp2p/pubsub/gossip/GossipRouter.kt index 0afc3eb24..2d3b21625 100644 --- a/libp2p/src/main/kotlin/io/libp2p/pubsub/gossip/GossipRouter.kt +++ b/libp2p/src/main/kotlin/io/libp2p/pubsub/gossip/GossipRouter.kt @@ -7,7 +7,7 @@ import io.libp2p.core.pubsub.ValidationResult import io.libp2p.etc.types.* import io.libp2p.etc.util.P2PService import io.libp2p.pubsub.* -import org.apache.logging.log4j.LogManager +import org.slf4j.LoggerFactory import pubsub.pb.Rpc import java.time.Duration import java.util.* @@ -71,7 +71,7 @@ fun P2PService.PeerHandler.getPeerProtocol(): PubsubProtocol { return PubsubProtocol.fromProtocol(proto) } -private val logger = LogManager.getLogger(GossipRouter::class.java) +private val logger = LoggerFactory.getLogger(GossipRouter::class.java) /** * Router implementing this protocol: https://github.com/libp2p/specs/tree/master/pubsub/gossipsub diff --git a/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXCodec.kt b/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXCodec.kt index 0ccd56305..548d1bce1 100644 --- a/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXCodec.kt +++ b/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXCodec.kt @@ -9,11 +9,11 @@ import io.netty.buffer.ByteBuf import io.netty.buffer.Unpooled import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.MessageToMessageCodec -import org.apache.logging.log4j.LogManager +import org.slf4j.LoggerFactory import java.io.IOException import java.security.GeneralSecurityException -private val logger = LogManager.getLogger(NoiseXXSecureChannel::class.java.name) +private val logger = LoggerFactory.getLogger(NoiseXXSecureChannel::class.java.name) class NoiseXXCodec(val aliceCipher: CipherState, val bobCipher: CipherState) : MessageToMessageCodec() { diff --git a/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXSecureChannel.kt b/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXSecureChannel.kt index 40bff231b..c894a366c 100644 --- a/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXSecureChannel.kt +++ b/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXSecureChannel.kt @@ -27,13 +27,13 @@ import io.netty.channel.SimpleChannelInboundHandler import io.netty.handler.codec.LengthFieldBasedFrameDecoder import io.netty.handler.codec.LengthFieldPrepender import io.netty.handler.timeout.ReadTimeoutHandler -import org.apache.logging.log4j.LogManager +import org.slf4j.LoggerFactory import spipe.pb.Spipe import java.util.concurrent.CompletableFuture enum class Role(val intVal: Int) { INIT(HandshakeState.INITIATOR), RESP(HandshakeState.RESPONDER) } -private val log = LogManager.getLogger(NoiseXXSecureChannel::class.java) +private val log = LoggerFactory.getLogger(NoiseXXSecureChannel::class.java) const val HandshakeNettyHandlerName = "HandshakeNettyHandler" const val HandshakeReadTimeoutNettyHandlerName = "HandshakeReadTimeoutNettyHandler" const val NoiseCodeNettyHandlerName = "NoiseXXCodec" diff --git a/libp2p/src/main/kotlin/io/libp2p/security/secio/SecIoCodec.kt b/libp2p/src/main/kotlin/io/libp2p/security/secio/SecIoCodec.kt index fb97ba04e..dfdbbb3d5 100644 --- a/libp2p/src/main/kotlin/io/libp2p/security/secio/SecIoCodec.kt +++ b/libp2p/src/main/kotlin/io/libp2p/security/secio/SecIoCodec.kt @@ -10,16 +10,16 @@ import io.netty.buffer.ByteBuf import io.netty.buffer.Unpooled import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.MessageToMessageCodec -import org.apache.logging.log4j.LogManager import org.bouncycastle.crypto.StreamCipher import org.bouncycastle.crypto.engines.AESEngine import org.bouncycastle.crypto.modes.SICBlockCipher import org.bouncycastle.crypto.params.KeyParameter import org.bouncycastle.crypto.params.ParametersWithIV +import org.slf4j.LoggerFactory import java.io.IOException class SecIoCodec(val local: SecioParams, val remote: SecioParams) : MessageToMessageCodec() { - private val log = LogManager.getLogger(SecIoCodec::class.java) + private val log = LoggerFactory.getLogger(SecIoCodec::class.java) private val localCipher = createCipher(local) private val remoteCipher = createCipher(remote) diff --git a/libp2p/src/main/kotlin/io/libp2p/security/secio/SecIoSecureChannel.kt b/libp2p/src/main/kotlin/io/libp2p/security/secio/SecIoSecureChannel.kt index 27157648b..c0a026d54 100644 --- a/libp2p/src/main/kotlin/io/libp2p/security/secio/SecIoSecureChannel.kt +++ b/libp2p/src/main/kotlin/io/libp2p/security/secio/SecIoSecureChannel.kt @@ -12,10 +12,10 @@ import io.netty.channel.ChannelHandlerContext import io.netty.channel.SimpleChannelInboundHandler import io.netty.handler.codec.LengthFieldBasedFrameDecoder import io.netty.handler.codec.LengthFieldPrepender -import org.apache.logging.log4j.LogManager +import org.slf4j.LoggerFactory import java.util.concurrent.CompletableFuture -private val log = LogManager.getLogger(SecIoSecureChannel::class.java) +private val log = LoggerFactory.getLogger(SecIoSecureChannel::class.java) private val HandshakeHandlerName = "SecIoHandshake" class SecIoSecureChannel(private val localKey: PrivKey) : SecureChannel { diff --git a/libp2p/src/test/kotlin/io/libp2p/security/SecureChannelTestBase.kt b/libp2p/src/test/kotlin/io/libp2p/security/SecureChannelTestBase.kt index 7c6f9fa06..cf69d61fb 100644 --- a/libp2p/src/test/kotlin/io/libp2p/security/SecureChannelTestBase.kt +++ b/libp2p/src/test/kotlin/io/libp2p/security/SecureChannelTestBase.kt @@ -18,17 +18,17 @@ import io.netty.buffer.ByteBuf import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelInboundHandlerAdapter import io.netty.util.ResourceLeakDetector -import org.apache.logging.log4j.LogManager import org.assertj.core.api.Assertions.fail import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource +import org.slf4j.LoggerFactory import java.nio.charset.StandardCharsets import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit typealias SecureChannelCtor = (PrivKey) -> SecureChannel -val logger = LogManager.getLogger(SecureChannelTestBase::class.java) +val logger = LoggerFactory.getLogger(SecureChannelTestBase::class.java) abstract class SecureChannelTestBase( val secureChannelCtor: SecureChannelCtor, diff --git a/libp2p/src/test/kotlin/io/libp2p/security/secio/EchoSampleTest.kt b/libp2p/src/test/kotlin/io/libp2p/security/secio/EchoSampleTest.kt index 1d885e21c..595926ad4 100644 --- a/libp2p/src/test/kotlin/io/libp2p/security/secio/EchoSampleTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/security/secio/EchoSampleTest.kt @@ -21,10 +21,10 @@ import io.netty.channel.ChannelHandler import io.netty.channel.ChannelHandlerContext import io.netty.handler.logging.LogLevel import io.netty.handler.logging.LoggingHandler -import org.apache.logging.log4j.LogManager import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test +import org.slf4j.LoggerFactory import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit @@ -51,7 +51,7 @@ class EchoSampleTest { @Test @Disabled fun connect1() { - val logger = LogManager.getLogger("test") + val logger = LoggerFactory.getLogger("test") val (privKey1, _) = generateKeyPair(KEY_TYPE.ECDSA) val applicationProtocols = listOf(createSimpleBinding("/echo/1.0.0") { EchoProtocol() }) diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/TestChannel.kt b/libp2p/src/test/kotlin/io/libp2p/tools/TestChannel.kt index ab00151fb..74ba9f44f 100644 --- a/libp2p/src/test/kotlin/io/libp2p/tools/TestChannel.kt +++ b/libp2p/src/test/kotlin/io/libp2p/tools/TestChannel.kt @@ -10,7 +10,7 @@ import io.libp2p.transport.implementation.ConnectionOverNetty import io.netty.channel.ChannelHandler import io.netty.channel.ChannelId import io.netty.channel.embedded.EmbeddedChannel -import org.apache.logging.log4j.LogManager +import org.slf4j.LoggerFactory import java.net.InetSocketAddress import java.net.SocketAddress import java.util.concurrent.CompletableFuture @@ -101,7 +101,7 @@ class TestChannel( return TestConnection(ch1, ch2) } - private val logger = LogManager.getLogger(TestChannel::class.java) + private val logger = LoggerFactory.getLogger(TestChannel::class.java) } class TestConnection(val ch1: TestChannel, val ch2: TestChannel) { diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/TestHandler.kt b/libp2p/src/test/kotlin/io/libp2p/tools/TestHandler.kt index 28edfd85d..7fcf6b333 100644 --- a/libp2p/src/test/kotlin/io/libp2p/tools/TestHandler.kt +++ b/libp2p/src/test/kotlin/io/libp2p/tools/TestHandler.kt @@ -5,7 +5,7 @@ import io.libp2p.etc.types.toHex import io.netty.buffer.ByteBuf import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelInboundHandlerAdapter -import org.apache.logging.log4j.LogManager +import org.slf4j.LoggerFactory open class TestHandler(val name: String = "") : ChannelInboundHandlerAdapter() { override fun channelActive(ctx: ChannelHandlerContext) { @@ -49,6 +49,6 @@ open class TestHandler(val name: String = "") : ChannelInboundHandlerAdapter() { } companion object { - private val logger = LogManager.getLogger(TestHandler::class.java) + private val logger = LoggerFactory.getLogger(TestHandler::class.java) } } diff --git a/libp2p/src/test/kotlin/io/libp2p/transport/TransportTests.kt b/libp2p/src/test/kotlin/io/libp2p/transport/TransportTests.kt index 2fff556d5..7e0975434 100644 --- a/libp2p/src/test/kotlin/io/libp2p/transport/TransportTests.kt +++ b/libp2p/src/test/kotlin/io/libp2p/transport/TransportTests.kt @@ -5,7 +5,6 @@ import io.libp2p.core.ConnectionHandler import io.libp2p.core.Libp2pException import io.libp2p.core.multiformats.Multiaddr import io.libp2p.core.transport.Transport -import org.apache.logging.log4j.LogManager import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows @@ -13,6 +12,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable +import org.slf4j.LoggerFactory import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit.SECONDS import java.util.concurrent.atomic.AtomicBoolean @@ -25,7 +25,7 @@ abstract class TransportTests { protected lateinit var transportUnderTest: Transport protected val nullConnHandler = ConnectionHandler { } - protected val logger = LogManager.getLogger("test") + protected val logger = LoggerFactory.getLogger("test") protected fun startListeners(server: Transport, startPortNumber: Int, howMany: Int) { val listening = (0 until howMany).map { diff --git a/tools/schedulers/src/main/java/io/libp2p/tools/schedulers/DefaultSchedulers.java b/tools/schedulers/src/main/java/io/libp2p/tools/schedulers/DefaultSchedulers.java index f3cad73bf..837012929 100644 --- a/tools/schedulers/src/main/java/io/libp2p/tools/schedulers/DefaultSchedulers.java +++ b/tools/schedulers/src/main/java/io/libp2p/tools/schedulers/DefaultSchedulers.java @@ -1,8 +1,8 @@ package io.libp2p.tools.schedulers; import com.google.common.util.concurrent.ThreadFactoryBuilder; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -11,7 +11,7 @@ public class DefaultSchedulers extends AbstractSchedulers { - private static final Logger logger = LogManager.getLogger(DefaultSchedulers.class); + private static final Logger logger = LoggerFactory.getLogger(DefaultSchedulers.class); private Consumer errorHandler = t -> logger.error("Unhandled exception:", t); private volatile boolean started; diff --git a/tools/schedulers/src/main/java/io/libp2p/tools/schedulers/LoggerMDCExecutor.java b/tools/schedulers/src/main/java/io/libp2p/tools/schedulers/LoggerMDCExecutor.java index f35747a32..2bd55b4c8 100644 --- a/tools/schedulers/src/main/java/io/libp2p/tools/schedulers/LoggerMDCExecutor.java +++ b/tools/schedulers/src/main/java/io/libp2p/tools/schedulers/LoggerMDCExecutor.java @@ -1,6 +1,6 @@ package io.libp2p.tools.schedulers; -import org.apache.logging.log4j.ThreadContext; +import org.slf4j.MDC; import java.util.ArrayList; import java.util.List; @@ -31,15 +31,15 @@ public LoggerMDCExecutor add(String mdcKey, Supplier mdcValueSupplier) { public void execute(Runnable command) { List oldValues = new ArrayList<>(mdcKeys.size()); for (int i = 0; i < mdcKeys.size(); i++) { - oldValues.add(ThreadContext.get(mdcKeys.get(i))); - ThreadContext.put(mdcKeys.get(i), mdcValueSuppliers.get(i).get()); + oldValues.add(MDC.get(mdcKeys.get(i))); + MDC.put(mdcKeys.get(i), mdcValueSuppliers.get(i).get()); } delegateExecutor.execute(command); for (int i = 0; i < mdcKeys.size(); i++) { if (oldValues.get(i) == null) { - ThreadContext.remove(mdcKeys.get(i)); + MDC.remove(mdcKeys.get(i)); } else { - ThreadContext.put(mdcKeys.get(i), oldValues.get(i)); + MDC.put(mdcKeys.get(i), oldValues.get(i)); } } } diff --git a/tools/simulator/build.gradle b/tools/simulator/build.gradle index 4798a1dbf..bab7ad655 100644 --- a/tools/simulator/build.gradle +++ b/tools/simulator/build.gradle @@ -11,8 +11,7 @@ dependencies { api("org.apache.commons:commons-math3:3.6.1") implementation("org.jetbrains.kotlin:kotlin-reflect:1.3.0") - runtimeOnly("org.apache.logging.log4j:log4j-core") - runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl") + runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl") } diff --git a/versions.gradle b/versions.gradle index cdf96f735..0305071bb 100644 --- a/versions.gradle +++ b/versions.gradle @@ -3,10 +3,10 @@ dependencyManagement { dependency "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" dependency "com.google.guava:guava:31.1-jre" - dependencySet(group: 'org.apache.logging.log4j', version: '2.19.0') { - entry 'log4j-api' + + dependency "org.slf4j:slf4j-api:2.0.7" + dependencySet(group: 'org.apache.logging.log4j', version: '2.20.0') { entry 'log4j-core' - entry 'log4j-slf4j-impl' entry 'log4j-slf4j2-impl' } From f42740f0d977e9b1c5e5979e81ed63edcbbca41b Mon Sep 17 00:00:00 2001 From: kevodwyer Date: Thu, 18 May 2023 12:36:53 +0100 Subject: [PATCH 05/16] Support webtransport multiaddrs (#280) * add multiaddr dependency from jitpack * Support webtransport multiaddrs * Implement dns multiaddr * Support quic-v1 and wss multiaddr --------- Co-authored-by: ian --- build.gradle.kts | 1 + .../io/libp2p/core/multiformats/Protocol.kt | 19 ++++++++++++++++--- .../libp2p/core/multiformats/MultiaddrTest.kt | 3 ++- settings.gradle | 1 + 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index dfe2d0e01..1c4fae0ce 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -57,6 +57,7 @@ configure( implementation("com.google.guava:guava") implementation("org.slf4j:slf4j-api") + implementation("com.github.multiformats:java-multibase:v1.1.1") testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.jupiter:junit-jupiter-params") diff --git a/libp2p/src/main/kotlin/io/libp2p/core/multiformats/Protocol.kt b/libp2p/src/main/kotlin/io/libp2p/core/multiformats/Protocol.kt index 3a6a513f2..f097591a1 100644 --- a/libp2p/src/main/kotlin/io/libp2p/core/multiformats/Protocol.kt +++ b/libp2p/src/main/kotlin/io/libp2p/core/multiformats/Protocol.kt @@ -2,6 +2,8 @@ package io.libp2p.core.multiformats import com.google.common.base.Utf8 import com.google.common.net.InetAddresses +import io.ipfs.multibase.Multibase +import io.ipfs.multibase.binary.Base32 import io.libp2p.core.PeerId import io.libp2p.etc.encode.Base58 import io.libp2p.etc.types.readUvarint @@ -9,7 +11,6 @@ import io.libp2p.etc.types.toByteArray import io.libp2p.etc.types.toByteBuf import io.libp2p.etc.types.writeUvarint import io.netty.buffer.ByteBuf -import org.bouncycastle.util.encoders.Base32 import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress @@ -36,6 +37,7 @@ enum class Protocol( DCCP(33, 16, "dccp", UINT16_PARSER, UINT16_STRINGIFIER), IP6(41, 128, "ip6", IP6_PARSER, IP6_STRINGIFIER), IP6ZONE(42, LENGTH_PREFIXED_VAR_SIZE, "ip6zone", UTF8_PARSER, UTF8_STRINGIFIER, UTF8_VALIDATOR), + DNS(53, LENGTH_PREFIXED_VAR_SIZE, "dns", UTF8_PARSER, UTF8_STRINGIFIER, UTF8_VALIDATOR), DNS4(54, LENGTH_PREFIXED_VAR_SIZE, "dns4", UTF8_PARSER, UTF8_STRINGIFIER, UTF8_VALIDATOR), DNS6(55, LENGTH_PREFIXED_VAR_SIZE, "dns6", UTF8_PARSER, UTF8_STRINGIFIER, UTF8_VALIDATOR), DNSADDR(56, LENGTH_PREFIXED_VAR_SIZE, "dnsaddr", UTF8_PARSER, UTF8_STRINGIFIER, UTF8_VALIDATOR), @@ -48,7 +50,11 @@ enum class Protocol( HTTPS(443, 0, "https"), ONION(444, 96, "onion", ONION_PARSER, ONION_STRINGIFIER), QUIC(460, 0, "quic"), + QUICV1(461, 0, "quic-v1"), + WEBTRANSPORT(465, 0, "webtransport"), + CERTHASH(466, LENGTH_PREFIXED_VAR_SIZE, "certhash", MULTIBASE_PARSER, MULTIBASE_BASE64_STRINGIFIER), WS(477, 0, "ws"), + WSS(478, 0, "wss"), P2PCIRCUIT(290, 0, "p2p-circuit"), HTTP(480, 0, "http"); @@ -180,14 +186,21 @@ private val BASE58_PARSER: (Protocol, String) -> ByteArray = { _, addr -> private val BASE58_STRINGIFIER: (Protocol, ByteArray) -> String = { _, bytes -> Base58.encode(bytes) } +private val MULTIBASE_PARSER: (Protocol, String) -> ByteArray = { _, addr -> + Multibase.decode(addr) +} +private val MULTIBASE_BASE64_STRINGIFIER: (Protocol, ByteArray) -> String = { _, bytes -> + Multibase.encode(Multibase.Base.Base64Url, bytes) +} private val ONION_PARSER: (Protocol, String) -> ByteArray = { _, addr -> val split = addr.split(":") if (split.size != 2) throw IllegalArgumentException("Onion address needs a port: $addr") // onion address without the ".onion" substring if (split[0].length != 16) throw IllegalArgumentException("failed to parse addr: $addr not a Tor onion address.") + val base32 = Base32() val base32Text = split[0].uppercase() - val onionHostBytes = Base32.decode(base32Text) + val onionHostBytes = base32.decode(base32Text) val port = split[1].toInt() if (port > 65535) throw IllegalArgumentException("Port is > 65535: $port") if (port < 1) throw IllegalArgumentException("Port is < 1: $port") @@ -201,5 +214,5 @@ private val ONION_STRINGIFIER: (Protocol, ByteArray) -> String = { _, bytes -> val byteBuf = bytes.toByteBuf() val host = byteBuf.readBytes(10).toByteArray() val port = byteBuf.readUnsignedShort() - Base32.toBase32String(host).lowercase() + ":" + port + String(Base32().encode(host)).lowercase() + ":" + port } diff --git a/libp2p/src/test/kotlin/io/libp2p/core/multiformats/MultiaddrTest.kt b/libp2p/src/test/kotlin/io/libp2p/core/multiformats/MultiaddrTest.kt index a5ef55cea..e1c274aaf 100644 --- a/libp2p/src/test/kotlin/io/libp2p/core/multiformats/MultiaddrTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/core/multiformats/MultiaddrTest.kt @@ -133,7 +133,8 @@ class MultiaddrTest { "/ip4/1.2.3.4/tcp/80/unix/a/b/c/d/e/f", "/ip4/127.0.0.1/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234/unix/stdio", "/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234/unix/stdio", - "/ip4/127.0.0.1/tcp/40001/p2p/16Uiu2HAkuqGKz8D6khfrnJnDrN5VxWWCoLU8Aq4eCFJuyXmfakB5" + "/ip4/127.0.0.1/tcp/40001/p2p/16Uiu2HAkuqGKz8D6khfrnJnDrN5VxWWCoLU8Aq4eCFJuyXmfakB5", + "/ip6/2001:6b0:30:1000:d00e:1dff:fe0b:c764/udp/4001/quic-v1/webtransport/certhash/uEiAEz_3prFf34VZff8XqA1iTdq2Ytp467ErTGr5dRFo60Q/certhash/uEiDyL7yksuIGJsYUvf0AHieLkTux5R5KBk-UsFtA1AG18A" ) @JvmStatic diff --git a/settings.gradle b/settings.gradle index 93790875b..3840b0611 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,6 +10,7 @@ dependencyResolutionManagement { repositories { mavenCentral() maven { url "https://artifacts.consensys.net/public/maven/maven/" } + maven { url "https://jitpack.io" } google() } } From 8971b312451e81824a06b05292831c8e5202f4b0 Mon Sep 17 00:00:00 2001 From: Dr Ian Preston Date: Wed, 24 May 2023 05:56:52 +0100 Subject: [PATCH 06/16] Yamux implementation (#281) * Initial yamux implementation * Implement per stream write buffers and per connection max write buffer size limit. * Make MuxHandler abstract. Move Mplex specific members to MplexHandler. Derive YamuxHandler from it * Refactor MuxHandler tests * Refactor MplexFrame, remove obsolete MuxFrame * Flush buffered writes in yamux on local disconnect --------- Co-authored-by: Anton Nashatyrev --- .../io/libp2p/core/mux/StreamMuxerProtocol.kt | 11 ++ .../src/main/kotlin/io/libp2p/mux/MuxFrame.kt | 23 --- .../main/kotlin/io/libp2p/mux/MuxHandler.kt | 43 ----- .../kotlin/io/libp2p/mux/mplex/MplexFlag.kt | 62 +++++++ .../kotlin/io/libp2p/mux/mplex/MplexFlags.kt | 52 ------ .../kotlin/io/libp2p/mux/mplex/MplexFrame.kt | 19 +- .../io/libp2p/mux/mplex/MplexFrameCodec.kt | 16 +- .../io/libp2p/mux/mplex/MplexHandler.kt | 50 ++++- .../kotlin/io/libp2p/mux/yamux/YamuxFlags.kt | 11 ++ .../kotlin/io/libp2p/mux/yamux/YamuxFrame.kt | 23 +++ .../io/libp2p/mux/yamux/YamuxFrameCodec.kt | 81 ++++++++ .../io/libp2p/mux/yamux/YamuxHandler.kt | 174 ++++++++++++++++++ .../io/libp2p/mux/yamux/YamuxStreamMuxer.kt | 39 ++++ .../kotlin/io/libp2p/mux/yamux/YamuxType.kt | 11 ++ .../test/kotlin/io/libp2p/core/HostTest.kt | 6 +- ...ndlerTest.kt => MuxHandlerAbstractTest.kt} | 61 +++--- .../libp2p/mux/mplex/MplexFrameCodecTest.kt | 42 ++++- .../io/libp2p/mux/mplex/MplexHandlerTest.kt | 32 ++++ .../io/libp2p/mux/yamux/YamuxHandlerTest.kt | 41 +++++ 19 files changed, 612 insertions(+), 185 deletions(-) delete mode 100644 libp2p/src/main/kotlin/io/libp2p/mux/MuxFrame.kt create mode 100644 libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFlag.kt delete mode 100644 libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFlags.kt create mode 100644 libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFlags.kt create mode 100644 libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrame.kt create mode 100644 libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrameCodec.kt create mode 100644 libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt create mode 100644 libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxStreamMuxer.kt create mode 100644 libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxType.kt rename libp2p/src/test/kotlin/io/libp2p/mux/{MultiplexHandlerTest.kt => MuxHandlerAbstractTest.kt} (87%) create mode 100644 libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexHandlerTest.kt create mode 100644 libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt diff --git a/libp2p/src/main/kotlin/io/libp2p/core/mux/StreamMuxerProtocol.kt b/libp2p/src/main/kotlin/io/libp2p/core/mux/StreamMuxerProtocol.kt index 0c961c0cb..879cd60cd 100644 --- a/libp2p/src/main/kotlin/io/libp2p/core/mux/StreamMuxerProtocol.kt +++ b/libp2p/src/main/kotlin/io/libp2p/core/mux/StreamMuxerProtocol.kt @@ -3,6 +3,7 @@ package io.libp2p.core.mux import io.libp2p.core.multistream.MultistreamProtocol import io.libp2p.core.multistream.ProtocolBinding import io.libp2p.mux.mplex.MplexStreamMuxer +import io.libp2p.mux.yamux.YamuxStreamMuxer fun interface StreamMuxerProtocol { @@ -18,5 +19,15 @@ fun interface StreamMuxerProtocol { multistreamProtocol ) } + + @JvmStatic + val Yamux = StreamMuxerProtocol { multistreamProtocol, protocols -> + YamuxStreamMuxer( + multistreamProtocol.createMultistream( + protocols + ).toStreamHandler(), + multistreamProtocol + ) + } } } diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/MuxFrame.kt b/libp2p/src/main/kotlin/io/libp2p/mux/MuxFrame.kt deleted file mode 100644 index 2d14c3f2e..000000000 --- a/libp2p/src/main/kotlin/io/libp2p/mux/MuxFrame.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.libp2p.mux - -import io.libp2p.etc.types.toByteArray -import io.libp2p.etc.types.toHex -import io.libp2p.etc.util.netty.mux.MuxId -import io.netty.buffer.ByteBuf -import io.netty.buffer.DefaultByteBufHolder -import io.netty.buffer.Unpooled - -open class MuxFrame(val id: MuxId, val flag: Flag, val data: ByteBuf? = null) : - DefaultByteBufHolder(data ?: Unpooled.EMPTY_BUFFER) { - - enum class Flag { - OPEN, - DATA, - CLOSE, - RESET - } - - override fun toString(): String { - return "MuxFrame(id=$id, flag=$flag, data=${data?.toByteArray()?.toHex()})" - } -} diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/MuxHandler.kt b/libp2p/src/main/kotlin/io/libp2p/mux/MuxHandler.kt index ce10cd67e..71a56ed6a 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/MuxHandler.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/MuxHandler.kt @@ -9,22 +9,18 @@ import io.libp2p.core.mux.StreamMuxer import io.libp2p.etc.CONNECTION import io.libp2p.etc.STREAM import io.libp2p.etc.types.forward -import io.libp2p.etc.types.sliceMaxSize import io.libp2p.etc.util.netty.mux.AbstractMuxHandler import io.libp2p.etc.util.netty.mux.MuxChannel import io.libp2p.etc.util.netty.mux.MuxChannelInitializer -import io.libp2p.etc.util.netty.mux.MuxId import io.libp2p.transport.implementation.StreamOverNetty import io.netty.buffer.ByteBuf import io.netty.channel.ChannelHandlerContext import java.util.concurrent.CompletableFuture -import java.util.concurrent.atomic.AtomicLong abstract class MuxHandler( private val ready: CompletableFuture?, inboundStreamHandler: StreamHandler<*> ) : AbstractMuxHandler(), StreamMuxer.Session { - private val idGenerator = AtomicLong(0xF) protected abstract val multistreamProtocol: MultistreamProtocol protected abstract val maxFrameDataLength: Int @@ -38,45 +34,6 @@ abstract class MuxHandler( ready?.complete(this) } - override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { - msg as MuxFrame - when (msg.flag) { - MuxFrame.Flag.OPEN -> onRemoteOpen(msg.id) - MuxFrame.Flag.CLOSE -> onRemoteDisconnect(msg.id) - MuxFrame.Flag.RESET -> onRemoteClose(msg.id) - MuxFrame.Flag.DATA -> childRead(msg.id, msg.data!!) - } - } - - override fun onChildWrite(child: MuxChannel, data: ByteBuf) { - val ctx = getChannelHandlerContext() - data.sliceMaxSize(maxFrameDataLength) - .map { frameSliceBuf -> - MuxFrame(child.id, MuxFrame.Flag.DATA, frameSliceBuf) - }.forEach { muxFrame -> - ctx.write(muxFrame) - } - ctx.flush() - } - - override fun onLocalOpen(child: MuxChannel) { - getChannelHandlerContext().writeAndFlush(MuxFrame(child.id, MuxFrame.Flag.OPEN)) - } - - override fun onLocalDisconnect(child: MuxChannel) { - getChannelHandlerContext().writeAndFlush(MuxFrame(child.id, MuxFrame.Flag.CLOSE)) - } - - override fun onLocalClose(child: MuxChannel) { - getChannelHandlerContext().writeAndFlush(MuxFrame(child.id, MuxFrame.Flag.RESET)) - } - - override fun onRemoteCreated(child: MuxChannel) { - } - - override fun generateNextId() = - MuxId(getChannelHandlerContext().channel().id(), idGenerator.incrementAndGet(), true) - private fun createStream(channel: MuxChannel): Stream { val connection = ctx!!.channel().attr(CONNECTION).get() val stream = StreamOverNetty(channel, connection, channel.initiator) diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFlag.kt b/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFlag.kt new file mode 100644 index 000000000..6cc4c685b --- /dev/null +++ b/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFlag.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2019 BLK Technologies Limited (web3labs.com). + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package io.libp2p.mux.mplex + +import io.libp2p.mux.mplex.MplexFlag.Type.* + +/** + * Contains all the permissible values for flags in the mplex protocol. + */ +enum class MplexFlag( + val value: Int, + val type: Type +) { + NewStream(0, OPEN), + MessageReceiver(1, DATA), + MessageInitiator(2, DATA), + CloseReceiver(3, CLOSE), + CloseInitiator(4, CLOSE), + ResetReceiver(5, RESET), + ResetInitiator(6, RESET); + + enum class Type { + OPEN, + DATA, + CLOSE, + RESET + } + + val isInitiator get() = value % 2 == 0 + + private val initiatorString get() = when (isInitiator) { + true -> "init" + false -> "resp" + } + + override fun toString(): String = "$type($initiatorString)" + + companion object { + private val valueToFlag = MplexFlag.values().associateBy { it.value } + + fun getByValue(flagValue: Int): MplexFlag = + valueToFlag[flagValue] ?: throw IllegalArgumentException("Invalid Mplex stream tag: $flagValue") + + fun getByType(type: Type, initiator: Boolean): MplexFlag = + when (type) { + OPEN -> NewStream + DATA -> if (initiator) MessageInitiator else MessageReceiver + CLOSE -> if (initiator) CloseInitiator else CloseReceiver + RESET -> if (initiator) ResetInitiator else ResetReceiver + } + } +} diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFlags.kt b/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFlags.kt deleted file mode 100644 index b42431260..000000000 --- a/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFlags.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2019 BLK Technologies Limited (web3labs.com). - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ -package io.libp2p.mux.mplex - -import io.libp2p.core.Libp2pException -import io.libp2p.mux.MuxFrame -import io.libp2p.mux.MuxFrame.Flag.CLOSE -import io.libp2p.mux.MuxFrame.Flag.DATA -import io.libp2p.mux.MuxFrame.Flag.OPEN -import io.libp2p.mux.MuxFrame.Flag.RESET - -/** - * Contains all the permissible values for flags in the mplex protocol. - */ -object MplexFlags { - const val NewStream = 0 - const val MessageReceiver = 1 - const val MessageInitiator = 2 - const val CloseReceiver = 3 - const val CloseInitiator = 4 - const val ResetReceiver = 5 - const val ResetInitiator = 6 - - fun isInitiator(mplexFlag: Int) = mplexFlag % 2 == 0 - - fun toAbstractFlag(mplexFlag: Int): MuxFrame.Flag = - when (mplexFlag) { - NewStream -> OPEN - MessageReceiver, MessageInitiator -> DATA - CloseReceiver, CloseInitiator -> CLOSE - ResetReceiver, ResetInitiator -> RESET - else -> throw Libp2pException("Unknown mplex flag: $mplexFlag") - } - - fun toMplexFlag(abstractFlag: MuxFrame.Flag, initiator: Boolean): Int = - when (abstractFlag) { - OPEN -> NewStream - DATA -> if (initiator) MessageInitiator else MessageReceiver - CLOSE -> if (initiator) CloseInitiator else CloseReceiver - RESET -> if (initiator) ResetInitiator else ResetReceiver - } -} diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFrame.kt b/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFrame.kt index b38b52f0c..13868402d 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFrame.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFrame.kt @@ -12,11 +12,9 @@ */ package io.libp2p.mux.mplex -import io.libp2p.etc.types.toByteArray -import io.libp2p.etc.types.toHex import io.libp2p.etc.util.netty.mux.MuxId -import io.libp2p.mux.MuxFrame import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled /** * Contains the fields that comprise an mplex frame. @@ -26,11 +24,16 @@ import io.netty.buffer.ByteBuf * @param data the data segment. * @see [mplex documentation](https://github.com/libp2p/specs/tree/master/mplex#opening-a-new-stream) */ -class MplexFrame(channelId: MuxId, val mplexFlag: Int, data: ByteBuf? = null) : - MuxFrame(channelId, MplexFlags.toAbstractFlag(mplexFlag), data) { +data class MplexFrame(val id: MuxId, val flag: MplexFlag, val data: ByteBuf) { - override fun toString(): String { - val init = if (MplexFlags.isInitiator(mplexFlag)) "init" else "resp" - return "MplexFrame(id=$id, flag=$flag ($init), data=${data?.toByteArray()?.toHex()})" + companion object { + fun createDataFrame(id: MuxId, data: ByteBuf) = + MplexFrame(id, MplexFlag.getByType(MplexFlag.Type.DATA, id.initiator), data) + fun createOpenFrame(id: MuxId) = + MplexFrame(id, MplexFlag.getByType(MplexFlag.Type.OPEN, id.initiator), Unpooled.EMPTY_BUFFER) + fun createCloseFrame(id: MuxId) = + MplexFrame(id, MplexFlag.getByType(MplexFlag.Type.CLOSE, id.initiator), Unpooled.EMPTY_BUFFER) + fun createResetFrame(id: MuxId) = + MplexFrame(id, MplexFlag.getByType(MplexFlag.Type.RESET, id.initiator), Unpooled.EMPTY_BUFFER) } } diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFrameCodec.kt b/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFrameCodec.kt index a31658e38..9abe21ed8 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFrameCodec.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFrameCodec.kt @@ -16,9 +16,7 @@ import io.libp2p.core.ProtocolViolationException import io.libp2p.etc.types.readUvarint import io.libp2p.etc.types.writeUvarint import io.libp2p.etc.util.netty.mux.MuxId -import io.libp2p.mux.MuxFrame import io.netty.buffer.ByteBuf -import io.netty.buffer.Unpooled import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.ByteToMessageCodec @@ -29,7 +27,7 @@ const val DEFAULT_MAX_MPLEX_FRAME_DATA_LENGTH = 1 shl 20 */ class MplexFrameCodec( val maxFrameDataLength: Int = DEFAULT_MAX_MPLEX_FRAME_DATA_LENGTH -) : ByteToMessageCodec() { +) : ByteToMessageCodec() { /** * Encodes the given mplex frame into bytes and writes them into the output list. @@ -38,10 +36,10 @@ class MplexFrameCodec( * @param msg the mplex frame. * @param out the list to write the bytes to. */ - override fun encode(ctx: ChannelHandlerContext, msg: MuxFrame, out: ByteBuf) { - out.writeUvarint(msg.id.id.shl(3).or(MplexFlags.toMplexFlag(msg.flag, msg.id.initiator).toLong())) - out.writeUvarint(msg.data?.readableBytes() ?: 0) - out.writeBytes(msg.data ?: Unpooled.EMPTY_BUFFER) + override fun encode(ctx: ChannelHandlerContext, msg: MplexFrame, out: ByteBuf) { + out.writeUvarint(msg.id.id.shl(3).or(msg.flag.value.toLong())) + out.writeUvarint(msg.data.readableBytes()) + out.writeBytes(msg.data) } /** @@ -76,8 +74,8 @@ class MplexFrameCodec( val streamId = header.shr(3) val data = msg.readSlice(lenData.toInt()) data.retain() // MessageToMessageCodec releases original buffer, but it needs to be relayed - val initiator = if (streamTag == MplexFlags.NewStream) false else !MplexFlags.isInitiator(streamTag) - val mplexFrame = MplexFrame(MuxId(ctx.channel().id(), streamId, initiator), streamTag, data) + val flag = MplexFlag.getByValue(streamTag) + val mplexFrame = MplexFrame(MuxId(ctx.channel().id(), streamId, !flag.isInitiator), flag, data) out.add(mplexFrame) } } diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexHandler.kt b/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexHandler.kt index 0cff7ffee..f886b3247 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexHandler.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexHandler.kt @@ -3,12 +3,60 @@ package io.libp2p.mux.mplex import io.libp2p.core.StreamHandler import io.libp2p.core.multistream.MultistreamProtocol import io.libp2p.core.mux.StreamMuxer +import io.libp2p.etc.types.sliceMaxSize +import io.libp2p.etc.util.netty.mux.MuxChannel +import io.libp2p.etc.util.netty.mux.MuxId import io.libp2p.mux.MuxHandler +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicLong open class MplexHandler( override val multistreamProtocol: MultistreamProtocol, override val maxFrameDataLength: Int, ready: CompletableFuture?, inboundStreamHandler: StreamHandler<*> -) : MuxHandler(ready, inboundStreamHandler) +) : MuxHandler(ready, inboundStreamHandler) { + + private val idGenerator = AtomicLong(0xF) + + override fun generateNextId() = + MuxId(getChannelHandlerContext().channel().id(), idGenerator.incrementAndGet(), true) + + override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { + msg as MplexFrame + when (msg.flag.type) { + MplexFlag.Type.OPEN -> onRemoteOpen(msg.id) + MplexFlag.Type.CLOSE -> onRemoteDisconnect(msg.id) + MplexFlag.Type.RESET -> onRemoteClose(msg.id) + MplexFlag.Type.DATA -> childRead(msg.id, msg.data) + } + } + + override fun onChildWrite(child: MuxChannel, data: ByteBuf) { + val ctx = getChannelHandlerContext() + data.sliceMaxSize(maxFrameDataLength) + .map { frameSliceBuf -> + MplexFrame.createDataFrame(child.id, frameSliceBuf) + }.forEach { muxFrame -> + ctx.write(muxFrame) + } + ctx.flush() + } + + override fun onLocalOpen(child: MuxChannel) { + getChannelHandlerContext().writeAndFlush(MplexFrame.createOpenFrame(child.id)) + } + + override fun onLocalDisconnect(child: MuxChannel) { + getChannelHandlerContext().writeAndFlush(MplexFrame.createCloseFrame(child.id)) + } + + override fun onLocalClose(child: MuxChannel) { + getChannelHandlerContext().writeAndFlush(MplexFrame.createResetFrame(child.id)) + } + + override fun onRemoteCreated(child: MuxChannel) { + } +} diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFlags.kt b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFlags.kt new file mode 100644 index 000000000..85499d0dd --- /dev/null +++ b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFlags.kt @@ -0,0 +1,11 @@ +package io.libp2p.mux.yamux + +/** + * Contains all the permissible values for flags in the yamux protocol. + */ +object YamuxFlags { + const val SYN = 1 + const val ACK = 2 + const val FIN = 4 + const val RST = 8 +} diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrame.kt b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrame.kt new file mode 100644 index 000000000..fefdf1aee --- /dev/null +++ b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrame.kt @@ -0,0 +1,23 @@ +package io.libp2p.mux.yamux + +import io.libp2p.etc.types.toByteArray +import io.libp2p.etc.util.netty.mux.MuxId +import io.netty.buffer.ByteBuf +import io.netty.buffer.DefaultByteBufHolder +import io.netty.buffer.Unpooled + +/** + * Contains the fields that comprise a yamux frame. + * @param streamId the ID of the stream. + * @param flag the flag value for this frame. + * @param data the data segment. + */ +class YamuxFrame(val id: MuxId, val type: Int, val flags: Int, val lenData: Long, val data: ByteBuf? = null) : + DefaultByteBufHolder(data ?: Unpooled.EMPTY_BUFFER) { + + override fun toString(): String { + if (data == null) + return "YamuxFrame(id=$id, type=$type, flag=$flags)" + return "YamuxFrame(id=$id, type=$type, flag=$flags, data=${String(data.toByteArray())})" + } +} diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrameCodec.kt b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrameCodec.kt new file mode 100644 index 000000000..d21fb2d4f --- /dev/null +++ b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrameCodec.kt @@ -0,0 +1,81 @@ +package io.libp2p.mux.yamux + +import io.libp2p.core.ProtocolViolationException +import io.libp2p.etc.util.netty.mux.MuxId +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.ByteToMessageCodec + +const val DEFAULT_MAX_YAMUX_FRAME_DATA_LENGTH = 1 shl 20 + +/** + * A Netty codec implementation that converts [YamuxFrame] instances to [ByteBuf] and vice-versa. + */ +class YamuxFrameCodec( + val isInitiator: Boolean, + val maxFrameDataLength: Int = DEFAULT_MAX_YAMUX_FRAME_DATA_LENGTH +) : ByteToMessageCodec() { + + /** + * Encodes the given yamux frame into bytes and writes them into the output list. + * @see [https://github.com/hashicorp/yamux/blob/master/spec.md] + * @param ctx the context. + * @param msg the yamux frame. + * @param out the list to write the bytes to. + */ + override fun encode(ctx: ChannelHandlerContext, msg: YamuxFrame, out: ByteBuf) { + out.writeByte(0) // version + out.writeByte(msg.type) + out.writeShort(msg.flags) + out.writeInt(msg.id.id.toInt()) + out.writeInt(msg.data?.readableBytes() ?: msg.lenData.toInt()) + out.writeBytes(msg.data ?: Unpooled.EMPTY_BUFFER) + } + + /** + * Decodes the bytes in the given byte buffer and constructs a [YamuxFrame] that is written into + * the output list. + * @param ctx the context. + * @param msg the byte buffer. + * @param out the list to write the extracted frame to. + */ + override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList) { + while (msg.isReadable) { + if (msg.readableBytes() < 12) + return + val readerIndex = msg.readerIndex() + msg.readByte(); // version always 0 + val type = msg.readUnsignedByte() + val flags = msg.readUnsignedShort() + val streamId = msg.readUnsignedInt() + val lenData = msg.readUnsignedInt() + if (type.toInt() != YamuxType.DATA) { + val yamuxFrame = YamuxFrame(MuxId(ctx.channel().id(), streamId, isInitiator.xor(streamId.mod(2).equals(1)).not()), type.toInt(), flags, lenData) + out.add(yamuxFrame) + continue + } + if (lenData > maxFrameDataLength) { + msg.skipBytes(msg.readableBytes()) + throw ProtocolViolationException("Yamux frame is too large: $lenData") + } + if (msg.readableBytes() < lenData) { + // not enough data to read the frame content + // will wait for more ... + msg.readerIndex(readerIndex) + return + } + val data = msg.readSlice(lenData.toInt()) + data.retain() // MessageToMessageCodec releases original buffer, but it needs to be relayed + val yamuxFrame = YamuxFrame(MuxId(ctx.channel().id(), streamId, isInitiator.xor(streamId.mod(2).equals(1)).not()), type.toInt(), flags, lenData, data) + out.add(yamuxFrame) + } + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + // notify higher level handlers on the error + ctx.fireExceptionCaught(cause) + // exceptions in [decode] are very likely unrecoverable so just close the connection + ctx.close() + } +} diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt new file mode 100644 index 000000000..645c54c78 --- /dev/null +++ b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt @@ -0,0 +1,174 @@ +package io.libp2p.mux.yamux + +import io.libp2p.core.Libp2pException +import io.libp2p.core.StreamHandler +import io.libp2p.core.multistream.MultistreamProtocol +import io.libp2p.core.mux.StreamMuxer +import io.libp2p.etc.types.sliceMaxSize +import io.libp2p.etc.util.netty.mux.MuxChannel +import io.libp2p.etc.util.netty.mux.MuxId +import io.libp2p.mux.MuxHandler +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger + +const val INITIAL_WINDOW_SIZE = 256 * 1024 +const val MAX_BUFFERED_CONNECTION_WRITES = 1024 * 1024 + +open class YamuxHandler( + override val multistreamProtocol: MultistreamProtocol, + override val maxFrameDataLength: Int, + ready: CompletableFuture?, + inboundStreamHandler: StreamHandler<*>, + initiator: Boolean +) : MuxHandler(ready, inboundStreamHandler) { + private val idGenerator = AtomicInteger(if (initiator) 1 else 2) // 0 is reserved + private val receiveWindows = ConcurrentHashMap() + private val sendWindows = ConcurrentHashMap() + private val sendBuffers = ConcurrentHashMap() + private val totalBufferedWrites = AtomicInteger() + + inner class SendBuffer(val ctx: ChannelHandlerContext) { + private val buffered = ArrayDeque() + + fun add(data: ByteBuf) { + buffered.add(data) + } + + fun flush(sendWindow: AtomicInteger, id: MuxId): Int { + var written = 0 + while (! buffered.isEmpty()) { + val buf = buffered.first() + if (buf.readableBytes() + written < sendWindow.get()) { + buffered.removeFirst() + sendBlocks(ctx, buf, sendWindow, id) + written += buf.readableBytes() + } else + break + } + return written + } + } + + override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { + msg as YamuxFrame + when (msg.type) { + YamuxType.DATA -> handleDataRead(msg) + YamuxType.WINDOW_UPDATE -> handleWindowUpdate(msg) + YamuxType.PING -> handlePing(msg) + YamuxType.GO_AWAY -> onRemoteClose(msg.id) + } + } + + fun handlePing(msg: YamuxFrame) { + val ctx = getChannelHandlerContext() + when (msg.flags) { + YamuxFlags.SYN -> ctx.writeAndFlush(YamuxFrame(MuxId(msg.id.parentId, 0, msg.id.initiator), YamuxType.PING, YamuxFlags.ACK, msg.lenData)) + YamuxFlags.ACK -> {} + } + } + + fun handleFlags(msg: YamuxFrame) { + val ctx = getChannelHandlerContext() + if (msg.flags == YamuxFlags.SYN) { + // ACK the new stream + onRemoteOpen(msg.id) + ctx.writeAndFlush(YamuxFrame(msg.id, YamuxType.WINDOW_UPDATE, YamuxFlags.ACK, 0)) + } + if (msg.flags == YamuxFlags.FIN) + onRemoteDisconnect(msg.id) + } + + fun handleDataRead(msg: YamuxFrame) { + val ctx = getChannelHandlerContext() + val size = msg.lenData + handleFlags(msg) + if (size.toInt() == 0) + return + val recWindow = receiveWindows.get(msg.id) + if (recWindow == null) + throw Libp2pException("No receive window for " + msg.id) + val newWindow = recWindow.addAndGet(-size.toInt()) + if (newWindow < INITIAL_WINDOW_SIZE / 2) { + val delta = INITIAL_WINDOW_SIZE / 2 + recWindow.addAndGet(delta) + ctx.write(YamuxFrame(msg.id, YamuxType.WINDOW_UPDATE, 0, delta.toLong())) + ctx.flush() + } + childRead(msg.id, msg.data!!) + } + + fun handleWindowUpdate(msg: YamuxFrame) { + handleFlags(msg) + val size = msg.lenData.toInt() + val sendWindow = sendWindows.get(msg.id) + if (sendWindow == null) + throw Libp2pException("No send window for " + msg.id) + sendWindow.addAndGet(size) + val buffer = sendBuffers.get(msg.id) + if (buffer != null) { + val writtenBytes = buffer.flush(sendWindow, msg.id) + totalBufferedWrites.addAndGet(-writtenBytes) + } + } + + override fun onChildWrite(child: MuxChannel, data: ByteBuf) { + val ctx = getChannelHandlerContext() + + val sendWindow = sendWindows.get(child.id) + if (sendWindow == null) + throw Libp2pException("No send window for " + child.id) + if (sendWindow.get() <= 0) { + // wait until the window is increased to send more data + val buffer = sendBuffers.getOrPut(child.id, { SendBuffer(ctx) }) + buffer.add(data) + if (totalBufferedWrites.addAndGet(data.readableBytes()) > MAX_BUFFERED_CONNECTION_WRITES) + throw Libp2pException("Overflowed send buffer for connection") + return + } + sendBlocks(ctx, data, sendWindow, child.id) + } + + fun sendBlocks(ctx: ChannelHandlerContext, data: ByteBuf, sendWindow: AtomicInteger, id: MuxId) { + data.sliceMaxSize(minOf(maxFrameDataLength, sendWindow.get())) + .map { frameSliceBuf -> + sendWindow.addAndGet(-frameSliceBuf.readableBytes()) + YamuxFrame(id, YamuxType.DATA, 0, frameSliceBuf.readableBytes().toLong(), frameSliceBuf) + }.forEach { muxFrame -> + ctx.write(muxFrame) + } + ctx.flush() + } + + override fun onLocalOpen(child: MuxChannel) { + getChannelHandlerContext().writeAndFlush(YamuxFrame(child.id, YamuxType.DATA, YamuxFlags.SYN, 0)) + receiveWindows.put(child.id, AtomicInteger(INITIAL_WINDOW_SIZE)) + sendWindows.put(child.id, AtomicInteger(INITIAL_WINDOW_SIZE)) + } + + override fun onLocalDisconnect(child: MuxChannel) { + sendWindows.remove(child.id) + receiveWindows.remove(child.id) + sendBuffers.remove(child.id) + getChannelHandlerContext().writeAndFlush(YamuxFrame(child.id, YamuxType.DATA, YamuxFlags.FIN, 0)) + } + + override fun onLocalClose(child: MuxChannel) { + getChannelHandlerContext().writeAndFlush(YamuxFrame(child.id, YamuxType.DATA, YamuxFlags.RST, 0)) + val sendWindow = sendWindows.remove(child.id) + val buffered = sendBuffers.remove(child.id) + if (buffered != null && sendWindow != null) { + buffered.flush(sendWindow, child.id) + } + } + + override fun onRemoteCreated(child: MuxChannel) { + receiveWindows.put(child.id, AtomicInteger(INITIAL_WINDOW_SIZE)) + sendWindows.put(child.id, AtomicInteger(INITIAL_WINDOW_SIZE)) + } + + override fun generateNextId() = + MuxId(getChannelHandlerContext().channel().id(), idGenerator.addAndGet(2).toLong(), true) +} diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxStreamMuxer.kt b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxStreamMuxer.kt new file mode 100644 index 000000000..4b43a0597 --- /dev/null +++ b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxStreamMuxer.kt @@ -0,0 +1,39 @@ +package io.libp2p.mux.yamux + +import io.libp2p.core.ChannelVisitor +import io.libp2p.core.Connection +import io.libp2p.core.P2PChannel +import io.libp2p.core.StreamHandler +import io.libp2p.core.multistream.MultistreamProtocol +import io.libp2p.core.multistream.ProtocolDescriptor +import io.libp2p.core.mux.StreamMuxer +import io.libp2p.core.mux.StreamMuxerDebug +import java.util.concurrent.CompletableFuture + +class YamuxStreamMuxer( + val inboundStreamHandler: StreamHandler<*>, + private val multistreamProtocol: MultistreamProtocol +) : StreamMuxer, StreamMuxerDebug { + + override val protocolDescriptor = ProtocolDescriptor("/yamux/1.0.0") + override var muxFramesDebugHandler: ChannelVisitor? = null + + override fun initChannel(ch: P2PChannel, selectedProtocol: String): CompletableFuture { + val muxSessionReady = CompletableFuture() + + val yamuxFrameCodec = YamuxFrameCodec(ch.isInitiator) + ch.pushHandler(yamuxFrameCodec) + muxFramesDebugHandler?.also { it.visit(ch as Connection) } + ch.pushHandler( + YamuxHandler( + multistreamProtocol, + yamuxFrameCodec.maxFrameDataLength, + muxSessionReady, + inboundStreamHandler, + ch.isInitiator + ) + ) + + return muxSessionReady + } +} diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxType.kt b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxType.kt new file mode 100644 index 000000000..cf66f4b8b --- /dev/null +++ b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxType.kt @@ -0,0 +1,11 @@ +package io.libp2p.mux.yamux + +/** + * Contains all the permissible values for flags in the yamux protocol. + */ +object YamuxType { + const val DATA = 0 + const val WINDOW_UPDATE = 1 + const val PING = 2 + const val GO_AWAY = 3 +} diff --git a/libp2p/src/test/kotlin/io/libp2p/core/HostTest.kt b/libp2p/src/test/kotlin/io/libp2p/core/HostTest.kt index f0ad484d9..eb4513f99 100644 --- a/libp2p/src/test/kotlin/io/libp2p/core/HostTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/core/HostTest.kt @@ -4,7 +4,7 @@ import io.libp2p.core.multistream.ProtocolMatcher import io.libp2p.etc.PROTOCOL import io.libp2p.etc.types.seconds import io.libp2p.etc.types.toByteArray -import io.libp2p.mux.MuxFrame +import io.libp2p.mux.mplex.MplexFrame import io.libp2p.protocol.Ping import io.libp2p.protocol.PingBinding import io.libp2p.protocol.PingProtocol @@ -131,7 +131,7 @@ class HostTest { val afterSecureTestHandler1 = TestByteBufChannelHandler("1-afterSecure") val preStreamTestHandler1 = TestByteBufChannelHandler("1-preStream") val streamTestHandler1 = TestByteBufChannelHandler("1-stream") - val muxFrameTestHandler1 = TestChannelHandler("1-mux") + val muxFrameTestHandler1 = TestChannelHandler("1-mux") hostFactory.hostBuilderModifier = { debug { @@ -148,7 +148,7 @@ class HostTest { val afterSecureTestHandler2 = TestByteBufChannelHandler("2-afterSecure") val preStreamTestHandler2 = TestByteBufChannelHandler("2-preStream") val streamTestHandler2 = TestByteBufChannelHandler("2-stream") - val muxFrameTestHandler2 = TestChannelHandler("2-mux") + val muxFrameTestHandler2 = TestChannelHandler("2-mux") hostFactory.hostBuilderModifier = { debug { diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/MultiplexHandlerTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt similarity index 87% rename from libp2p/src/test/kotlin/io/libp2p/mux/MultiplexHandlerTest.kt rename to libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt index 5a8e18013..b4ff22a37 100644 --- a/libp2p/src/test/kotlin/io/libp2p/mux/MultiplexHandlerTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt @@ -4,19 +4,10 @@ import io.libp2p.core.ConnectionClosedException import io.libp2p.core.Libp2pException import io.libp2p.core.Stream import io.libp2p.core.StreamHandler -import io.libp2p.core.multistream.MultistreamProtocolV1 -import io.libp2p.etc.types.fromHex import io.libp2p.etc.types.getX import io.libp2p.etc.types.toByteArray -import io.libp2p.etc.types.toByteBuf import io.libp2p.etc.types.toHex -import io.libp2p.etc.util.netty.mux.MuxId import io.libp2p.etc.util.netty.nettyInitializer -import io.libp2p.mux.MuxFrame.Flag.DATA -import io.libp2p.mux.MuxFrame.Flag.OPEN -import io.libp2p.mux.MuxFrame.Flag.RESET -import io.libp2p.mux.mplex.DEFAULT_MAX_MPLEX_FRAME_DATA_LENGTH -import io.libp2p.mux.mplex.MplexHandler import io.libp2p.tools.TestChannel import io.netty.buffer.ByteBuf import io.netty.channel.ChannelHandler @@ -37,12 +28,14 @@ import java.util.concurrent.CompletableFuture /** * Created by Anton Nashatyrev on 09.07.2019. */ -class MultiplexHandlerTest { +abstract class MuxHandlerAbstractTest { val dummyParentChannelId = DefaultChannelId.newInstance() val childHandlers = mutableListOf() lateinit var multistreamHandler: MuxHandler lateinit var ech: TestChannel + abstract fun createMuxHandler(streamHandler: StreamHandler): MuxHandler + @BeforeEach fun startMultiplexor() { childHandlers.clear() @@ -54,19 +47,29 @@ class MultiplexHandlerTest { childHandlers += handler } ) - multistreamHandler = object : MplexHandler( - MultistreamProtocolV1, DEFAULT_MAX_MPLEX_FRAME_DATA_LENGTH, null, streamHandler - ) { - // MuxHandler consumes the exception. Override this behaviour for testing - @Deprecated("Deprecated in Java") - override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { - ctx.fireExceptionCaught(cause) - } - } + multistreamHandler = createMuxHandler(streamHandler) ech = TestChannel("test", true, LoggingHandler(LogLevel.ERROR), multistreamHandler) } + abstract fun openStream(id: Long): Boolean + abstract fun writeStream(id: Long, msg: String): Boolean + abstract fun resetStream(id: Long): Boolean + + fun createStreamHandler(channelInitializer: ChannelHandler) = object : StreamHandler { + override fun handleStream(stream: Stream): CompletableFuture { + stream.pushHandler(channelInitializer) + return CompletableFuture.completedFuture(Unit) + } + } + + fun assertHandlerCount(count: Int) = assertEquals(count, childHandlers.size) + fun assertLastMessage(handler: Int, msgCount: Int, msg: String) { + val messages = childHandlers[handler].inboundMessages + assertEquals(msgCount, messages.size) + assertEquals(msg, messages.last()) + } + @Test fun singleStream() { openStream(12) @@ -238,26 +241,6 @@ class MultiplexHandlerTest { assertThrows(ConnectionClosedException::class.java) { staleStream.stream.getX(3.0) } } - fun assertHandlerCount(count: Int) = assertEquals(count, childHandlers.size) - fun assertLastMessage(handler: Int, msgCount: Int, msg: String) { - val messages = childHandlers[handler].inboundMessages - assertEquals(msgCount, messages.size) - assertEquals(msg, messages.last()) - } - - fun openStream(id: Long) = writeFrame(id, OPEN) - fun writeStream(id: Long, msg: String) = writeFrame(id, DATA, msg.fromHex().toByteBuf()) - fun resetStream(id: Long) = writeFrame(id, RESET) - fun writeFrame(id: Long, flag: MuxFrame.Flag, data: ByteBuf? = null) = - ech.writeInbound(MuxFrame(MuxId(dummyParentChannelId, id, true), flag, data)) - - fun createStreamHandler(channelInitializer: ChannelHandler) = object : StreamHandler { - override fun handleStream(stream: Stream): CompletableFuture { - stream.pushHandler(channelInitializer) - return CompletableFuture.completedFuture(Unit) - } - } - class TestHandler : ChannelInboundHandlerAdapter() { val inboundMessages = mutableListOf() var ctx: ChannelHandlerContext? = null diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexFrameCodecTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexFrameCodecTest.kt index 031350c55..8139f61df 100644 --- a/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexFrameCodecTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexFrameCodecTest.kt @@ -36,7 +36,7 @@ class MplexFrameCodecTest { val channelLarge = EmbeddedChannel(MplexFrameCodec(maxFrameDataLength = 1024)) val mplexFrame = MplexFrame( - MuxId(dummyId, 777, true), MplexFlags.MessageInitiator, + MuxId(dummyId, 777, true), MplexFlag.MessageInitiator, ByteArray(1024).toByteBuf() ) @@ -61,9 +61,9 @@ class MplexFrameCodecTest { val channel = EmbeddedChannel(MplexFrameCodec()) val mplexFrames = arrayOf( - MplexFrame(MuxId(dummyId, 777, true), MplexFlags.MessageInitiator, "Hello-1".toByteArray().toByteBuf()), - MplexFrame(MuxId(dummyId, 888, true), MplexFlags.MessageInitiator, "Hello-2".toByteArray().toByteBuf()), - MplexFrame(MuxId(dummyId, 999, true), MplexFlags.MessageInitiator, "Hello-3".toByteArray().toByteBuf()) + MplexFrame(MuxId(dummyId, 777, true), MplexFlag.MessageInitiator, "Hello-1".toByteArray().toByteBuf()), + MplexFrame(MuxId(dummyId, 888, true), MplexFlag.MessageInitiator, "Hello-2".toByteArray().toByteBuf()), + MplexFrame(MuxId(dummyId, 999, true), MplexFlag.MessageInitiator, "Hello-3".toByteArray().toByteBuf()) ) assertTrue( channel.writeOutbound(*mplexFrames) @@ -86,8 +86,36 @@ class MplexFrameCodecTest { assertEquals(777, resultFrames[0].id.id) assertEquals(888, resultFrames[1].id.id) assertEquals(999, resultFrames[2].id.id) - assertEquals("Hello-1", resultFrames[0].data!!.toByteArray().toString(UTF_8)) - assertEquals("Hello-2", resultFrames[1].data!!.toByteArray().toString(UTF_8)) - assertEquals("Hello-3", resultFrames[2].data!!.toByteArray().toString(UTF_8)) + assertEquals("Hello-1", resultFrames[0].data.toByteArray().toString(UTF_8)) + assertEquals("Hello-2", resultFrames[1].data.toByteArray().toString(UTF_8)) + assertEquals("Hello-3", resultFrames[2].data.toByteArray().toString(UTF_8)) + } + + @Test + fun `test id initiator is inverted on decoding`() { + val channel = EmbeddedChannel(MplexFrameCodec()) + + val mplexFrames = arrayOf( + MplexFrame.createOpenFrame(MuxId(dummyId, 1, true)), + MplexFrame.createDataFrame(MuxId(dummyId, 2, true), "Hello-2".toByteArray().toByteBuf()), + MplexFrame.createDataFrame(MuxId(dummyId, 3, false), "Hello-3".toByteArray().toByteBuf()), + MplexFrame.createCloseFrame(MuxId(dummyId, 4, true)), + MplexFrame.createCloseFrame(MuxId(dummyId, 5, false)), + MplexFrame.createResetFrame(MuxId(dummyId, 6, true)), + MplexFrame.createResetFrame(MuxId(dummyId, 7, false)), + ) + assertTrue( + channel.writeOutbound(*mplexFrames) + ) + + repeat(mplexFrames.size) { idx -> + val wireBytes = channel.readOutbound() + channel.writeInbound(wireBytes) + val resFrame = channel.readInbound() + + assertEquals(mplexFrames[idx].id.id, resFrame.id.id) + assertEquals(!mplexFrames[idx].id.initiator, resFrame.id.initiator) + assertEquals(mplexFrames[idx].flag, resFrame.flag) + } } } diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexHandlerTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexHandlerTest.kt new file mode 100644 index 000000000..e64115a57 --- /dev/null +++ b/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexHandlerTest.kt @@ -0,0 +1,32 @@ +package io.libp2p.mux.mplex + +import io.libp2p.core.StreamHandler +import io.libp2p.core.multistream.MultistreamProtocolV1 +import io.libp2p.etc.types.fromHex +import io.libp2p.etc.types.toByteBuf +import io.libp2p.etc.util.netty.mux.MuxId +import io.libp2p.mux.MuxHandler +import io.libp2p.mux.MuxHandlerAbstractTest +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import io.netty.channel.ChannelHandlerContext + +class MplexHandlerTest : MuxHandlerAbstractTest() { + + override fun createMuxHandler(streamHandler: StreamHandler): MuxHandler = + object : MplexHandler( + MultistreamProtocolV1, DEFAULT_MAX_MPLEX_FRAME_DATA_LENGTH, null, streamHandler + ) { + // MuxHandler consumes the exception. Override this behaviour for testing + @Deprecated("Deprecated in Java") + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + ctx.fireExceptionCaught(cause) + } + } + + override fun openStream(id: Long) = writeFrame(id, MplexFlag.Type.OPEN) + override fun writeStream(id: Long, msg: String) = writeFrame(id, MplexFlag.Type.DATA, msg.fromHex().toByteBuf()) + override fun resetStream(id: Long) = writeFrame(id, MplexFlag.Type.RESET) + fun writeFrame(id: Long, flagType: MplexFlag.Type, data: ByteBuf = Unpooled.EMPTY_BUFFER) = + ech.writeInbound(MplexFrame(MuxId(dummyParentChannelId, id, true), MplexFlag.getByType(flagType, true), data)) +} diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt new file mode 100644 index 000000000..69016f56b --- /dev/null +++ b/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt @@ -0,0 +1,41 @@ +package io.libp2p.mux.yamux + +import io.libp2p.core.StreamHandler +import io.libp2p.core.multistream.MultistreamProtocolV1 +import io.libp2p.etc.types.fromHex +import io.libp2p.etc.types.toByteBuf +import io.libp2p.etc.util.netty.mux.MuxId +import io.libp2p.mux.MuxHandler +import io.libp2p.mux.MuxHandlerAbstractTest +import io.netty.channel.ChannelHandlerContext + +class YamuxHandlerTest : MuxHandlerAbstractTest() { + + override fun createMuxHandler(streamHandler: StreamHandler): MuxHandler = + object : YamuxHandler( + MultistreamProtocolV1, DEFAULT_MAX_YAMUX_FRAME_DATA_LENGTH, null, streamHandler, true + ) { + // MuxHandler consumes the exception. Override this behaviour for testing + @Deprecated("Deprecated in Java") + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + ctx.fireExceptionCaught(cause) + } + } + + override fun openStream(id: Long) = + ech.writeInbound(YamuxFrame(MuxId(dummyParentChannelId, id, true), YamuxType.DATA, YamuxFlags.SYN, 0)) + + override fun writeStream(id: Long, msg: String) = + ech.writeInbound( + YamuxFrame( + MuxId(dummyParentChannelId, id, true), + YamuxType.DATA, + 0, + msg.fromHex().size.toLong(), + msg.fromHex().toByteBuf() + ) + ) + + override fun resetStream(id: Long) = + ech.writeInbound(YamuxFrame(MuxId(dummyParentChannelId, id, true), YamuxType.GO_AWAY, 0, 0)) +} From 63869ca88bf259a2e4eba73319d5efc138467ba6 Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Thu, 25 May 2023 07:50:42 +0300 Subject: [PATCH 07/16] Refactor: test fixtures sort. Regression bug fix (#284) * Refactor: move test utility classes to testFixtures module * Fix regression: MplexFrame should implement ReferenceCounted to release the underlying ByteBuf after encoding with MplexFrameCodec * Add unit test reproducing the issue --- build.gradle.kts | 3 ++ libp2p/build.gradle.kts | 5 ++- .../kotlin/io/libp2p/mux/mplex/MplexFrame.kt | 3 +- .../libp2p/mux/mplex/MplexFrameCodecTest.kt | 31 +++++++++++++------ .../io/libp2p/pubsub/DeterministicFuzz.kt | 0 .../kotlin/io/libp2p/pubsub/MockRouter.kt | 0 .../kotlin/io/libp2p/pubsub/TestRouter.kt | 0 .../libp2p/pubsub/gossip/Eth2GossipParams.kt | 0 .../io/libp2p/tools/CountingPingProtocol.kt | 0 .../kotlin/io/libp2p/tools/DnsAvailability.kt | 0 .../io/libp2p/tools/DoNothingProtocol.kt | 0 .../kotlin/io/libp2p/tools/EchoProtocol.kt | 3 +- .../kotlin/io/libp2p/tools/HostFactory.kt | 0 .../kotlin/io/libp2p/tools/NullHost.kt | 0 .../kotlin/io/libp2p/tools/NullTransport.kt | 0 .../kotlin/io/libp2p/tools/P2pdRunner.kt | 0 .../kotlin/io/libp2p/tools/Stubs.kt | 0 .../kotlin/io/libp2p/tools/TCPProxy.kt | 4 --- .../kotlin/io/libp2p/tools/TestChannel.kt | 0 .../kotlin/io/libp2p/tools/TestHandler.kt | 0 .../kotlin/io/libp2p/tools/TestLogAppender.kt | 0 .../io/libp2p/tools/TestStreamChannel.kt | 0 .../io/libp2p/tools/protobuf/ProtobufUtils.kt | 0 .../io/libp2p/tools/protobuf/RpcBuilder.kt | 0 .../transport/NullConnectionUpgrader.kt | 0 25 files changed, 33 insertions(+), 16 deletions(-) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/pubsub/DeterministicFuzz.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/pubsub/MockRouter.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/pubsub/TestRouter.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/pubsub/gossip/Eth2GossipParams.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/CountingPingProtocol.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/DnsAvailability.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/DoNothingProtocol.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/EchoProtocol.kt (97%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/HostFactory.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/NullHost.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/NullTransport.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/P2pdRunner.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/Stubs.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/TCPProxy.kt (97%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/TestChannel.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/TestHandler.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/TestLogAppender.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/TestStreamChannel.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/protobuf/ProtobufUtils.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/tools/protobuf/RpcBuilder.kt (100%) rename libp2p/src/{test => testFixtures}/kotlin/io/libp2p/transport/NullConnectionUpgrader.kt (100%) diff --git a/build.gradle.kts b/build.gradle.kts index 1c4fae0ce..b6e9a7271 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,6 +59,9 @@ configure( implementation("org.slf4j:slf4j-api") implementation("com.github.multiformats:java-multibase:v1.1.1") + testFixturesImplementation("com.google.guava:guava") + testFixturesImplementation("org.slf4j:slf4j-api") + testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.jupiter:junit-jupiter-params") testImplementation("io.mockk:mockk") diff --git a/libp2p/build.gradle.kts b/libp2p/build.gradle.kts index 89c85a2d8..4012cb2d7 100644 --- a/libp2p/build.gradle.kts +++ b/libp2p/build.gradle.kts @@ -19,9 +19,12 @@ dependencies { implementation("org.bouncycastle:bcpkix-jdk15on") testImplementation(project(":tools:schedulers")) - testImplementation("org.apache.logging.log4j:log4j-core") + testFixturesApi("org.apache.logging.log4j:log4j-core") + testFixturesImplementation(project(":tools:schedulers")) testFixturesImplementation("io.netty:netty-transport-classes-epoll") + testFixturesImplementation("io.netty:netty-handler") + testFixturesImplementation("org.junit.jupiter:junit-jupiter-api") jmhImplementation(project(":tools:schedulers")) jmhImplementation("org.openjdk.jmh:jmh-core") diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFrame.kt b/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFrame.kt index 13868402d..fb4e11bc0 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFrame.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexFrame.kt @@ -14,6 +14,7 @@ package io.libp2p.mux.mplex import io.libp2p.etc.util.netty.mux.MuxId import io.netty.buffer.ByteBuf +import io.netty.buffer.DefaultByteBufHolder import io.netty.buffer.Unpooled /** @@ -24,7 +25,7 @@ import io.netty.buffer.Unpooled * @param data the data segment. * @see [mplex documentation](https://github.com/libp2p/specs/tree/master/mplex#opening-a-new-stream) */ -data class MplexFrame(val id: MuxId, val flag: MplexFlag, val data: ByteBuf) { +data class MplexFrame(val id: MuxId, val flag: MplexFlag, val data: ByteBuf) : DefaultByteBufHolder(data) { companion object { fun createDataFrame(id: MuxId, data: ByteBuf) = diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexFrameCodecTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexFrameCodecTest.kt index 8139f61df..6ba816250 100644 --- a/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexFrameCodecTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexFrameCodecTest.kt @@ -30,20 +30,20 @@ class MplexFrameCodecTest { ) } val dummyId = DefaultChannelId.newInstance() + val maxFrameDataLength = 1024 + val channel = EmbeddedChannel(MplexFrameCodec(maxFrameDataLength = maxFrameDataLength)) @Test fun `check max frame size limit`() { - val channelLarge = EmbeddedChannel(MplexFrameCodec(maxFrameDataLength = 1024)) - val mplexFrame = MplexFrame( MuxId(dummyId, 777, true), MplexFlag.MessageInitiator, - ByteArray(1024).toByteBuf() + ByteArray(maxFrameDataLength).toByteBuf() ) assertTrue( - channelLarge.writeOutbound(mplexFrame) + channel.writeOutbound(mplexFrame) ) - val largeFrameBytes = channelLarge.readOutbound() + val largeFrameBytes = channel.readOutbound() val largeFrameBytesTrunc = largeFrameBytes.slice(0, largeFrameBytes.readableBytes() - 1) val channelSmall = EmbeddedChannel(MplexFrameCodec(maxFrameDataLength = 128)) @@ -58,8 +58,6 @@ class MplexFrameCodecTest { @ParameterizedTest @MethodSource("splitIndexes") fun testDecoder(sliceIdx: List) { - val channel = EmbeddedChannel(MplexFrameCodec()) - val mplexFrames = arrayOf( MplexFrame(MuxId(dummyId, 777, true), MplexFlag.MessageInitiator, "Hello-1".toByteArray().toByteBuf()), MplexFrame(MuxId(dummyId, 888, true), MplexFlag.MessageInitiator, "Hello-2".toByteArray().toByteBuf()), @@ -93,8 +91,6 @@ class MplexFrameCodecTest { @Test fun `test id initiator is inverted on decoding`() { - val channel = EmbeddedChannel(MplexFrameCodec()) - val mplexFrames = arrayOf( MplexFrame.createOpenFrame(MuxId(dummyId, 1, true)), MplexFrame.createDataFrame(MuxId(dummyId, 2, true), "Hello-2".toByteArray().toByteBuf()), @@ -118,4 +114,21 @@ class MplexFrameCodecTest { assertEquals(mplexFrames[idx].flag, resFrame.flag) } } + + @Test + fun `check the frame underlying buffer is released after send`() { + val frameDataBuf = "Hello-1".toByteArray().toByteBuf() + + assertTrue(frameDataBuf.refCnt() == 1) + + channel.writeOutbound( + MplexFrame(MuxId(dummyId, 777, true), MplexFlag.MessageInitiator, frameDataBuf) + ) + + val encodedFrame = channel.readOutbound() + // bytes are released after sending to the wire + encodedFrame.release() + + assertTrue(frameDataBuf.refCnt() == 0) + } } diff --git a/libp2p/src/test/kotlin/io/libp2p/pubsub/DeterministicFuzz.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/pubsub/DeterministicFuzz.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/pubsub/DeterministicFuzz.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/pubsub/DeterministicFuzz.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/pubsub/MockRouter.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/pubsub/MockRouter.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/pubsub/MockRouter.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/pubsub/MockRouter.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/pubsub/TestRouter.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/pubsub/TestRouter.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/pubsub/TestRouter.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/pubsub/TestRouter.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/pubsub/gossip/Eth2GossipParams.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/pubsub/gossip/Eth2GossipParams.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/pubsub/gossip/Eth2GossipParams.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/pubsub/gossip/Eth2GossipParams.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/CountingPingProtocol.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/CountingPingProtocol.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/CountingPingProtocol.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/CountingPingProtocol.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/DnsAvailability.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/DnsAvailability.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/DnsAvailability.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/DnsAvailability.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/DoNothingProtocol.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/DoNothingProtocol.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/DoNothingProtocol.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/DoNothingProtocol.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/EchoProtocol.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/EchoProtocol.kt similarity index 97% rename from libp2p/src/test/kotlin/io/libp2p/tools/EchoProtocol.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/EchoProtocol.kt index 4da4b6581..17b14dda4 100644 --- a/libp2p/src/test/kotlin/io/libp2p/tools/EchoProtocol.kt +++ b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/EchoProtocol.kt @@ -44,7 +44,8 @@ open class EchoProtocol : ProtocolHandler(Long.MAX_VALUE, Long.M } } - open inner class EchoInitiator(val ready: CompletableFuture) : ProtocolMessageHandler, EchoController { + open inner class EchoInitiator(val ready: CompletableFuture) : + ProtocolMessageHandler, EchoController { lateinit var stream: Stream var ret = CompletableFuture() diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/HostFactory.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/HostFactory.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/HostFactory.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/HostFactory.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/NullHost.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/NullHost.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/NullHost.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/NullHost.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/NullTransport.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/NullTransport.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/NullTransport.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/NullTransport.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/P2pdRunner.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/P2pdRunner.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/P2pdRunner.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/P2pdRunner.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/Stubs.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/Stubs.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/Stubs.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/Stubs.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/TCPProxy.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/TCPProxy.kt similarity index 97% rename from libp2p/src/test/kotlin/io/libp2p/tools/TCPProxy.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/TCPProxy.kt index 6c3de7390..3f4ad2346 100644 --- a/libp2p/src/test/kotlin/io/libp2p/tools/TCPProxy.kt +++ b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/TCPProxy.kt @@ -12,8 +12,6 @@ import io.netty.channel.socket.nio.NioServerSocketChannel import io.netty.channel.socket.nio.NioSocketChannel import io.netty.handler.logging.LogLevel import io.netty.handler.logging.LoggingHandler -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test import java.util.concurrent.CompletableFuture // Utility class (aka sniffer) that just forwards TCP traffic back'n'forth to another TCP address and log it @@ -67,8 +65,6 @@ class TCPProxy { return future } - @Test - @Disabled fun run() { start(11111, "localhost", 10000) .channel().closeFuture().await() diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/TestChannel.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/TestChannel.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/TestChannel.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/TestChannel.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/TestHandler.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/TestHandler.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/TestHandler.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/TestHandler.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/TestLogAppender.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/TestLogAppender.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/TestLogAppender.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/TestLogAppender.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/TestStreamChannel.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/TestStreamChannel.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/TestStreamChannel.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/TestStreamChannel.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/protobuf/ProtobufUtils.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/protobuf/ProtobufUtils.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/protobuf/ProtobufUtils.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/protobuf/ProtobufUtils.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/tools/protobuf/RpcBuilder.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/protobuf/RpcBuilder.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/tools/protobuf/RpcBuilder.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/tools/protobuf/RpcBuilder.kt diff --git a/libp2p/src/test/kotlin/io/libp2p/transport/NullConnectionUpgrader.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/transport/NullConnectionUpgrader.kt similarity index 100% rename from libp2p/src/test/kotlin/io/libp2p/transport/NullConnectionUpgrader.kt rename to libp2p/src/testFixtures/kotlin/io/libp2p/transport/NullConnectionUpgrader.kt From 0981ec69761a469052aea388b452cb22bf65b9cb Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Tue, 30 May 2023 09:59:38 +0300 Subject: [PATCH 08/16] Improve muxer test coverage. Fix several muxer issues (#285) * Refactor MuxHandlerAbstractTest: add ability to handle outbound messages * Control and check ByteBuf allocations and releases * Add more testcases and assertions * [Generic] (buffer leak) release message buffer if inbound frame have non-existing streamId: 71c68c4 * [Yamux] Process RST (Reset) flag: 42e40f6 * [Generic] Writing to a stream which was prior closed should be prohibited: d3c4580 * [Generic] Receiving a data on a steam which was remotely closed should result in exception (recoverable, i.e. connection should not be terminated): 8974317 * [Yamux] switch the logic of onLocalDisconnect() and onLocalClose() methods. onLocalDisconnect() should leave the stream open for inbound data: 820c252 * [Yamux] need to clean up stream entries on any kind of stream closure (local Reset, remote Reset, local + remote Close): 66ef36b * Convert RemoteWriteClosed to singleton --- .../etc/util/netty/mux/AbstractMuxHandler.kt | 32 +- .../libp2p/etc/util/netty/mux/MuxChannel.kt | 15 +- .../etc/util/netty/mux/RemoteWriteClosed.kt | 6 + .../main/kotlin/io/libp2p/mux/MuxHandler.kt | 4 + .../io/libp2p/mux/mplex/MplexHandler.kt | 3 +- .../io/libp2p/mux/yamux/YamuxHandler.kt | 46 ++- .../io/libp2p/mux/MuxHandlerAbstractTest.kt | 332 +++++++++++++++--- .../io/libp2p/mux/mplex/MplexHandlerTest.kt | 46 ++- .../io/libp2p/mux/yamux/YamuxHandlerTest.kt | 49 ++- .../kotlin/io/libp2p/tools/ByteBufExt.kt | 10 + 10 files changed, 440 insertions(+), 103 deletions(-) create mode 100644 libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/RemoteWriteClosed.kt create mode 100644 libp2p/src/testFixtures/kotlin/io/libp2p/tools/ByteBufExt.kt diff --git a/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt b/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt index 8dec78669..f50c3a088 100644 --- a/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt +++ b/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt @@ -50,13 +50,26 @@ abstract class AbstractMuxHandler() : } fun getChannelHandlerContext(): ChannelHandlerContext { - return ctx ?: throw InternalErrorException("Internal error: handler context should be initialized at this stage") + return ctx + ?: throw InternalErrorException("Internal error: handler context should be initialized at this stage") } protected fun childRead(id: MuxId, msg: TData) { - val child = streamMap[id] ?: throw ConnectionClosedException("Channel with id $id not opened") - pendingReadComplete += id - child.pipeline().fireChannelRead(msg) + val child = streamMap[id] + when { + child == null -> { + releaseMessage(msg) + throw ConnectionClosedException("Channel with id $id not opened") + } + child.remoteDisconnected -> { + releaseMessage(msg) + throw ConnectionClosedException("Channel with id $id was closed for sending by remote") + } + else -> { + pendingReadComplete += id + child.pipeline().fireChannelRead(msg) + } + } } override fun channelReadComplete(ctx: ChannelHandlerContext) { @@ -64,6 +77,12 @@ abstract class AbstractMuxHandler() : pendingReadComplete.clear() } + /** + * Needs to be called when message was not passed to the child channel pipeline due to any error. + * (if a message was passed to the child channel it's the child channel's responsibility to release the message) + */ + abstract fun releaseMessage(msg: TData) + abstract fun onChildWrite(child: MuxChannel, data: TData) protected fun onRemoteOpen(id: MuxId) { @@ -96,6 +115,7 @@ abstract class AbstractMuxHandler() : fun onClosed(child: MuxChannel) { streamMap.remove(child.id) + onChildClosed(child) } abstract override fun channelRead(ctx: ChannelHandlerContext, msg: Any) @@ -103,6 +123,7 @@ abstract class AbstractMuxHandler() : protected abstract fun onLocalOpen(child: MuxChannel) protected abstract fun onLocalClose(child: MuxChannel) protected abstract fun onLocalDisconnect(child: MuxChannel) + protected abstract fun onChildClosed(child: MuxChannel) private fun createChild( id: MuxId, @@ -142,5 +163,6 @@ abstract class AbstractMuxHandler() : } } - private fun checkClosed() = if (closed) throw ConnectionClosedException("Can't create a new stream: connection was closed: " + ctx!!.channel()) else Unit + private fun checkClosed() = + if (closed) throw ConnectionClosedException("Can't create a new stream: connection was closed: " + ctx!!.channel()) else Unit } diff --git a/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/MuxChannel.kt b/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/MuxChannel.kt index 6b992b0a2..855046c5a 100644 --- a/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/MuxChannel.kt +++ b/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/MuxChannel.kt @@ -1,5 +1,6 @@ package io.libp2p.etc.util.netty.mux +import io.libp2p.core.ConnectionClosedException import io.libp2p.etc.util.netty.AbstractChildChannel import io.netty.channel.ChannelMetadata import io.netty.channel.ChannelOutboundBuffer @@ -16,8 +17,8 @@ class MuxChannel( val initiator: Boolean ) : AbstractChildChannel(parent.ctx!!.channel(), id) { - private var remoteDisconnected = false - private var localDisconnected = false + var remoteDisconnected = false + var localDisconnected = false override fun metadata(): ChannelMetadata = ChannelMetadata(true) override fun localAddress0() = @@ -35,6 +36,9 @@ class MuxChannel( while (true) { val msg = buf.current() ?: break try { + if (localDisconnected) { + throw ConnectionClosedException("The stream was closed for writing locally: $id") + } // the msg is released by both onChildWrite and buf.remove() so we need to retain // however it is still to be confirmed that no buf leaks happen here TODO ReferenceCountUtil.retain(msg) @@ -55,7 +59,7 @@ class MuxChannel( } fun onRemoteDisconnected() { - pipeline().fireUserEventTriggered(RemoteWriteClosed()) + pipeline().fireUserEventTriggered(RemoteWriteClosed) remoteDisconnected = true closeIfBothDisconnected() } @@ -74,11 +78,6 @@ class MuxChannel( } } -/** - * This Netty user event is fired to the [Stream] channel when remote peer closes its write side of the Stream - */ -class RemoteWriteClosed - data class MultiplexSocketAddress(val parentAddress: SocketAddress, val streamId: MuxId) : SocketAddress() { override fun toString(): String { return "Mux[$parentAddress-$streamId]" diff --git a/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/RemoteWriteClosed.kt b/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/RemoteWriteClosed.kt new file mode 100644 index 000000000..fceb10dfa --- /dev/null +++ b/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/RemoteWriteClosed.kt @@ -0,0 +1,6 @@ +package io.libp2p.etc.util.netty.mux + +/** + * This Netty user event is fired to the [Stream] channel when remote peer closes its write side of the Stream + */ +object RemoteWriteClosed diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/MuxHandler.kt b/libp2p/src/main/kotlin/io/libp2p/mux/MuxHandler.kt index 71a56ed6a..08a6bd12b 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/MuxHandler.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/MuxHandler.kt @@ -52,4 +52,8 @@ abstract class MuxHandler( }.thenApply { it.attr(STREAM).get() } return StreamPromise(stream, controller) } + + override fun releaseMessage(msg: ByteBuf) { + msg.release() + } } diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexHandler.kt b/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexHandler.kt index f886b3247..b87bdd8e6 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexHandler.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/mplex/MplexHandler.kt @@ -57,6 +57,5 @@ open class MplexHandler( getChannelHandlerContext().writeAndFlush(MplexFrame.createResetFrame(child.id)) } - override fun onRemoteCreated(child: MuxChannel) { - } + override fun onChildClosed(child: MuxChannel) {} } diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt index 645c54c78..b92538eeb 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt @@ -72,13 +72,15 @@ open class YamuxHandler( fun handleFlags(msg: YamuxFrame) { val ctx = getChannelHandlerContext() - if (msg.flags == YamuxFlags.SYN) { - // ACK the new stream - onRemoteOpen(msg.id) - ctx.writeAndFlush(YamuxFrame(msg.id, YamuxType.WINDOW_UPDATE, YamuxFlags.ACK, 0)) + when (msg.flags) { + YamuxFlags.SYN -> { + // ACK the new stream + onRemoteOpen(msg.id) + ctx.writeAndFlush(YamuxFrame(msg.id, YamuxType.WINDOW_UPDATE, YamuxFlags.ACK, 0)) + } + YamuxFlags.FIN -> onRemoteDisconnect(msg.id) + YamuxFlags.RST -> onRemoteClose(msg.id) } - if (msg.flags == YamuxFlags.FIN) - onRemoteDisconnect(msg.id) } fun handleDataRead(msg: YamuxFrame) { @@ -88,8 +90,10 @@ open class YamuxHandler( if (size.toInt() == 0) return val recWindow = receiveWindows.get(msg.id) - if (recWindow == null) + if (recWindow == null) { + releaseMessage(msg.data!!) throw Libp2pException("No receive window for " + msg.id) + } val newWindow = recWindow.addAndGet(-size.toInt()) if (newWindow < INITIAL_WINDOW_SIZE / 2) { val delta = INITIAL_WINDOW_SIZE / 2 @@ -143,30 +147,36 @@ open class YamuxHandler( } override fun onLocalOpen(child: MuxChannel) { + onStreamCreate(child) getChannelHandlerContext().writeAndFlush(YamuxFrame(child.id, YamuxType.DATA, YamuxFlags.SYN, 0)) + } + + override fun onRemoteCreated(child: MuxChannel) { + onStreamCreate(child) + } + + private fun onStreamCreate(child: MuxChannel) { receiveWindows.put(child.id, AtomicInteger(INITIAL_WINDOW_SIZE)) sendWindows.put(child.id, AtomicInteger(INITIAL_WINDOW_SIZE)) } override fun onLocalDisconnect(child: MuxChannel) { - sendWindows.remove(child.id) - receiveWindows.remove(child.id) - sendBuffers.remove(child.id) - getChannelHandlerContext().writeAndFlush(YamuxFrame(child.id, YamuxType.DATA, YamuxFlags.FIN, 0)) - } - - override fun onLocalClose(child: MuxChannel) { - getChannelHandlerContext().writeAndFlush(YamuxFrame(child.id, YamuxType.DATA, YamuxFlags.RST, 0)) val sendWindow = sendWindows.remove(child.id) val buffered = sendBuffers.remove(child.id) if (buffered != null && sendWindow != null) { buffered.flush(sendWindow, child.id) } + getChannelHandlerContext().writeAndFlush(YamuxFrame(child.id, YamuxType.DATA, YamuxFlags.FIN, 0)) } - override fun onRemoteCreated(child: MuxChannel) { - receiveWindows.put(child.id, AtomicInteger(INITIAL_WINDOW_SIZE)) - sendWindows.put(child.id, AtomicInteger(INITIAL_WINDOW_SIZE)) + override fun onLocalClose(child: MuxChannel) { + getChannelHandlerContext().writeAndFlush(YamuxFrame(child.id, YamuxType.DATA, YamuxFlags.RST, 0)) + } + + override fun onChildClosed(child: MuxChannel) { + sendWindows.remove(child.id) + receiveWindows.remove(child.id) + sendBuffers.remove(child.id) } override fun generateNextId() = diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt index b4ff22a37..466718cd9 100644 --- a/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt @@ -2,21 +2,24 @@ package io.libp2p.mux import io.libp2p.core.ConnectionClosedException import io.libp2p.core.Libp2pException -import io.libp2p.core.Stream import io.libp2p.core.StreamHandler +import io.libp2p.etc.types.fromHex import io.libp2p.etc.types.getX -import io.libp2p.etc.types.toByteArray import io.libp2p.etc.types.toHex +import io.libp2p.etc.util.netty.mux.RemoteWriteClosed import io.libp2p.etc.util.netty.nettyInitializer +import io.libp2p.mux.MuxHandlerAbstractTest.AbstractTestMuxFrame.Flag.* import io.libp2p.tools.TestChannel +import io.libp2p.tools.readAllBytesAndRelease import io.netty.buffer.ByteBuf -import io.netty.channel.ChannelHandler +import io.netty.buffer.Unpooled import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelInboundHandlerAdapter -import io.netty.channel.DefaultChannelId import io.netty.handler.logging.LogLevel import io.netty.handler.logging.LoggingHandler import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Index +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertThrows @@ -29,38 +32,85 @@ import java.util.concurrent.CompletableFuture * Created by Anton Nashatyrev on 09.07.2019. */ abstract class MuxHandlerAbstractTest { - val dummyParentChannelId = DefaultChannelId.newInstance() val childHandlers = mutableListOf() lateinit var multistreamHandler: MuxHandler lateinit var ech: TestChannel + val parentChannelId get() = ech.id() + + val allocatedBufs = mutableListOf() + + abstract val maxFrameDataLength: Int + abstract fun createMuxHandler(streamHandler: StreamHandler<*>): MuxHandler + + fun createTestStreamHandler(): StreamHandler = + StreamHandler { stream -> + val handler = TestHandler() + stream.pushHandler( + nettyInitializer { + it.addLastLocal(handler) + } + ) + CompletableFuture.completedFuture(handler) + } - abstract fun createMuxHandler(streamHandler: StreamHandler): MuxHandler + fun StreamHandler.onNewStream(block: (T) -> Unit): StreamHandler = + StreamHandler { stream -> + this.handleStream(stream) + .thenApply { + block(it) + it + } + } @BeforeEach fun startMultiplexor() { - childHandlers.clear() - val streamHandler = createStreamHandler( - nettyInitializer { - println("New child channel created") - val handler = TestHandler() - it.addLastLocal(handler) - childHandlers += handler + val streamHandler = createTestStreamHandler() + .onNewStream { + childHandlers += it } - ) multistreamHandler = createMuxHandler(streamHandler) ech = TestChannel("test", true, LoggingHandler(LogLevel.ERROR), multistreamHandler) } - abstract fun openStream(id: Long): Boolean - abstract fun writeStream(id: Long, msg: String): Boolean - abstract fun resetStream(id: Long): Boolean + @AfterEach + open fun cleanUpAndCheck() { + childHandlers.clear() - fun createStreamHandler(channelInitializer: ChannelHandler) = object : StreamHandler { - override fun handleStream(stream: Stream): CompletableFuture { - stream.pushHandler(channelInitializer) - return CompletableFuture.completedFuture(Unit) + allocatedBufs.forEach { + assertThat(it.refCnt()).isEqualTo(1) } + allocatedBufs.clear() + } + + data class AbstractTestMuxFrame( + val streamId: Long, + val flag: Flag, + val data: String = "" + ) { + enum class Flag { Open, Data, Close, Reset } + } + + abstract fun writeFrame(frame: AbstractTestMuxFrame) + abstract fun readFrame(): AbstractTestMuxFrame? + fun readFrameOrThrow() = readFrame() ?: throw AssertionError("No outbound frames") + + fun openStream(id: Long) = writeFrame(AbstractTestMuxFrame(id, Open)) + fun writeStream(id: Long, msg: String) = writeFrame(AbstractTestMuxFrame(id, Data, msg)) + fun closeStream(id: Long) = writeFrame(AbstractTestMuxFrame(id, Close)) + fun resetStream(id: Long) = writeFrame(AbstractTestMuxFrame(id, Reset)) + + fun openStreamByLocal(): TestHandler { + val handlerFut = multistreamHandler.createStream(createTestStreamHandler()).controller + ech.runPendingTasks() + return handlerFut.get() + } + + protected fun allocateBuf(): ByteBuf { + val buf = Unpooled.buffer() + buf.retain() // ref counter to 2 to check that exactly 1 ref remains at the end + allocatedBufs += buf + return buf } fun assertHandlerCount(count: Int) = assertEquals(count, childHandlers.size) @@ -74,6 +124,7 @@ abstract class MuxHandlerAbstractTest { fun singleStream() { openStream(12) assertHandlerCount(1) + assertTrue(childHandlers[0].isActivated) writeStream(12, "22") assertHandlerCount(1) @@ -89,6 +140,9 @@ abstract class MuxHandlerAbstractTest { assertHandlerCount(1) assertEquals(3, childHandlers[0].inboundMessages.size) assertEquals("66", childHandlers[0].inboundMessages.last()) + + assertFalse(childHandlers[0].isInactivated) + assertTrue(childHandlers[0].exceptions.isEmpty()) } @Test @@ -149,6 +203,11 @@ abstract class MuxHandlerAbstractTest { writeStream(22, "34") assertHandlerCount(2) assertLastMessage(1, 2, "34") + + assertFalse(childHandlers[0].isInactivated) + assertTrue(childHandlers[0].exceptions.isEmpty()) + assertFalse(childHandlers[1].isInactivated) + assertTrue(childHandlers[1].exceptions.isEmpty()) } @Test @@ -171,8 +230,10 @@ abstract class MuxHandlerAbstractTest { assertHandlerCount(1) assertLastMessage(0, 4, "25") + assertFalse(childHandlers[0].isInactivated) resetStream(12) - assertHandlerCount(1) + assertTrue(childHandlers[0].isHandlerRemoved) + assertTrue(childHandlers[0].exceptions.isEmpty()) openStream(22) writeStream(22, "33") @@ -183,49 +244,69 @@ abstract class MuxHandlerAbstractTest { assertHandlerCount(2) assertLastMessage(1, 2, "34") - resetStream(12) - assertHandlerCount(2) + assertFalse(childHandlers[1].isInactivated) + resetStream(22) + assertTrue(childHandlers[1].isHandlerRemoved) + assertTrue(childHandlers[1].exceptions.isEmpty()) } @Test fun streamIsReset() { openStream(22) - assertFalse(childHandlers[0].ctx!!.channel().closeFuture().isDone) + assertFalse(childHandlers[0].ctx.channel().closeFuture().isDone) + assertFalse(childHandlers[0].isInactivated) resetStream(22) - assertTrue(childHandlers[0].ctx!!.channel().closeFuture().isDone) + assertTrue(childHandlers[0].ctx.channel().closeFuture().isDone) + assertTrue(childHandlers[0].isHandlerRemoved) } @Test fun streamIsResetWhenChannelIsClosed() { openStream(22) - assertFalse(childHandlers[0].ctx!!.channel().closeFuture().isDone) + assertFalse(childHandlers[0].ctx.channel().closeFuture().isDone) ech.close().await() - assertTrue(childHandlers[0].ctx!!.channel().closeFuture().isDone) + assertTrue(childHandlers[0].ctx.channel().closeFuture().isDone) + assertTrue(childHandlers[0].isHandlerRemoved) + assertTrue(childHandlers[0].exceptions.isEmpty()) } @Test - fun cantWriteToResetStream() { + fun cantReceiveOnResetStream() { openStream(18) resetStream(18) assertThrows(Libp2pException::class.java) { writeStream(18, "35") } + assertTrue(childHandlers[0].isHandlerRemoved) + } + + @Test + fun cantReceiveOnClosedStream() { + openStream(18) + closeStream(18) + + assertThrows(Libp2pException::class.java) { + writeStream(18, "35") + } + assertFalse(childHandlers[0].isInactivated) } @Test - fun cantWriteToNonExistentStream() { + fun cantReceiveOnNonExistentStream() { assertThrows(Libp2pException::class.java) { writeStream(92, "35") } + assertHandlerCount(0) } @Test fun canResetNonExistentStream() { resetStream(99) + assertHandlerCount(0) } @Test @@ -239,33 +320,170 @@ abstract class MuxHandlerAbstractTest { } assertThrows(ConnectionClosedException::class.java) { staleStream.stream.getX(3.0) } + assertHandlerCount(0) + } + + @Test + fun `local create and after local disconnect should still read`() { + val handler = openStreamByLocal() + handler.ctx.writeAndFlush("1984".fromHex().toByteBuf(allocateBuf())) + handler.ctx.disconnect().sync() + + val openFrame = readFrameOrThrow() + assertThat(openFrame.flag).isEqualTo(Open) + + val dataFrame = readFrameOrThrow() + assertThat(dataFrame.flag).isEqualTo(Data) + assertThat(dataFrame.streamId).isEqualTo(openFrame.streamId) + + val closeFrame = readFrameOrThrow() + assertThat(closeFrame.flag).isEqualTo(Close) + + assertThat(readFrame()).isNull() + assertThat(handler.isInactivated).isTrue() + assertThat(handler.isUnregistered).isFalse() + assertThat(handler.inboundMessages).isEmpty() + + writeStream(dataFrame.streamId, "1122") + assertThat(handler.inboundMessages).isNotEmpty + } + + @Test + fun `local create and after remote disconnect should still write`() { + val handler = openStreamByLocal() + + val openFrame = readFrameOrThrow() + assertThat(openFrame.flag).isEqualTo(Open) + assertThat(readFrame()).isNull() + + closeStream(openFrame.streamId) + + assertThat(handler.isInactivated).isFalse() + assertThat(handler.isUnregistered).isFalse() + assertThat(handler.userEvents).containsExactly(RemoteWriteClosed) + + handler.ctx.writeAndFlush("1984".fromHex().toByteBuf(allocateBuf())) + + val readFrame = readFrameOrThrow() + assertThat(readFrame.flag).isEqualTo(Data) + assertThat(readFrame.data).isEqualTo("1984") + assertThat(readFrame()).isNull() + } + + @Test + fun `test remote and local disconnect closes stream`() { + val handler = openStreamByLocal() + handler.ctx.disconnect().sync() + + readFrameOrThrow() + val closeFrame = readFrameOrThrow() + assertThat(closeFrame.flag).isEqualTo(Close) + + assertThat(handler.isInactivated).isTrue() + assertThat(handler.isUnregistered).isFalse() + + closeStream(closeFrame.streamId) + + assertThat(handler.isHandlerRemoved).isTrue() + } + + @Test + fun `test large message is split onto slices`() { + val handler = openStreamByLocal() + readFrameOrThrow() + + val largeMessage = "42".repeat(maxFrameDataLength - 1) + "4344" + handler.ctx.writeAndFlush(largeMessage.fromHex().toByteBuf(allocateBuf())) + + val dataFrame1 = readFrameOrThrow() + assertThat(dataFrame1.data.fromHex()) + .hasSize(maxFrameDataLength) + .contains(0x42, Index.atIndex(0)) + .contains(0x42, Index.atIndex(maxFrameDataLength - 2)) + .contains(0x43, Index.atIndex(maxFrameDataLength - 1)) + + val dataFrame2 = readFrameOrThrow() + assertThat(dataFrame2.data.fromHex()) + .hasSize(1) + .contains(0x44, Index.atIndex(0)) + + assertThat(readFrame()).isNull() + } + + @Test + fun `should throw when writing to locally closed stream`() { + val handler = openStreamByLocal() + handler.ctx.disconnect() + + assertThrows(Exception::class.java) { + handler.ctx.writeAndFlush("42".fromHex().toByteBuf(allocateBuf())).sync() + } + } + + @Test + fun `should throw when writing to reset stream`() { + val handler = openStreamByLocal() + handler.ctx.close() + + assertThrows(Exception::class.java) { + handler.ctx.writeAndFlush("42".fromHex().toByteBuf(allocateBuf())).sync() + } + } + + @Test + fun `should throw when writing to closed connection`() { + val handler = openStreamByLocal() + ech.close().sync() + + assertThrows(Exception::class.java) { + handler.ctx.writeAndFlush("42".fromHex().toByteBuf(allocateBuf())).sync() + } } class TestHandler : ChannelInboundHandlerAdapter() { val inboundMessages = mutableListOf() - var ctx: ChannelHandlerContext? = null + lateinit var ctx: ChannelHandlerContext var readCompleteEventCount = 0 - override fun channelInactive(ctx: ChannelHandlerContext?) { - println("MultiplexHandlerTest.channelInactive") + val exceptions = mutableListOf() + val userEvents = mutableListOf() + var isHandlerAdded = false + var isRegistered = false + var isActivated = false + var isInactivated = false + var isUnregistered = false + var isHandlerRemoved = false + + init { + println("New child channel created") } - override fun channelRead(ctx: ChannelHandlerContext?, msg: Any?) { - println("MultiplexHandlerTest.channelRead") - msg as ByteBuf - inboundMessages += msg.toByteArray().toHex() + override fun handlerAdded(ctx: ChannelHandlerContext) { + assertFalse(isHandlerAdded) + isHandlerAdded = true + println("MultiplexHandlerTest.handlerAdded") + this.ctx = ctx } - override fun channelUnregistered(ctx: ChannelHandlerContext?) { - println("MultiplexHandlerTest.channelUnregistered") + override fun channelRegistered(ctx: ChannelHandlerContext?) { + assertTrue(isHandlerAdded) + assertFalse(isRegistered) + isRegistered = true + println("MultiplexHandlerTest.channelRegistered") } - override fun channelActive(ctx: ChannelHandlerContext?) { + override fun channelActive(ctx: ChannelHandlerContext) { + assertTrue(isRegistered) + assertFalse(isActivated) + isActivated = true println("MultiplexHandlerTest.channelActive") } - override fun channelRegistered(ctx: ChannelHandlerContext?) { - println("MultiplexHandlerTest.channelRegistered") + override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { + assertTrue(isActivated) + println("MultiplexHandlerTest.channelRead") + msg as ByteBuf + inboundMessages += msg.readAllBytesAndRelease().toHex() } override fun channelReadComplete(ctx: ChannelHandlerContext?) { @@ -273,17 +491,39 @@ abstract class MuxHandlerAbstractTest { println("MultiplexHandlerTest.channelReadComplete") } - override fun handlerAdded(ctx: ChannelHandlerContext?) { - println("MultiplexHandlerTest.handlerAdded") - this.ctx = ctx + override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { + userEvents += evt + println("MultiplexHandlerTest.userEventTriggered: $evt") } - override fun exceptionCaught(ctx: ChannelHandlerContext?, cause: Throwable?) { + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + exceptions += cause println("MultiplexHandlerTest.exceptionCaught") } + override fun channelInactive(ctx: ChannelHandlerContext) { + assertTrue(isActivated) + assertFalse(isInactivated) + isInactivated = true + println("MultiplexHandlerTest.channelInactive") + } + + override fun channelUnregistered(ctx: ChannelHandlerContext?) { + assertTrue(isInactivated) + assertFalse(isUnregistered) + isUnregistered = true + println("MultiplexHandlerTest.channelUnregistered") + } + override fun handlerRemoved(ctx: ChannelHandlerContext?) { + assertTrue(isUnregistered) + assertFalse(isHandlerRemoved) + isHandlerRemoved = true println("MultiplexHandlerTest.handlerRemoved") } } + + companion object { + fun ByteArray.toByteBuf(buf: ByteBuf): ByteBuf = buf.writeBytes(this) + } } diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexHandlerTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexHandlerTest.kt index e64115a57..091107331 100644 --- a/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexHandlerTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexHandlerTest.kt @@ -3,19 +3,22 @@ package io.libp2p.mux.mplex import io.libp2p.core.StreamHandler import io.libp2p.core.multistream.MultistreamProtocolV1 import io.libp2p.etc.types.fromHex -import io.libp2p.etc.types.toByteBuf +import io.libp2p.etc.types.toHex import io.libp2p.etc.util.netty.mux.MuxId import io.libp2p.mux.MuxHandler import io.libp2p.mux.MuxHandlerAbstractTest -import io.netty.buffer.ByteBuf +import io.libp2p.mux.MuxHandlerAbstractTest.AbstractTestMuxFrame.Flag.* +import io.libp2p.tools.readAllBytesAndRelease import io.netty.buffer.Unpooled import io.netty.channel.ChannelHandlerContext class MplexHandlerTest : MuxHandlerAbstractTest() { - override fun createMuxHandler(streamHandler: StreamHandler): MuxHandler = + override val maxFrameDataLength = 256 + + override fun createMuxHandler(streamHandler: StreamHandler<*>): MuxHandler = object : MplexHandler( - MultistreamProtocolV1, DEFAULT_MAX_MPLEX_FRAME_DATA_LENGTH, null, streamHandler + MultistreamProtocolV1, maxFrameDataLength, null, streamHandler ) { // MuxHandler consumes the exception. Override this behaviour for testing @Deprecated("Deprecated in Java") @@ -24,9 +27,34 @@ class MplexHandlerTest : MuxHandlerAbstractTest() { } } - override fun openStream(id: Long) = writeFrame(id, MplexFlag.Type.OPEN) - override fun writeStream(id: Long, msg: String) = writeFrame(id, MplexFlag.Type.DATA, msg.fromHex().toByteBuf()) - override fun resetStream(id: Long) = writeFrame(id, MplexFlag.Type.RESET) - fun writeFrame(id: Long, flagType: MplexFlag.Type, data: ByteBuf = Unpooled.EMPTY_BUFFER) = - ech.writeInbound(MplexFrame(MuxId(dummyParentChannelId, id, true), MplexFlag.getByType(flagType, true), data)) + override fun writeFrame(frame: AbstractTestMuxFrame) { + val mplexFlag = when (frame.flag) { + Open -> MplexFlag.Type.OPEN + Data -> MplexFlag.Type.DATA + Close -> MplexFlag.Type.CLOSE + Reset -> MplexFlag.Type.RESET + } + val data = when { + frame.data.isEmpty() -> Unpooled.EMPTY_BUFFER + else -> frame.data.fromHex().toByteBuf(allocateBuf()) + } + val mplexFrame = + MplexFrame(MuxId(parentChannelId, frame.streamId, true), MplexFlag.getByType(mplexFlag, true), data) + ech.writeInbound(mplexFrame) + } + + override fun readFrame(): AbstractTestMuxFrame? { + val maybeMplexFrame = ech.readOutbound() + return maybeMplexFrame?.let { mplexFrame -> + val flag = when (mplexFrame.flag.type) { + MplexFlag.Type.OPEN -> Open + MplexFlag.Type.DATA -> Data + MplexFlag.Type.CLOSE -> Close + MplexFlag.Type.RESET -> Reset + else -> throw AssertionError("Unknown mplex flag: ${mplexFrame.flag}") + } + val sData = maybeMplexFrame.data.readAllBytesAndRelease().toHex() + AbstractTestMuxFrame(mplexFrame.id.id, flag, sData) + } + } } diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt index 69016f56b..b920d1285 100644 --- a/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt @@ -3,17 +3,21 @@ package io.libp2p.mux.yamux import io.libp2p.core.StreamHandler import io.libp2p.core.multistream.MultistreamProtocolV1 import io.libp2p.etc.types.fromHex -import io.libp2p.etc.types.toByteBuf +import io.libp2p.etc.types.toHex import io.libp2p.etc.util.netty.mux.MuxId import io.libp2p.mux.MuxHandler import io.libp2p.mux.MuxHandlerAbstractTest +import io.libp2p.mux.MuxHandlerAbstractTest.AbstractTestMuxFrame.Flag.* +import io.libp2p.tools.readAllBytesAndRelease import io.netty.channel.ChannelHandlerContext class YamuxHandlerTest : MuxHandlerAbstractTest() { - override fun createMuxHandler(streamHandler: StreamHandler): MuxHandler = + override val maxFrameDataLength = 256 + + override fun createMuxHandler(streamHandler: StreamHandler<*>): MuxHandler = object : YamuxHandler( - MultistreamProtocolV1, DEFAULT_MAX_YAMUX_FRAME_DATA_LENGTH, null, streamHandler, true + MultistreamProtocolV1, maxFrameDataLength, null, streamHandler, true ) { // MuxHandler consumes the exception. Override this behaviour for testing @Deprecated("Deprecated in Java") @@ -22,20 +26,35 @@ class YamuxHandlerTest : MuxHandlerAbstractTest() { } } - override fun openStream(id: Long) = - ech.writeInbound(YamuxFrame(MuxId(dummyParentChannelId, id, true), YamuxType.DATA, YamuxFlags.SYN, 0)) - - override fun writeStream(id: Long, msg: String) = - ech.writeInbound( - YamuxFrame( - MuxId(dummyParentChannelId, id, true), + override fun writeFrame(frame: AbstractTestMuxFrame) { + val muxId = MuxId(parentChannelId, frame.streamId, true) + val yamuxFrame = when (frame.flag) { + Open -> YamuxFrame(muxId, YamuxType.DATA, YamuxFlags.SYN, 0) + Data -> YamuxFrame( + muxId, YamuxType.DATA, 0, - msg.fromHex().size.toLong(), - msg.fromHex().toByteBuf() + frame.data.fromHex().size.toLong(), + frame.data.fromHex().toByteBuf(allocateBuf()) ) - ) + Close -> YamuxFrame(muxId, YamuxType.DATA, YamuxFlags.FIN, 0) + Reset -> YamuxFrame(muxId, YamuxType.DATA, YamuxFlags.RST, 0) + } + ech.writeInbound(yamuxFrame) + } - override fun resetStream(id: Long) = - ech.writeInbound(YamuxFrame(MuxId(dummyParentChannelId, id, true), YamuxType.GO_AWAY, 0, 0)) + override fun readFrame(): AbstractTestMuxFrame? { + val maybeYamuxFrame = ech.readOutbound() + return maybeYamuxFrame?.let { yamuxFrame -> + val flag = when { + yamuxFrame.flags == YamuxFlags.SYN -> Open + yamuxFrame.flags == YamuxFlags.FIN -> Close + yamuxFrame.flags == YamuxFlags.RST -> Reset + yamuxFrame.type == YamuxType.DATA -> Data + else -> throw AssertionError("Unsupported yamux frame: $yamuxFrame") + } + val sData = yamuxFrame.data?.readAllBytesAndRelease()?.toHex() ?: "" + AbstractTestMuxFrame(yamuxFrame.id.id, flag, sData) + } + } } diff --git a/libp2p/src/testFixtures/kotlin/io/libp2p/tools/ByteBufExt.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/ByteBufExt.kt new file mode 100644 index 000000000..bb91e5840 --- /dev/null +++ b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/ByteBufExt.kt @@ -0,0 +1,10 @@ +package io.libp2p.tools + +import io.netty.buffer.ByteBuf + +fun ByteBuf.readAllBytesAndRelease(): ByteArray { + val arr = ByteArray(readableBytes()) + this.readBytes(arr) + this.release() + return arr +} From de42c1122265f66835fa0d1078a38c2f6328272e Mon Sep 17 00:00:00 2001 From: Dr Ian Preston Date: Wed, 31 May 2023 14:23:32 +0100 Subject: [PATCH 09/16] Implement TLS security protocol including early muxer negotiation (#283) * Add early muxer negotiation support * Implement TLS security protocol * Remove the insecure Secio from builders so it can't be accidentally included * Add TLS tests --- jitpack.yml | 2 + libp2p/build.gradle.kts | 1 + .../java/io/libp2p/core/dsl/HostBuilder.java | 11 +- .../kotlin/io/libp2p/core/dsl/Builders.kt | 10 +- .../core/multistream/NegotiatedProtocol.kt | 15 + .../core/multistream/ProtocolBinding.kt | 6 +- .../libp2p/core/mux/NegotiatedStreamMuxer.kt | 5 + .../io/libp2p/core/security/SecureChannel.kt | 13 +- .../kotlin/io/libp2p/crypto/keys/Ecdsa.kt | 4 + .../io/libp2p/multistream/ProtocolSelect.kt | 2 - .../noise/NoiseSecureChannelSession.kt | 2 +- .../security/noise/NoiseXXSecureChannel.kt | 8 +- .../plaintext/PlaintextInsecureChannel.kt | 8 +- .../security/secio/SecIoSecureChannel.kt | 8 +- .../libp2p/security/tls/TLSSecureChannel.kt | 343 ++++++++++++++++++ .../io/libp2p/transport/ConnectionUpgrader.kt | 22 +- .../implementation/ConnectionBuilder.kt | 6 +- .../java/io/libp2p/core/HostTestJava.java | 10 +- .../io/libp2p/core/dsl/BuilderDefaultsTest.kt | 4 +- .../kotlin/io/libp2p/crypto/KeyTypesTest.kt | 43 +++ .../security/CipherSecureChannelTest.kt | 13 +- .../libp2p/security/SecureChannelTestBase.kt | 10 +- .../security/noise/NoiseHandshakeTest.kt | 6 +- .../security/noise/NoiseSecureChannelTest.kt | 1 + .../plaintext/PlaintextInsecureChannelTest.kt | 1 + .../libp2p/security/secio/EchoSampleTest.kt | 2 +- .../security/secio/SecIoSecureChannelTest.kt | 1 + .../libp2p/security/tls/CertificatesTest.kt | 66 ++++ .../security/tls/TlsSecureChannelTest.kt | 42 +++ .../kotlin/io/libp2p/pubsub/TestRouter.kt | 2 +- .../kotlin/io/libp2p/tools/HostFactory.kt | 3 +- .../transport/NullConnectionUpgrader.kt | 3 +- .../simulate/stream/Libp2pConnectionImpl.kt | 3 +- versions.gradle | 3 +- 34 files changed, 624 insertions(+), 55 deletions(-) create mode 100644 jitpack.yml create mode 100644 libp2p/src/main/kotlin/io/libp2p/core/multistream/NegotiatedProtocol.kt create mode 100644 libp2p/src/main/kotlin/io/libp2p/core/mux/NegotiatedStreamMuxer.kt create mode 100644 libp2p/src/main/kotlin/io/libp2p/security/tls/TLSSecureChannel.kt create mode 100644 libp2p/src/test/kotlin/io/libp2p/crypto/KeyTypesTest.kt create mode 100644 libp2p/src/test/kotlin/io/libp2p/security/tls/CertificatesTest.kt create mode 100644 libp2p/src/test/kotlin/io/libp2p/security/tls/TlsSecureChannelTest.kt diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 000000000..46c852919 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,2 @@ +jdk: + - openjdk11 \ No newline at end of file diff --git a/libp2p/build.gradle.kts b/libp2p/build.gradle.kts index 4012cb2d7..49f8833ae 100644 --- a/libp2p/build.gradle.kts +++ b/libp2p/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation("org.bouncycastle:bcprov-jdk15on") implementation("org.bouncycastle:bcpkix-jdk15on") + implementation("org.bouncycastle:bctls-jdk15on") testImplementation(project(":tools:schedulers")) diff --git a/libp2p/src/main/java/io/libp2p/core/dsl/HostBuilder.java b/libp2p/src/main/java/io/libp2p/core/dsl/HostBuilder.java index f32724268..069d5701a 100644 --- a/libp2p/src/main/java/io/libp2p/core/dsl/HostBuilder.java +++ b/libp2p/src/main/java/io/libp2p/core/dsl/HostBuilder.java @@ -3,7 +3,7 @@ import io.libp2p.core.Host; import io.libp2p.core.crypto.PrivKey; import io.libp2p.core.multistream.ProtocolBinding; -import io.libp2p.core.mux.StreamMuxerProtocol; +import io.libp2p.core.mux.*; import io.libp2p.core.security.SecureChannel; import io.libp2p.core.transport.Transport; import io.libp2p.transport.ConnectionUpgrader; @@ -11,8 +11,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.function.Function; -import java.util.function.Supplier; +import java.util.function.*; public class HostBuilder { public HostBuilder() { this(DefaultMode.Standard); } @@ -41,7 +40,7 @@ public final HostBuilder transport( @SafeVarargs public final HostBuilder secureChannel( - Function... secureChannels) { + BiFunction, SecureChannel>... secureChannels) { secureChannels_.addAll(Arrays.asList(secureChannels)); return this; } @@ -76,7 +75,7 @@ public Host build() { b.getTransports().add(t::apply) ); secureChannels_.forEach(sc -> - b.getSecureChannels().add(sc::apply) + b.getSecureChannels().add((k, m) -> sc.apply(k, (List)m)) ); muxers_.forEach(m -> b.getMuxers().add(m.get()) @@ -91,7 +90,7 @@ public Host build() { private DefaultMode defaultMode_; private List> transports_ = new ArrayList<>(); - private List> secureChannels_ = new ArrayList<>(); + private List, SecureChannel>> secureChannels_ = new ArrayList<>(); private List> muxers_ = new ArrayList<>(); private List> protocols_ = new ArrayList<>(); private List listenAddresses_ = new ArrayList<>(); diff --git a/libp2p/src/main/kotlin/io/libp2p/core/dsl/Builders.kt b/libp2p/src/main/kotlin/io/libp2p/core/dsl/Builders.kt index 203aa476f..5482b5e8e 100644 --- a/libp2p/src/main/kotlin/io/libp2p/core/dsl/Builders.kt +++ b/libp2p/src/main/kotlin/io/libp2p/core/dsl/Builders.kt @@ -27,7 +27,7 @@ import io.libp2p.host.HostImpl import io.libp2p.host.MemoryAddressBook import io.libp2p.network.NetworkImpl import io.libp2p.protocol.IdentifyBinding -import io.libp2p.security.secio.SecIoSecureChannel +import io.libp2p.security.noise.NoiseXXSecureChannel import io.libp2p.transport.ConnectionUpgrader import io.libp2p.transport.tcp.TcpTransport import io.netty.channel.ChannelHandler @@ -35,7 +35,7 @@ import io.netty.handler.logging.LogLevel import io.netty.handler.logging.LoggingHandler typealias TransportCtor = (ConnectionUpgrader) -> Transport -typealias SecureChannelCtor = (PrivKey) -> SecureChannel +typealias SecureChannelCtor = (PrivKey, List) -> SecureChannel typealias IdentityFactory = () -> PrivKey class HostConfigurationException(message: String) : RuntimeException(message) @@ -131,7 +131,7 @@ open class Builder { if (def == Defaults.Standard) { if (identity.factory == null) identity.random() if (transports.values.isEmpty()) transports { add(::TcpTransport) } - if (secureChannels.values.isEmpty()) secureChannels { add(::SecIoSecureChannel) } + if (secureChannels.values.isEmpty()) secureChannels { add(::NoiseXXSecureChannel) } if (muxers.values.isEmpty()) muxers { add(StreamMuxerProtocol.Mplex) } } @@ -160,8 +160,6 @@ open class Builder { val privKey = identity.factory!!() - val secureChannels = secureChannels.values.map { it(privKey) } - protocols.values.mapNotNull { (it as? IdentifyBinding) }.map { it.protocol }.find { it.idMessage == null }?.apply { // initializing Identify with appropriate values IdentifyOuterClass.Identify.newBuilder().apply { @@ -177,6 +175,8 @@ open class Builder { val muxers = muxers.map { it.createMuxer(streamMultistreamProtocol, protocols.values) } + val secureChannels = secureChannels.values.map { it(privKey, muxers) } + if (debug.muxFramesHandler.handlers.isNotEmpty()) { val broadcast = ChannelVisitor.createBroadcast(*debug.muxFramesHandler.handlers.toTypedArray()) muxers.mapNotNull { it as? StreamMuxerDebug }.forEach { diff --git a/libp2p/src/main/kotlin/io/libp2p/core/multistream/NegotiatedProtocol.kt b/libp2p/src/main/kotlin/io/libp2p/core/multistream/NegotiatedProtocol.kt new file mode 100644 index 000000000..e0b04d655 --- /dev/null +++ b/libp2p/src/main/kotlin/io/libp2p/core/multistream/NegotiatedProtocol.kt @@ -0,0 +1,15 @@ +package io.libp2p.core.multistream + +import io.libp2p.core.P2PChannel +import java.util.concurrent.CompletableFuture + +/** + * Represents [ProtocolBinding] with exact protocol version which was agreed on + */ +open class NegotiatedProtocol> ( + val binding: TBinding, + val protocol: ProtocolId +) { + open fun initChannel(ch: P2PChannel): CompletableFuture = + binding.initChannel(ch, protocol) +} diff --git a/libp2p/src/main/kotlin/io/libp2p/core/multistream/ProtocolBinding.kt b/libp2p/src/main/kotlin/io/libp2p/core/multistream/ProtocolBinding.kt index 054b3c0f3..6e0b1224a 100644 --- a/libp2p/src/main/kotlin/io/libp2p/core/multistream/ProtocolBinding.kt +++ b/libp2p/src/main/kotlin/io/libp2p/core/multistream/ProtocolBinding.kt @@ -44,7 +44,7 @@ interface ProtocolBinding { /** * Returns initializer for this protocol on the provided channel, together with an optional controller object. */ - fun initChannel(ch: P2PChannel, selectedProtocol: String): CompletableFuture + fun initChannel(ch: P2PChannel, selectedProtocol: ProtocolId): CompletableFuture /** * If the [matcher] of this binding is not [Mode.STRICT] then it can't play initiator role since @@ -56,7 +56,7 @@ interface ProtocolBinding { val srcBinding = this return object : ProtocolBinding { override val protocolDescriptor = ProtocolDescriptor(protocols, srcBinding.protocolDescriptor.protocolMatcher) - override fun initChannel(ch: P2PChannel, selectedProtocol: String): CompletableFuture = + override fun initChannel(ch: P2PChannel, selectedProtocol: ProtocolId): CompletableFuture = srcBinding.initChannel(ch, selectedProtocol) } } @@ -68,7 +68,7 @@ interface ProtocolBinding { fun createSimple(protocolName: ProtocolId, handler: P2PChannelHandler): ProtocolBinding { return object : ProtocolBinding { override val protocolDescriptor = ProtocolDescriptor(protocolName) - override fun initChannel(ch: P2PChannel, selectedProtocol: String): CompletableFuture { + override fun initChannel(ch: P2PChannel, selectedProtocol: ProtocolId): CompletableFuture { return handler.initChannel(ch) } } diff --git a/libp2p/src/main/kotlin/io/libp2p/core/mux/NegotiatedStreamMuxer.kt b/libp2p/src/main/kotlin/io/libp2p/core/mux/NegotiatedStreamMuxer.kt new file mode 100644 index 000000000..4053b16df --- /dev/null +++ b/libp2p/src/main/kotlin/io/libp2p/core/mux/NegotiatedStreamMuxer.kt @@ -0,0 +1,5 @@ +package io.libp2p.core.mux + +import io.libp2p.core.multistream.NegotiatedProtocol + +typealias NegotiatedStreamMuxer = NegotiatedProtocol diff --git a/libp2p/src/main/kotlin/io/libp2p/core/security/SecureChannel.kt b/libp2p/src/main/kotlin/io/libp2p/core/security/SecureChannel.kt index fb14f039c..abd0b69cd 100644 --- a/libp2p/src/main/kotlin/io/libp2p/core/security/SecureChannel.kt +++ b/libp2p/src/main/kotlin/io/libp2p/core/security/SecureChannel.kt @@ -3,11 +3,13 @@ package io.libp2p.core.security import io.libp2p.core.PeerId import io.libp2p.core.crypto.PubKey import io.libp2p.core.multistream.ProtocolBinding +import io.libp2p.core.mux.NegotiatedStreamMuxer /** * The SecureChannel interface is implemented by all security channels, such as SecIO, TLS 1.3, Noise, and so on. */ interface SecureChannel : ProtocolBinding { + open class Session( /** * The peer ID of the local peer. @@ -20,8 +22,15 @@ interface SecureChannel : ProtocolBinding { val remoteId: PeerId, /** - * The public key of the + * The public key of the remote peer. + */ + val remotePubKey: PubKey, + + /** + * Contains muxer if security protocol supports + * [Early Multiplexer Negotiation](https://docs.libp2p.io/concepts/multiplex/early-negotiation/) + * and the protocol was successfully negotiated. Else contains `null` */ - val remotePubKey: PubKey + val earlyMuxer: NegotiatedStreamMuxer? ) } diff --git a/libp2p/src/main/kotlin/io/libp2p/crypto/keys/Ecdsa.kt b/libp2p/src/main/kotlin/io/libp2p/crypto/keys/Ecdsa.kt index b8f3e124e..3ff0de363 100644 --- a/libp2p/src/main/kotlin/io/libp2p/crypto/keys/Ecdsa.kt +++ b/libp2p/src/main/kotlin/io/libp2p/crypto/keys/Ecdsa.kt @@ -91,6 +91,10 @@ class EcdsaPublicKey(val pub: JavaECPublicKey) : PubKey(Crypto.KeyType.ECDSA) { override fun raw(): ByteArray = pub.encoded + fun javaKey(): JavaECPublicKey { + return pub + } + fun toUncompressedBytes(): ByteArray = byteArrayOf(0x04) + pub.w.affineX.toBytes(32) + pub.w.affineY.toBytes(32) diff --git a/libp2p/src/main/kotlin/io/libp2p/multistream/ProtocolSelect.kt b/libp2p/src/main/kotlin/io/libp2p/multistream/ProtocolSelect.kt index 434216fa0..332d8f2b6 100644 --- a/libp2p/src/main/kotlin/io/libp2p/multistream/ProtocolSelect.kt +++ b/libp2p/src/main/kotlin/io/libp2p/multistream/ProtocolSelect.kt @@ -23,7 +23,6 @@ class ProtocolSelect(val protocols: List() var activeFired = false - var userEvent = false override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { // when protocol data immediately follows protocol id in the same packet @@ -43,7 +42,6 @@ class ProtocolSelect(val protocols: List { val protocolBinding = protocols.find { it.protocolDescriptor.protocolMatcher.matches(evt.proto) } diff --git a/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseSecureChannelSession.kt b/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseSecureChannelSession.kt index b6e5485ef..bdf032a57 100644 --- a/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseSecureChannelSession.kt +++ b/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseSecureChannelSession.kt @@ -11,4 +11,4 @@ class NoiseSecureChannelSession( remotePubKey: PubKey, val aliceCipher: CipherState, val bobCipher: CipherState -) : SecureChannel.Session(localId, remoteId, remotePubKey) +) : SecureChannel.Session(localId, remoteId, remotePubKey, null) diff --git a/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXSecureChannel.kt b/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXSecureChannel.kt index c894a366c..9bb407ef6 100644 --- a/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXSecureChannel.kt +++ b/libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXSecureChannel.kt @@ -11,6 +11,7 @@ import io.libp2p.core.crypto.PubKey import io.libp2p.core.crypto.marshalPublicKey import io.libp2p.core.crypto.unmarshalPublicKey import io.libp2p.core.multistream.ProtocolDescriptor +import io.libp2p.core.mux.StreamMuxer import io.libp2p.core.security.SecureChannel import io.libp2p.etc.REMOTE_PEER_ID import io.libp2p.etc.types.toByteArray @@ -48,6 +49,9 @@ class UShortLengthCodec : CombinedChannelDuplexHandler) : this(localKey) + companion object { const val protocolName = "Noise_XX_25519_ChaChaPoly_SHA256" const val announce = "/noise" @@ -63,10 +67,6 @@ class NoiseXXSecureChannel(private val localKey: PrivKey) : override val protocolDescriptor = ProtocolDescriptor(announce) - fun initChannel(ch: P2PChannel): CompletableFuture { - return initChannel(ch, "") - } // initChannel - override fun initChannel( ch: P2PChannel, selectedProtocol: String diff --git a/libp2p/src/main/kotlin/io/libp2p/security/plaintext/PlaintextInsecureChannel.kt b/libp2p/src/main/kotlin/io/libp2p/security/plaintext/PlaintextInsecureChannel.kt index 66518ef18..a75c71ce3 100644 --- a/libp2p/src/main/kotlin/io/libp2p/security/plaintext/PlaintextInsecureChannel.kt +++ b/libp2p/src/main/kotlin/io/libp2p/security/plaintext/PlaintextInsecureChannel.kt @@ -9,6 +9,7 @@ import io.libp2p.core.crypto.PrivKey import io.libp2p.core.crypto.PubKey import io.libp2p.core.crypto.unmarshalPublicKey import io.libp2p.core.multistream.ProtocolDescriptor +import io.libp2p.core.mux.StreamMuxer import io.libp2p.core.security.SecureChannel import io.libp2p.etc.types.toProtobuf import io.libp2p.security.InvalidInitialPacket @@ -23,6 +24,10 @@ import plaintext.pb.Plaintext import java.util.concurrent.CompletableFuture class PlaintextInsecureChannel(private val localKey: PrivKey) : SecureChannel { + + @Suppress("UNUSED_PARAMETER") + constructor(localKey: PrivKey, muxerProtocols: List) : this(localKey) + override val protocolDescriptor = ProtocolDescriptor("/plaintext/2.0.0") override fun initChannel(ch: P2PChannel, selectedProtocol: String): CompletableFuture { @@ -107,7 +112,8 @@ class PlaintextHandshakeHandler( val session = SecureChannel.Session( localPeerId, remotePeerId, - remotePubKey + remotePubKey, + null ) handshakeCompleted.complete(session) diff --git a/libp2p/src/main/kotlin/io/libp2p/security/secio/SecIoSecureChannel.kt b/libp2p/src/main/kotlin/io/libp2p/security/secio/SecIoSecureChannel.kt index c0a026d54..ae049e9bf 100644 --- a/libp2p/src/main/kotlin/io/libp2p/security/secio/SecIoSecureChannel.kt +++ b/libp2p/src/main/kotlin/io/libp2p/security/secio/SecIoSecureChannel.kt @@ -5,6 +5,7 @@ import io.libp2p.core.P2PChannel import io.libp2p.core.PeerId import io.libp2p.core.crypto.PrivKey import io.libp2p.core.multistream.ProtocolDescriptor +import io.libp2p.core.mux.StreamMuxer import io.libp2p.core.security.SecureChannel import io.libp2p.etc.REMOTE_PEER_ID import io.netty.buffer.ByteBuf @@ -19,6 +20,10 @@ private val log = LoggerFactory.getLogger(SecIoSecureChannel::class.java) private val HandshakeHandlerName = "SecIoHandshake" class SecIoSecureChannel(private val localKey: PrivKey) : SecureChannel { + + @Suppress("UNUSED_PARAMETER") + constructor(localKey: PrivKey, muxerProtocols: List) : this(localKey) + override val protocolDescriptor = ProtocolDescriptor("/secio/1.0.0") override fun initChannel(ch: P2PChannel, selectedProtocol: String): CompletableFuture { @@ -68,7 +73,8 @@ private class SecIoHandshake( val session = SecureChannel.Session( PeerId.fromPubKey(secIoCodec.local.permanentPubKey), PeerId.fromPubKey(secIoCodec.remote.permanentPubKey), - secIoCodec.remote.permanentPubKey + secIoCodec.remote.permanentPubKey, + null ) handshakeComplete.complete(session) ctx.channel().pipeline().remove(HandshakeHandlerName) diff --git a/libp2p/src/main/kotlin/io/libp2p/security/tls/TLSSecureChannel.kt b/libp2p/src/main/kotlin/io/libp2p/security/tls/TLSSecureChannel.kt new file mode 100644 index 000000000..4d162fc9e --- /dev/null +++ b/libp2p/src/main/kotlin/io/libp2p/security/tls/TLSSecureChannel.kt @@ -0,0 +1,343 @@ +package io.libp2p.security.tls + +import crypto.pb.Crypto +import io.libp2p.core.* +import io.libp2p.core.crypto.PrivKey +import io.libp2p.core.crypto.PubKey +import io.libp2p.core.crypto.unmarshalPublicKey +import io.libp2p.core.multistream.ProtocolBinding +import io.libp2p.core.multistream.ProtocolDescriptor +import io.libp2p.core.multistream.ProtocolId +import io.libp2p.core.mux.NegotiatedStreamMuxer +import io.libp2p.core.mux.StreamMuxer +import io.libp2p.core.security.SecureChannel +import io.libp2p.crypto.Libp2pCrypto +import io.libp2p.crypto.keys.EcdsaPublicKey +import io.libp2p.crypto.keys.Ed25519PublicKey +import io.libp2p.crypto.keys.generateEcdsaKeyPair +import io.libp2p.crypto.keys.generateEd25519KeyPair +import io.libp2p.etc.REMOTE_PEER_ID +import io.libp2p.security.InvalidRemotePubKey +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.handler.ssl.ApplicationProtocolConfig +import io.netty.handler.ssl.ClientAuth +import io.netty.handler.ssl.SslContextBuilder +import io.netty.handler.ssl.SslHandler +import org.bouncycastle.asn1.* +import org.bouncycastle.asn1.edec.EdECObjectIdentifiers +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.AlgorithmIdentifier +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters +import org.bouncycastle.jcajce.interfaces.EdDSAPublicKey +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import java.math.BigInteger +import java.security.KeyFactory +import java.security.PrivateKey +import java.security.PublicKey +import java.security.Security +import java.security.cert.Certificate +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import java.security.interfaces.ECPublicKey +import java.security.spec.* +import java.time.Instant +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.logging.Level +import java.util.logging.Logger +import javax.net.ssl.X509TrustManager + +private val log = Logger.getLogger(TlsSecureChannel::class.java.name) + +const val NoEarlyMuxerNegotiationEntry = "libp2p" +const val SetupHandlerName = "TlsSetup" +val certificatePrefix = "libp2p-tls-handshake:".encodeToByteArray() + +class TlsSecureChannel(private val localKey: PrivKey, private val muxers: List, private val certAlgorithm: String) : + SecureChannel { + + constructor(localKey: PrivKey, muxerIds: List) : this(localKey, muxerIds, "Ed25519") {} + + companion object { + const val announce = "/tls/1.0.0" + init { + Security.insertProviderAt(Libp2pCrypto.provider, 1) + Security.insertProviderAt(BouncyCastleJsseProvider(), 2) + } + } + + override val protocolDescriptor = ProtocolDescriptor(announce) + + fun initChannel(ch: P2PChannel): CompletableFuture { + return initChannel(ch, "") + } + + override fun initChannel( + ch: P2PChannel, + selectedProtocol: String + ): CompletableFuture { + val handshakeComplete = CompletableFuture() + ch.pushHandler(SetupHandlerName, ChannelSetup(localKey, muxers, certAlgorithm, ch, handshakeComplete)) + return handshakeComplete + } +} + +fun buildTlsHandler( + localKey: PrivKey, + expectedRemotePeer: Optional, + muxers: List, + certAlgorithm: String, + ch: P2PChannel, + handshakeComplete: CompletableFuture, + ctx: ChannelHandlerContext +): SslHandler { + val connectionKeys = if (certAlgorithm.equals("ECDSA")) generateEcdsaKeyPair() else generateEd25519KeyPair() + val javaPrivateKey = getJavaKey(connectionKeys.first) + val sslContext = ( + if (ch.isInitiator) + SslContextBuilder.forClient().keyManager(javaPrivateKey, listOf(buildCert(localKey, connectionKeys.first))) + else + SslContextBuilder.forServer(javaPrivateKey, listOf(buildCert(localKey, connectionKeys.first))) + ) + .protocols(listOf("TLSv1.3")) + .ciphers(listOf("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256")) + .clientAuth(ClientAuth.REQUIRE) + .trustManager(Libp2pTrustManager(expectedRemotePeer)) + .sslContextProvider(BouncyCastleJsseProvider()) + .applicationProtocolConfig( + ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.FATAL_ALERT, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.FATAL_ALERT, + muxers.allProtocols + NoEarlyMuxerNegotiationEntry // early muxer negotiation + ) + ) + .build() + val handler = sslContext.newHandler(ctx.alloc()) + handler.sslCloseFuture().addListener { _ -> ctx.close() } + val handshake = handler.handshakeFuture() + val engine = handler.engine() + handshake.addListener { fut -> + if (! fut.isSuccess) { + var cause = fut.cause() + if (cause != null && cause.cause != null) + cause = cause.cause + handshakeComplete.completeExceptionally(cause) + } else { + val negotiatedProtocols = sslContext.applicationProtocolNegotiator().protocols() + val selectedMuxer = muxers.findBestMatch(negotiatedProtocols) + handshakeComplete.complete( + SecureChannel.Session( + PeerId.fromPubKey(localKey.publicKey()), + verifyAndExtractPeerId(engine.session.peerCertificates), + getPublicKeyFromCert(engine.session.peerCertificates), + selectedMuxer + ) + ) + ctx.fireChannelActive() + } + } + return handler +} + +private val > List.allProtocols: List get() = + this.flatMap { it.protocolDescriptor.announceProtocols } + +private fun List.findBestMatch(remoteProtocols: List): NegotiatedStreamMuxer? = + this.firstNotNullOfOrNull { muxer -> + remoteProtocols.firstOrNull { remoteProtocol -> + muxer.protocolDescriptor.protocolMatcher.matches(remoteProtocol) + }?.let { negotiatedProtocol -> + NegotiatedStreamMuxer(muxer, negotiatedProtocol) + } + } + +private class ChannelSetup( + private val localKey: PrivKey, + private val muxers: List, + private val certAlgorithm: String, + private val ch: P2PChannel, + private val handshakeComplete: CompletableFuture +) : SimpleChannelInboundHandler() { + private var activated = false + + override fun channelActive(ctx: ChannelHandlerContext) { + if (! activated) { + activated = true + val expectedRemotePeerId = ctx.channel().attr(REMOTE_PEER_ID).get() + ctx.channel().pipeline().addLast( + buildTlsHandler( + localKey, + Optional.ofNullable(expectedRemotePeerId), + muxers, + certAlgorithm, + ch, + handshakeComplete, + ctx + ) + ) + ctx.channel().pipeline().remove(SetupHandlerName) + } + } + + override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) { + // it seems there is no guarantee from Netty that channelActive() must be called before channelRead() + channelActive(ctx) + ctx.fireChannelRead(msg) + ctx.fireChannelActive() + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + handshakeComplete.completeExceptionally(cause) + log.log(Level.FINE, "TLS setup failed", cause) + ctx.channel().close() + } + + override fun channelUnregistered(ctx: ChannelHandlerContext) { + handshakeComplete.completeExceptionally(ConnectionClosedException("Connection was closed ${ctx.channel()}")) + super.channelUnregistered(ctx) + } +} + +class Libp2pTrustManager(private val expectedRemotePeer: Optional) : X509TrustManager { + override fun checkClientTrusted(certs: Array?, authType: String?) { + if (certs?.size != 1) + throw CertificateException() + val claimedPeerId = verifyAndExtractPeerId(arrayOf(certs.get(0))) + if (expectedRemotePeer.map { ex -> ! ex.equals(claimedPeerId) }.orElse(false)) + throw InvalidRemotePubKey() + } + + override fun checkServerTrusted(certs: Array?, authType: String?) { + return checkClientTrusted(certs, authType) + } + + override fun getAcceptedIssuers(): Array { + return arrayOf() + } +} + +fun getJavaKey(priv: PrivKey): PrivateKey { + if (priv.keyType == Crypto.KeyType.Ed25519) { + val kf = KeyFactory.getInstance("Ed25519", Libp2pCrypto.provider) + val privKeyInfo = + PrivateKeyInfo(AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), DEROctetString(priv.raw())) + val pkcs8KeySpec = PKCS8EncodedKeySpec(privKeyInfo.encoded) + return kf.generatePrivate(pkcs8KeySpec) + } + if (priv.keyType == Crypto.KeyType.ECDSA) { + val kf = KeyFactory.getInstance("ECDSA", Libp2pCrypto.provider) + val pkcs8KeySpec = PKCS8EncodedKeySpec(priv.raw()) + return kf.generatePrivate(pkcs8KeySpec) + } + + if (priv.keyType == Crypto.KeyType.RSA) { + throw IllegalStateException("Unimplemented RSA key support for TLS") + } + throw IllegalArgumentException("Unsupported TLS key type:" + priv.keyType) +} + +fun getAsn1EncodedPublicKey(pub: PubKey): ByteArray { + if (pub.keyType == Crypto.KeyType.Ed25519) { + return SubjectPublicKeyInfo(AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), pub.raw()).encoded + } + if (pub.keyType == Crypto.KeyType.ECDSA) { + return (pub as EcdsaPublicKey).javaKey().encoded + } + throw IllegalArgumentException("Unsupported TLS key type:" + pub.keyType) +} + +fun getPubKey(pub: PublicKey): PubKey { + if (pub.algorithm.equals("EdDSA") || pub.algorithm.equals("Ed25519")) { + val raw = (pub as EdDSAPublicKey).pointEncoding + return Ed25519PublicKey(Ed25519PublicKeyParameters(raw)) + } + if (pub.algorithm.equals("EC")) { + return EcdsaPublicKey(pub as ECPublicKey) + } + if (pub.algorithm.equals("RSA")) + throw IllegalStateException("Unimplemented RSA public key support for TLS") + throw IllegalStateException("Unsupported key type: " + pub.algorithm) +} + +fun verifyAndExtractPeerId(chain: Array): PeerId { + if (chain.size != 1) + throw java.lang.IllegalStateException("Cert chain must have exactly 1 element!") + val cert = chain.get(0) + // peerid is in the certificate extension + val bcCert = org.bouncycastle.asn1.x509.Certificate + .getInstance(ASN1Primitive.fromByteArray(cert.getEncoded())) + val bcX509Cert = X509CertificateHolder(bcCert) + val libp2pOid = ASN1ObjectIdentifier("1.3.6.1.4.1.53594.1.1") + val extension = bcX509Cert.extensions.getExtension(libp2pOid) + if (extension == null) + throw IllegalStateException("Certificate extension not present!") + val input = ASN1InputStream(extension.extnValue.encoded) + val wrapper = input.readObject() as DEROctetString + val seq = ASN1InputStream(wrapper.octets).readObject() as DLSequence + val pubKeyProto = (seq.getObjectAt(0) as DEROctetString).octets + val signature = (seq.getObjectAt(1) as DEROctetString).octets + val pubKey = unmarshalPublicKey(pubKeyProto) + if (! pubKey.verify(certificatePrefix.plus(cert.publicKey.encoded), signature)) + throw IllegalStateException("Invalid signature on TLS certificate extension!") + + cert.verify(cert.publicKey) + val now = Date() + if (bcCert.endDate.date.before(now)) + throw IllegalStateException("TLS certificate has expired!") + if (bcCert.startDate.date.after(now)) + throw IllegalStateException("TLS certificate is not valid yet!") + return PeerId.fromPubKey(pubKey) +} + +fun getPublicKeyFromCert(chain: Array): PubKey { + if (chain.size != 1) + throw java.lang.IllegalStateException("Cert chain must have exactly 1 element!") + val cert = chain.get(0) + return getPubKey(cert.publicKey) +} + +/** Build a self signed cert, with an extension containing the host key + sig(cert public key) + * + */ +fun buildCert(hostKey: PrivKey, subjectKey: PrivKey): X509Certificate { + val publicKeyAsn1 = getAsn1EncodedPublicKey(subjectKey.publicKey()) + val subPubKeyInfo = SubjectPublicKeyInfo.getInstance(publicKeyAsn1) + + val now = Instant.now() + val validFrom = Date.from(now.minusSeconds(3600)) + val oneYear = 60L * 60 * 24 * 365 + val validTo = Date.from(now.plusSeconds(oneYear)) + val issuer = X500Name("O=Peergos,L=Oxford,C=UK") + val subject = issuer + + val signature = hostKey.sign(certificatePrefix.plus(publicKeyAsn1)) + val hostPublicProto = hostKey.publicKey().bytes() + val extension = DERSequence(arrayOf(DEROctetString(hostPublicProto), DEROctetString(signature))) + + var certBuilder = X509v3CertificateBuilder( + issuer, + BigInteger.valueOf(now.toEpochMilli()), + validFrom, + validTo, + subject, + subPubKeyInfo + ).addExtension(ASN1ObjectIdentifier("1.3.6.1.4.1.53594.1.1"), true, extension) + val sigAlg = when (subjectKey.keyType) { + Crypto.KeyType.Ed25519 -> "Ed25519" + Crypto.KeyType.ECDSA -> "SHA256withECDSA" + else -> throw IllegalStateException("Unsupported certificate key type: " + subjectKey.keyType) + } + val signer = JcaContentSignerBuilder(sigAlg) + .setProvider(Libp2pCrypto.provider) + .build(getJavaKey(subjectKey)) + return JcaX509CertificateConverter().getCertificate(certBuilder.build(signer)) +} diff --git a/libp2p/src/main/kotlin/io/libp2p/transport/ConnectionUpgrader.kt b/libp2p/src/main/kotlin/io/libp2p/transport/ConnectionUpgrader.kt index 9aa6776d0..4df94b197 100644 --- a/libp2p/src/main/kotlin/io/libp2p/transport/ConnectionUpgrader.kt +++ b/libp2p/src/main/kotlin/io/libp2p/transport/ConnectionUpgrader.kt @@ -3,8 +3,12 @@ package io.libp2p.transport import io.libp2p.core.Connection import io.libp2p.core.multistream.MultistreamProtocol import io.libp2p.core.multistream.ProtocolBinding +import io.libp2p.core.mux.NegotiatedStreamMuxer import io.libp2p.core.mux.StreamMuxer import io.libp2p.core.security.SecureChannel +import io.libp2p.etc.getP2PChannel +import io.libp2p.etc.types.forward +import io.libp2p.etc.util.netty.nettyInitializer import java.util.concurrent.CompletableFuture /** @@ -31,14 +35,26 @@ open class ConnectionUpgrader( connection, muxers ) - } // establishMuxer + } private fun , R> establish( multistreamProtocol: MultistreamProtocol, connection: Connection, - channels: List + bindings: List ): CompletableFuture { - val multistream = multistreamProtocol.createMultistream(channels) + val multistream = multistreamProtocol.createMultistream(bindings) return multistream.initChannel(connection) } // establish + + companion object { + fun establishMuxer(muxer: NegotiatedStreamMuxer, connection: Connection): CompletableFuture { + val res = CompletableFuture() + connection.pushHandler( + nettyInitializer { + muxer.initChannel(it.channel.getP2PChannel()).forward(res) + } + ) + return res + } + } } // ConnectionUpgrader diff --git a/libp2p/src/main/kotlin/io/libp2p/transport/implementation/ConnectionBuilder.kt b/libp2p/src/main/kotlin/io/libp2p/transport/implementation/ConnectionBuilder.kt index b4efd1073..960c94f33 100644 --- a/libp2p/src/main/kotlin/io/libp2p/transport/implementation/ConnectionBuilder.kt +++ b/libp2p/src/main/kotlin/io/libp2p/transport/implementation/ConnectionBuilder.kt @@ -32,7 +32,11 @@ class ConnectionBuilder( upgrader.establishSecureChannel(connection) .thenCompose { connection.setSecureSession(it) - upgrader.establishMuxer(connection) + if (it.earlyMuxer != null) { + ConnectionUpgrader.establishMuxer(it.earlyMuxer, connection) + } else { + upgrader.establishMuxer(connection) + } }.thenApply { connection.setMuxerSession(it) connHandler.handleConnection(connection) diff --git a/libp2p/src/test/java/io/libp2p/core/HostTestJava.java b/libp2p/src/test/java/io/libp2p/core/HostTestJava.java index b3e729112..c1284aa01 100644 --- a/libp2p/src/test/java/io/libp2p/core/HostTestJava.java +++ b/libp2p/src/test/java/io/libp2p/core/HostTestJava.java @@ -9,7 +9,7 @@ import io.libp2p.core.mux.StreamMuxerProtocol; import io.libp2p.protocol.Ping; import io.libp2p.protocol.PingController; -import io.libp2p.security.secio.SecIoSecureChannel; +import io.libp2p.security.tls.*; import io.libp2p.transport.tcp.TcpTransport; import kotlin.Pair; import org.junit.jupiter.api.Assertions; @@ -37,14 +37,14 @@ void ping() throws Exception { Host clientHost = new HostBuilder() .transport(TcpTransport::new) - .secureChannel(SecIoSecureChannel::new) - .muxer(StreamMuxerProtocol::getMplex) + .secureChannel(TlsSecureChannel::new) + .muxer(StreamMuxerProtocol::getYamux) .build(); Host serverHost = new HostBuilder() .transport(TcpTransport::new) - .secureChannel(SecIoSecureChannel::new) - .muxer(StreamMuxerProtocol::getMplex) + .secureChannel(TlsSecureChannel::new) + .muxer(StreamMuxerProtocol::getYamux) .protocol(new Ping()) .listen(localListenAddress) .build(); diff --git a/libp2p/src/test/kotlin/io/libp2p/core/dsl/BuilderDefaultsTest.kt b/libp2p/src/test/kotlin/io/libp2p/core/dsl/BuilderDefaultsTest.kt index e2609512b..f7bc79b7a 100644 --- a/libp2p/src/test/kotlin/io/libp2p/core/dsl/BuilderDefaultsTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/core/dsl/BuilderDefaultsTest.kt @@ -46,7 +46,7 @@ class BuilderDefaultsTest { host(Builder.Defaults.None) { identity { random() } transports { +::TcpTransport } - secureChannels { +::SecIoSecureChannel } + secureChannels { add(::SecIoSecureChannel) } } } } @@ -56,7 +56,7 @@ class BuilderDefaultsTest { val host = host(Builder.Defaults.None) { identity { random() } transports { +::TcpTransport } - secureChannels { +::SecIoSecureChannel } + secureChannels { add(::SecIoSecureChannel) } muxers { + StreamMuxerProtocol.Mplex } } diff --git a/libp2p/src/test/kotlin/io/libp2p/crypto/KeyTypesTest.kt b/libp2p/src/test/kotlin/io/libp2p/crypto/KeyTypesTest.kt new file mode 100644 index 000000000..9a5b5805a --- /dev/null +++ b/libp2p/src/test/kotlin/io/libp2p/crypto/KeyTypesTest.kt @@ -0,0 +1,43 @@ +package io.libp2p.crypto + +import io.libp2p.crypto.keys.generateEcdsaKeyPair +import io.libp2p.crypto.keys.generateEd25519KeyPair +import io.libp2p.crypto.keys.generateRsaKeyPair +import io.libp2p.crypto.keys.generateSecp256k1KeyPair +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class KeyTypesTest { + + @Test + fun ed25519() { + val pair = generateEd25519KeyPair() + val toSign = "G'day!".toByteArray() + val signed = pair.first.sign(toSign) + assertTrue(pair.second.verify(toSign, signed)) + } + + @Test + fun rsa() { + val pair = generateRsaKeyPair(2048) + val toSign = "G'day!".toByteArray() + val signed = pair.first.sign(toSign) + assertTrue(pair.second.verify(toSign, signed)) + } + + @Test + fun secp256k1() { + val pair = generateSecp256k1KeyPair() + val toSign = "G'day!".toByteArray() + val signed = pair.first.sign(toSign) + assertTrue(pair.second.verify(toSign, signed)) + } + + @Test + fun ecdsa() { + val pair = generateEcdsaKeyPair() // p-256 + val toSign = "G'day!".toByteArray() + val signed = pair.first.sign(toSign) + assertTrue(pair.second.verify(toSign, signed)) + } +} diff --git a/libp2p/src/test/kotlin/io/libp2p/security/CipherSecureChannelTest.kt b/libp2p/src/test/kotlin/io/libp2p/security/CipherSecureChannelTest.kt index 7d0cd9bcc..b466f04e6 100644 --- a/libp2p/src/test/kotlin/io/libp2p/security/CipherSecureChannelTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/security/CipherSecureChannelTest.kt @@ -3,6 +3,7 @@ package io.libp2p.security import io.libp2p.core.PeerId import io.libp2p.core.crypto.KEY_TYPE import io.libp2p.core.crypto.generateKeyPair +import io.libp2p.core.mux.StreamMuxer import io.libp2p.tools.TestChannel import io.libp2p.tools.TestLogAppender import io.netty.buffer.Unpooled @@ -12,8 +13,8 @@ import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test import java.util.concurrent.TimeUnit.SECONDS -abstract class CipherSecureChannelTest(secureChannelCtor: SecureChannelCtor, announce: String) : - SecureChannelTestBase(secureChannelCtor, announce) { +abstract class CipherSecureChannelTest(secureChannelCtor: SecureChannelCtor, muxers: List, announce: String) : + SecureChannelTestBase(secureChannelCtor, muxers, announce) { @Test fun `incorrect initiator remote PeerId should throw`() { @@ -21,8 +22,8 @@ abstract class CipherSecureChannelTest(secureChannelCtor: SecureChannelCtor, ann val (privKey2, _) = generateKeyPair(KEY_TYPE.ECDSA) val (_, wrongPubKey) = generateKeyPair(KEY_TYPE.ECDSA) - val protocolSelect1 = makeSelector(privKey1) - val protocolSelect2 = makeSelector(privKey2) + val protocolSelect1 = makeSelector(privKey1, muxerIds) + val protocolSelect2 = makeSelector(privKey2, muxerIds) val eCh1 = makeDialChannel("#1", protocolSelect1, PeerId.fromPubKey(wrongPubKey)) val eCh2 = makeListenChannel("#2", protocolSelect2) @@ -39,8 +40,8 @@ abstract class CipherSecureChannelTest(secureChannelCtor: SecureChannelCtor, ann val (privKey1, _) = generateKeyPair(KEY_TYPE.ECDSA) val (privKey2, pubKey2) = generateKeyPair(KEY_TYPE.ECDSA) - val protocolSelect1 = makeSelector(privKey1) - val protocolSelect2 = makeSelector(privKey2) + val protocolSelect1 = makeSelector(privKey1, muxerIds) + val protocolSelect2 = makeSelector(privKey2, muxerIds) val eCh1 = makeDialChannel("#1", protocolSelect1, PeerId.fromPubKey(pubKey2)) val eCh2 = makeListenChannel("#2", protocolSelect2) diff --git a/libp2p/src/test/kotlin/io/libp2p/security/SecureChannelTestBase.kt b/libp2p/src/test/kotlin/io/libp2p/security/SecureChannelTestBase.kt index cf69d61fb..a91651a75 100644 --- a/libp2p/src/test/kotlin/io/libp2p/security/SecureChannelTestBase.kt +++ b/libp2p/src/test/kotlin/io/libp2p/security/SecureChannelTestBase.kt @@ -5,6 +5,7 @@ import io.libp2p.core.crypto.KEY_TYPE import io.libp2p.core.crypto.PrivKey import io.libp2p.core.crypto.generateKeyPair import io.libp2p.core.multistream.ProtocolMatcher +import io.libp2p.core.mux.StreamMuxer import io.libp2p.core.security.SecureChannel import io.libp2p.etc.types.seconds import io.libp2p.etc.types.toByteArray @@ -26,12 +27,13 @@ import java.nio.charset.StandardCharsets import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit -typealias SecureChannelCtor = (PrivKey) -> SecureChannel +typealias SecureChannelCtor = (PrivKey, List) -> SecureChannel val logger = LoggerFactory.getLogger(SecureChannelTestBase::class.java) abstract class SecureChannelTestBase( val secureChannelCtor: SecureChannelCtor, + val muxerIds: List, val announce: String ) { init { @@ -56,8 +58,8 @@ abstract class SecureChannelTestBase( val (privKey1, _) = generateKeyPair(KEY_TYPE.ECDSA) val (privKey2, pubKey2) = generateKeyPair(KEY_TYPE.ECDSA) - val protocolSelect1 = makeSelector(privKey1) - val protocolSelect2 = makeSelector(privKey2) + val protocolSelect1 = makeSelector(privKey1, muxerIds) + val protocolSelect2 = makeSelector(privKey2, muxerIds) val eCh1 = makeDialChannel("#1", protocolSelect1, PeerId.fromPubKey(pubKey2)) val eCh2 = makeListenChannel("#2", protocolSelect2) @@ -117,7 +119,7 @@ abstract class SecureChannelTestBase( } } // secureInterconnect - protected fun makeSelector(key: PrivKey) = ProtocolSelect(listOf(secureChannelCtor(key))) + protected fun makeSelector(key: PrivKey, muxers: List) = ProtocolSelect(listOf(secureChannelCtor(key, muxers))) protected fun makeDialChannel( name: String, diff --git a/libp2p/src/test/kotlin/io/libp2p/security/noise/NoiseHandshakeTest.kt b/libp2p/src/test/kotlin/io/libp2p/security/noise/NoiseHandshakeTest.kt index a2dfb0282..a6f6454b2 100644 --- a/libp2p/src/test/kotlin/io/libp2p/security/noise/NoiseHandshakeTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/security/noise/NoiseHandshakeTest.kt @@ -146,7 +146,7 @@ class NoiseHandshakeTest { fun testAnnounceAndMatch() { val (privKey1, _) = generateKeyPair(KEY_TYPE.ECDSA) - val ch1 = NoiseXXSecureChannel(privKey1) + val ch1 = NoiseXXSecureChannel(privKey1, listOf()) Assertions.assertTrue( ch1.protocolDescriptor.matchesAny(ch1.protocolDescriptor.announceProtocols) @@ -156,11 +156,11 @@ class NoiseHandshakeTest { @Test fun testStaticNoiseKeyPerProcess() { val (privKey1, _) = generateKeyPair(KEY_TYPE.ECDSA) - NoiseXXSecureChannel(privKey1) + NoiseXXSecureChannel(privKey1, listOf()) val b1 = NoiseXXSecureChannel.localStaticPrivateKey25519.copyOf() val (privKey2, _) = generateKeyPair(KEY_TYPE.ECDSA) - NoiseXXSecureChannel(privKey2) + NoiseXXSecureChannel(privKey2, listOf()) val b2 = NoiseXXSecureChannel.localStaticPrivateKey25519.copyOf() Assertions.assertTrue(b1.contentEquals(b2), "NoiseXX static keys are not maintained between sessions.") diff --git a/libp2p/src/test/kotlin/io/libp2p/security/noise/NoiseSecureChannelTest.kt b/libp2p/src/test/kotlin/io/libp2p/security/noise/NoiseSecureChannelTest.kt index 031144ba5..b08ad60a2 100644 --- a/libp2p/src/test/kotlin/io/libp2p/security/noise/NoiseSecureChannelTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/security/noise/NoiseSecureChannelTest.kt @@ -8,5 +8,6 @@ import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable @Tag("secure-channel") class NoiseSecureChannelTest : CipherSecureChannelTest( ::NoiseXXSecureChannel, + listOf(), NoiseXXSecureChannel.announce ) diff --git a/libp2p/src/test/kotlin/io/libp2p/security/plaintext/PlaintextInsecureChannelTest.kt b/libp2p/src/test/kotlin/io/libp2p/security/plaintext/PlaintextInsecureChannelTest.kt index d16e41c82..a3b751ab8 100644 --- a/libp2p/src/test/kotlin/io/libp2p/security/plaintext/PlaintextInsecureChannelTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/security/plaintext/PlaintextInsecureChannelTest.kt @@ -6,5 +6,6 @@ import org.junit.jupiter.api.Tag @Tag("secure-channel") class PlaintextInsecureChannelTest : SecureChannelTestBase( ::PlaintextInsecureChannel, + listOf(), "/plaintext/2.0.0" ) diff --git a/libp2p/src/test/kotlin/io/libp2p/security/secio/EchoSampleTest.kt b/libp2p/src/test/kotlin/io/libp2p/security/secio/EchoSampleTest.kt index 595926ad4..f1c8b097c 100644 --- a/libp2p/src/test/kotlin/io/libp2p/security/secio/EchoSampleTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/security/secio/EchoSampleTest.kt @@ -63,7 +63,7 @@ class EchoSampleTest { } val upgrader = ConnectionUpgrader( MultistreamProtocolV1.copyWithHandlers(nettyToChannelHandler(LoggingHandler("#1", LogLevel.INFO))), - listOf(SecIoSecureChannel(privKey1)), + listOf(SecIoSecureChannel(privKey1, listOf())), MultistreamProtocolV1.copyWithHandlers(nettyToChannelHandler(LoggingHandler("#2", LogLevel.INFO))), listOf(muxer) ) diff --git a/libp2p/src/test/kotlin/io/libp2p/security/secio/SecIoSecureChannelTest.kt b/libp2p/src/test/kotlin/io/libp2p/security/secio/SecIoSecureChannelTest.kt index 96eefa6bf..4df5314f3 100644 --- a/libp2p/src/test/kotlin/io/libp2p/security/secio/SecIoSecureChannelTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/security/secio/SecIoSecureChannelTest.kt @@ -6,5 +6,6 @@ import org.junit.jupiter.api.Tag @Tag("secure-channel") class SecIoSecureChannelTest : CipherSecureChannelTest( ::SecIoSecureChannel, + listOf(), "/secio/1.0.0" ) diff --git a/libp2p/src/test/kotlin/io/libp2p/security/tls/CertificatesTest.kt b/libp2p/src/test/kotlin/io/libp2p/security/tls/CertificatesTest.kt new file mode 100644 index 000000000..feb2e6a98 --- /dev/null +++ b/libp2p/src/test/kotlin/io/libp2p/security/tls/CertificatesTest.kt @@ -0,0 +1,66 @@ +package io.libp2p.security.tls + +import io.libp2p.core.PeerId +import io.libp2p.crypto.keys.generateEd25519KeyPair +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.util.encoders.Hex +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class CertificatesTest { + + @Test + fun ed25519Peer() { + val hex = "308201773082011ea003020102020900f5bd0debaa597f52300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d030107034200046bf9871220d71dcb3483ecdfcbfcc7c103f8509d0974b3c18ab1f1be1302d643103a08f7a7722c1b247ba3876fe2c59e26526f479d7718a85202ddbe47562358a37f307d307b060a2b0601040183a25a01010101ff046a30680424080112207fda21856709c5ae12fd6e8450623f15f11955d384212b89f56e7e136d2e17280440aaa6bffabe91b6f30c35e3aa4f94b1188fed96b0ffdd393f4c58c1c047854120e674ce64c788406d1c2c4b116581fd7411b309881c3c7f20b46e54c7e6fe7f0f300a06082a8648ce3d040302034700304402207d1a1dbd2bda235ff2ec87daf006f9b04ba076a5a5530180cd9c2e8f6399e09d0220458527178c7e77024601dbb1b256593e9b96d961b96349d1f560114f61a87595" + val certBytes = Hex.decode(hex) + val certHolder = X509CertificateHolder(certBytes) + val cert = JcaX509CertificateConverter().setProvider(BouncyCastleProvider()).getCertificate(certHolder) + val peerIdFromCert = verifyAndExtractPeerId(arrayOf(cert)) + val expectedPeerId = PeerId.fromBase58("12D3KooWJRSrypvnpHgc6ZAgyCni4KcSmbV7uGRaMw5LgMKT18fq") + assertEquals(peerIdFromCert, expectedPeerId) + } + + @Test + fun ecdsaPeer() { + val hex = "308201c030820166a003020102020900eaf419a6e3edb4a6300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d030107034200048dbf1116c7c608d6d5292bd826c3feb53483a89fce434bf64538a359c8e07538ff71f6766239be6a146dcc1a5f3bb934bcd4ae2ae1d4da28ac68b4a20593f06ba381c63081c33081c0060a2b0601040183a25a01010101ff0481ae3081ab045f0803125b3059301306072a8648ce3d020106082a8648ce3d0301070342000484b93fa456a74bd0153919f036db7bc63c802f055bc7023395d0203de718ee0fc7b570b767cdd858aca6c7c4113ff002e78bd2138ac1a3b26dde3519e06979ad04483046022100bc84014cea5a41feabdf4c161096564b9ccf4b62fbef4fe1cd382c84e11101780221009204f086a84cb8ed8a9ddd7868dc90c792ee434adf62c66f99a08a5eba11615b300a06082a8648ce3d0403020348003045022054b437be9a2edf591312d68ff24bf91367ad4143f76cf80b5658f232ade820da022100e23b48de9df9c25d4c83ddddf75d2676f0b9318ee2a6c88a736d85eab94a912f" + val certBytes = Hex.decode(hex) + val certHolder = X509CertificateHolder(certBytes) + val cert = JcaX509CertificateConverter().setProvider(BouncyCastleProvider()).getCertificate(certHolder) + val peerIdFromCert = verifyAndExtractPeerId(arrayOf(cert)) + val expectedPeerId = PeerId.fromBase58("QmZcrvr3r4S3QvwFdae3c2EWTfo792Y14UpzCZurhmiWeX") + assertEquals(peerIdFromCert, expectedPeerId) + } + + @Test + fun secp256k1Peer() { + val hex = "3082018230820128a003020102020900f3b305f55622cfdf300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d0301070342000458f7e9581748ff9bdd933b655cc0e5552a1248f840658cc221dec2186b5a2fe4641b86ab7590a3422cdbb1000cf97662f27e5910d7569f22feed8829c8b52e0fa38188308185308182060a2b0601040183a25a01010101ff0471306f042508021221026b053094d1112bce799dc8026040ae6d4eb574157929f1598172061f753d9b1b04463044022040712707e97794c478d93989aaa28ae1f71c03af524a8a4bd2d98424948a782302207b61b7f074b696a25fb9e0059141a811cccc4cc28042d9301b9b2a4015e87470300a06082a8648ce3d04030203480030450220143ae4d86fdc8675d2480bb6912eca5e39165df7f572d836aa2f2d6acfab13f8022100831d1979a98f0c4a6fb5069ca374de92f1a1205c962a6d90ad3d7554cb7d9df4" + val certBytes = Hex.decode(hex) + val certHolder = X509CertificateHolder(certBytes) + val cert = JcaX509CertificateConverter().setProvider(BouncyCastleProvider()).getCertificate(certHolder) + val peerIdFromCert = verifyAndExtractPeerId(arrayOf(cert)) + val expectedPeerId = PeerId.fromBase58("16Uiu2HAm2dSCBFxuge46aEt7U1oejtYuBUZXxASHqmcfVmk4gsbx") + assertEquals(peerIdFromCert, expectedPeerId) + } + + @Test + fun invalidCert() { + val hex = "308201773082011da003020102020830a73c5d896a1109300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d03010703420004bbe62df9a7c1c46b7f1f21d556deec5382a36df146fb29c7f1240e60d7d5328570e3b71d99602b77a65c9b3655f62837f8d66b59f1763b8c9beba3be07778043a37f307d307b060a2b0601040183a25a01010101ff046a3068042408011220ec8094573afb9728088860864f7bcea2d4fd412fef09a8e2d24d482377c20db60440ecabae8354afa2f0af4b8d2ad871e865cb5a7c0c8d3dbdbf42de577f92461a0ebb0a28703e33581af7d2a4f2270fc37aec6261fcc95f8af08f3f4806581c730a300a06082a8648ce3d040302034800304502202dfb17a6fa0f94ee0e2e6a3b9fb6e986f311dee27392058016464bd130930a61022100ba4b937a11c8d3172b81e7cd04aedb79b978c4379c2b5b24d565dd5d67d3cb3c" + val certBytes = Hex.decode(hex) + val certHolder = X509CertificateHolder(certBytes) + val cert = JcaX509CertificateConverter().setProvider(BouncyCastleProvider()).getCertificate(certHolder) + assertThrows({ verifyAndExtractPeerId(arrayOf(cert)) }) + } + + @Test + fun buildEd25519Cert() { + val host = generateEd25519KeyPair() + val conn = generateEd25519KeyPair() + val cert = buildCert(host.first, conn.first) + val peerIdFromCert = verifyAndExtractPeerId(arrayOf(cert)) + val expectedPeerId = PeerId.fromPubKey(host.second) + assertEquals(peerIdFromCert, expectedPeerId) + } +} diff --git a/libp2p/src/test/kotlin/io/libp2p/security/tls/TlsSecureChannelTest.kt b/libp2p/src/test/kotlin/io/libp2p/security/tls/TlsSecureChannelTest.kt new file mode 100644 index 000000000..1d5fe5ed5 --- /dev/null +++ b/libp2p/src/test/kotlin/io/libp2p/security/tls/TlsSecureChannelTest.kt @@ -0,0 +1,42 @@ +package io.libp2p.security.tls + +import io.libp2p.core.PeerId +import io.libp2p.core.crypto.KEY_TYPE +import io.libp2p.core.crypto.generateKeyPair +import io.libp2p.core.multistream.MultistreamProtocolDebug +import io.libp2p.core.mux.StreamMuxerProtocol +import io.libp2p.multistream.MultistreamProtocolDebugV1 +import io.libp2p.security.InvalidRemotePubKey +import io.libp2p.security.SecureChannelTestBase +import io.libp2p.tools.TestChannel +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit + +val MultistreamProtocolV1: MultistreamProtocolDebug = MultistreamProtocolDebugV1() + +@Tag("secure-channel") +class TlsSecureChannelTest : SecureChannelTestBase( + ::TlsSecureChannel, + listOf(StreamMuxerProtocol.Yamux.createMuxer(MultistreamProtocolV1, listOf())), + TlsSecureChannel.announce +) { + @Test + fun `incorrect initiator remote PeerId should throw`() { + val (privKey1, _) = generateKeyPair(KEY_TYPE.ECDSA) + val (privKey2, _) = generateKeyPair(KEY_TYPE.ECDSA) + val (_, wrongPubKey) = generateKeyPair(KEY_TYPE.ECDSA) + + val protocolSelect1 = makeSelector(privKey1, muxerIds) + val protocolSelect2 = makeSelector(privKey2, muxerIds) + + val eCh1 = makeDialChannel("#1", protocolSelect1, PeerId.fromPubKey(wrongPubKey)) + val eCh2 = makeListenChannel("#2", protocolSelect2) + + TestChannel.interConnect(eCh1, eCh2) + + Assertions.assertThatThrownBy { protocolSelect1.selectedFuture.get(10, TimeUnit.SECONDS) } + .hasCauseInstanceOf(InvalidRemotePubKey::class.java) + } +} diff --git a/libp2p/src/testFixtures/kotlin/io/libp2p/pubsub/TestRouter.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/pubsub/TestRouter.kt index cf8fd7e7e..a90a9f45e 100644 --- a/libp2p/src/testFixtures/kotlin/io/libp2p/pubsub/TestRouter.kt +++ b/libp2p/src/testFixtures/kotlin/io/libp2p/pubsub/TestRouter.kt @@ -68,7 +68,7 @@ class TestRouter( ConnectionOverNetty(parentChannel, NullTransport(), initiator) connection.setSecureSession( SecureChannel.Session( - peerId, remoteRouter.peerId, remoteRouter.keyPair.second + peerId, remoteRouter.peerId, remoteRouter.keyPair.second, null ) ) diff --git a/libp2p/src/testFixtures/kotlin/io/libp2p/tools/HostFactory.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/HostFactory.kt index 2af4b035f..08d3be9e3 100644 --- a/libp2p/src/testFixtures/kotlin/io/libp2p/tools/HostFactory.kt +++ b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/HostFactory.kt @@ -7,6 +7,7 @@ import io.libp2p.core.crypto.PrivKey import io.libp2p.core.crypto.PubKey import io.libp2p.core.crypto.generateKeyPair import io.libp2p.core.dsl.Builder +import io.libp2p.core.dsl.SecureChannelCtor import io.libp2p.core.dsl.host import io.libp2p.core.multiformats.Multiaddr import io.libp2p.core.multiformats.Protocol @@ -26,7 +27,7 @@ class HostFactory { var keyType = KEY_TYPE.ECDSA var tcpPort = 5000 var transportCtor = ::TcpTransport - var secureCtor = ::NoiseXXSecureChannel + var secureCtor: SecureChannelCtor = ::NoiseXXSecureChannel var mplexCtor = ::MplexStreamMuxer var muxLogLevel: LogLevel? = LogLevel.DEBUG diff --git a/libp2p/src/testFixtures/kotlin/io/libp2p/transport/NullConnectionUpgrader.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/transport/NullConnectionUpgrader.kt index 64e6c173f..62f1b6c2c 100644 --- a/libp2p/src/testFixtures/kotlin/io/libp2p/transport/NullConnectionUpgrader.kt +++ b/libp2p/src/testFixtures/kotlin/io/libp2p/transport/NullConnectionUpgrader.kt @@ -25,7 +25,8 @@ class NullConnectionUpgrader : val nonsenseSession = SecureChannel.Session( PeerId.random(), PeerId.random(), - generateKeyPair(KEY_TYPE.RSA).second + generateKeyPair(KEY_TYPE.RSA).second, + null ) return CompletableFuture.completedFuture(nonsenseSession) } // establishSecureChannel diff --git a/tools/simulator/src/main/kotlin/io/libp2p/simulate/stream/Libp2pConnectionImpl.kt b/tools/simulator/src/main/kotlin/io/libp2p/simulate/stream/Libp2pConnectionImpl.kt index 55c6f4f1e..9c17a0050 100644 --- a/tools/simulator/src/main/kotlin/io/libp2p/simulate/stream/Libp2pConnectionImpl.kt +++ b/tools/simulator/src/main/kotlin/io/libp2p/simulate/stream/Libp2pConnectionImpl.kt @@ -24,7 +24,8 @@ class Libp2pConnectionImpl( SecureChannel.Session( PeerId.fromPubKey(localPubkey), PeerId.fromPubKey(remotePubkey), - remotePubkey + remotePubkey, + null ) ) } diff --git a/versions.gradle b/versions.gradle index 0305071bb..44bf4a254 100644 --- a/versions.gradle +++ b/versions.gradle @@ -27,7 +27,7 @@ dependencyManagement { entry 'protobuf-java' entry 'protoc' } - dependencySet(group: "io.netty", version: "4.1.87.Final") { + dependencySet(group: "io.netty", version: "4.1.90.Final") { entry 'netty-common' entry 'netty-handler' entry 'netty-transport' @@ -40,6 +40,7 @@ dependencyManagement { dependencySet(group: "org.bouncycastle", version: "1.70") { entry 'bcprov-jdk15on' entry 'bcpkix-jdk15on' + entry 'bctls-jdk15on' } } } \ No newline at end of file From 7a00ca32c815b38c5394adfe413f9b6828053133 Mon Sep 17 00:00:00 2001 From: Dr Ian Preston Date: Thu, 8 Jun 2023 13:18:13 +0100 Subject: [PATCH 10/16] Fix ecdsa cert usage in TLS which bouncycastle broke (#293) * Bouncycastle ECDSA certs in TLS were broken * Change unit test to cover this Co-authored-by: Anton Nashatyrev --- .../libp2p/security/tls/TLSSecureChannel.kt | 27 +++++++++++-------- .../java/io/libp2p/core/HostTestJava.java | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/libp2p/src/main/kotlin/io/libp2p/security/tls/TLSSecureChannel.kt b/libp2p/src/main/kotlin/io/libp2p/security/tls/TLSSecureChannel.kt index 4d162fc9e..ad4d229f5 100644 --- a/libp2p/src/main/kotlin/io/libp2p/security/tls/TLSSecureChannel.kt +++ b/libp2p/src/main/kotlin/io/libp2p/security/tls/TLSSecureChannel.kt @@ -71,6 +71,13 @@ class TlsSecureChannel(private val localKey: PrivKey, private val muxers: List): TlsSecureChannel { + return TlsSecureChannel(localKey, muxerIds, "ECDSA") } } @@ -132,8 +139,15 @@ fun buildTlsHandler( cause = cause.cause handshakeComplete.completeExceptionally(cause) } else { - val negotiatedProtocols = sslContext.applicationProtocolNegotiator().protocols() - val selectedMuxer = muxers.findBestMatch(negotiatedProtocols) + val nextProtocol = handler.applicationProtocol() + val selectedMuxer = muxers + .filter { mux -> + mux.protocolDescriptor.protocolMatcher.matches(nextProtocol) + } + .map { mux -> + NegotiatedStreamMuxer(mux, nextProtocol) + } + .firstOrNull() handshakeComplete.complete( SecureChannel.Session( PeerId.fromPubKey(localKey.publicKey()), @@ -151,15 +165,6 @@ fun buildTlsHandler( private val > List.allProtocols: List get() = this.flatMap { it.protocolDescriptor.announceProtocols } -private fun List.findBestMatch(remoteProtocols: List): NegotiatedStreamMuxer? = - this.firstNotNullOfOrNull { muxer -> - remoteProtocols.firstOrNull { remoteProtocol -> - muxer.protocolDescriptor.protocolMatcher.matches(remoteProtocol) - }?.let { negotiatedProtocol -> - NegotiatedStreamMuxer(muxer, negotiatedProtocol) - } - } - private class ChannelSetup( private val localKey: PrivKey, private val muxers: List, diff --git a/libp2p/src/test/java/io/libp2p/core/HostTestJava.java b/libp2p/src/test/java/io/libp2p/core/HostTestJava.java index c1284aa01..f8f92ef86 100644 --- a/libp2p/src/test/java/io/libp2p/core/HostTestJava.java +++ b/libp2p/src/test/java/io/libp2p/core/HostTestJava.java @@ -37,7 +37,7 @@ void ping() throws Exception { Host clientHost = new HostBuilder() .transport(TcpTransport::new) - .secureChannel(TlsSecureChannel::new) + .secureChannel((k, m) -> new TlsSecureChannel(k, m, "ECDSA")) .muxer(StreamMuxerProtocol::getYamux) .build(); From 3c8867855b8232cc101772bb471a8d1212c11d21 Mon Sep 17 00:00:00 2001 From: Dr Ian Preston Date: Fri, 9 Jun 2023 13:40:50 +0100 Subject: [PATCH 11/16] Fix yamux remote open (#292) * yamux: Don't error on window update for closed stream * stream closes are often window updates with size 0 * Make sure window is initialised before calling onRemoteOpen * Add the unit test reproducing issues with Yamux --------- Co-authored-by: Anton Nashatyrev --- .../io/libp2p/mux/yamux/YamuxHandler.kt | 25 ++++++----- .../io/libp2p/mux/MuxHandlerAbstractTest.kt | 44 +++++++++++++------ .../io/libp2p/mux/yamux/YamuxHandlerTest.kt | 28 +++++++----- 3 files changed, 64 insertions(+), 33 deletions(-) diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt index b92538eeb..3f975f441 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt @@ -75,7 +75,7 @@ open class YamuxHandler( when (msg.flags) { YamuxFlags.SYN -> { // ACK the new stream - onRemoteOpen(msg.id) + onRemoteYamuxOpen(msg.id) ctx.writeAndFlush(YamuxFrame(msg.id, YamuxType.WINDOW_UPDATE, YamuxFlags.ACK, 0)) } YamuxFlags.FIN -> onRemoteDisconnect(msg.id) @@ -107,9 +107,12 @@ open class YamuxHandler( fun handleWindowUpdate(msg: YamuxFrame) { handleFlags(msg) val size = msg.lenData.toInt() + if (size == 0) + return val sendWindow = sendWindows.get(msg.id) - if (sendWindow == null) - throw Libp2pException("No send window for " + msg.id) + if (sendWindow == null) { + return + } sendWindow.addAndGet(size) val buffer = sendBuffers.get(msg.id) if (buffer != null) { @@ -122,8 +125,9 @@ open class YamuxHandler( val ctx = getChannelHandlerContext() val sendWindow = sendWindows.get(child.id) - if (sendWindow == null) + if (sendWindow == null) { throw Libp2pException("No send window for " + child.id) + } if (sendWindow.get() <= 0) { // wait until the window is increased to send more data val buffer = sendBuffers.getOrPut(child.id, { SendBuffer(ctx) }) @@ -147,17 +151,18 @@ open class YamuxHandler( } override fun onLocalOpen(child: MuxChannel) { - onStreamCreate(child) + onStreamCreate(child.id) getChannelHandlerContext().writeAndFlush(YamuxFrame(child.id, YamuxType.DATA, YamuxFlags.SYN, 0)) } - override fun onRemoteCreated(child: MuxChannel) { - onStreamCreate(child) + private fun onRemoteYamuxOpen(id: MuxId) { + onStreamCreate(id) + onRemoteOpen(id) } - private fun onStreamCreate(child: MuxChannel) { - receiveWindows.put(child.id, AtomicInteger(INITIAL_WINDOW_SIZE)) - sendWindows.put(child.id, AtomicInteger(INITIAL_WINDOW_SIZE)) + private fun onStreamCreate(childId: MuxId) { + receiveWindows.putIfAbsent(childId, AtomicInteger(INITIAL_WINDOW_SIZE)) + sendWindows.putIfAbsent(childId, AtomicInteger(INITIAL_WINDOW_SIZE)) } override fun onLocalDisconnect(child: MuxChannel) { diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt index 466718cd9..39478f281 100644 --- a/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt @@ -38,6 +38,7 @@ abstract class MuxHandlerAbstractTest { val parentChannelId get() = ech.id() val allocatedBufs = mutableListOf() + val activeEventHandlers = mutableListOf() abstract val maxFrameDataLength: Int abstract fun createMuxHandler(streamHandler: StreamHandler<*>): MuxHandler @@ -75,6 +76,9 @@ abstract class MuxHandlerAbstractTest { @AfterEach open fun cleanUpAndCheck() { + childHandlers.forEach { + assertThat(it.exceptions).isEmpty() + } childHandlers.clear() allocatedBufs.forEach { @@ -113,6 +117,8 @@ abstract class MuxHandlerAbstractTest { return buf } + protected fun allocateMessage(hexBytes: String) = hexBytes.fromHex().toByteBuf(allocateBuf()) + fun assertHandlerCount(count: Int) = assertEquals(count, childHandlers.size) fun assertLastMessage(handler: Int, msgCount: Int, msg: String) { val messages = childHandlers[handler].inboundMessages @@ -142,7 +148,6 @@ abstract class MuxHandlerAbstractTest { assertEquals("66", childHandlers[0].inboundMessages.last()) assertFalse(childHandlers[0].isInactivated) - assertTrue(childHandlers[0].exceptions.isEmpty()) } @Test @@ -205,9 +210,7 @@ abstract class MuxHandlerAbstractTest { assertLastMessage(1, 2, "34") assertFalse(childHandlers[0].isInactivated) - assertTrue(childHandlers[0].exceptions.isEmpty()) assertFalse(childHandlers[1].isInactivated) - assertTrue(childHandlers[1].exceptions.isEmpty()) } @Test @@ -233,7 +236,6 @@ abstract class MuxHandlerAbstractTest { assertFalse(childHandlers[0].isInactivated) resetStream(12) assertTrue(childHandlers[0].isHandlerRemoved) - assertTrue(childHandlers[0].exceptions.isEmpty()) openStream(22) writeStream(22, "33") @@ -247,7 +249,6 @@ abstract class MuxHandlerAbstractTest { assertFalse(childHandlers[1].isInactivated) resetStream(22) assertTrue(childHandlers[1].isHandlerRemoved) - assertTrue(childHandlers[1].exceptions.isEmpty()) } @Test @@ -270,7 +271,6 @@ abstract class MuxHandlerAbstractTest { assertTrue(childHandlers[0].ctx.channel().closeFuture().isDone) assertTrue(childHandlers[0].isHandlerRemoved) - assertTrue(childHandlers[0].exceptions.isEmpty()) } @Test @@ -326,7 +326,7 @@ abstract class MuxHandlerAbstractTest { @Test fun `local create and after local disconnect should still read`() { val handler = openStreamByLocal() - handler.ctx.writeAndFlush("1984".fromHex().toByteBuf(allocateBuf())) + handler.ctx.writeAndFlush(allocateMessage("1984")) handler.ctx.disconnect().sync() val openFrame = readFrameOrThrow() @@ -362,7 +362,7 @@ abstract class MuxHandlerAbstractTest { assertThat(handler.isUnregistered).isFalse() assertThat(handler.userEvents).containsExactly(RemoteWriteClosed) - handler.ctx.writeAndFlush("1984".fromHex().toByteBuf(allocateBuf())) + handler.ctx.writeAndFlush(allocateMessage("1984")) val readFrame = readFrameOrThrow() assertThat(readFrame.flag).isEqualTo(Data) @@ -393,7 +393,7 @@ abstract class MuxHandlerAbstractTest { readFrameOrThrow() val largeMessage = "42".repeat(maxFrameDataLength - 1) + "4344" - handler.ctx.writeAndFlush(largeMessage.fromHex().toByteBuf(allocateBuf())) + handler.ctx.writeAndFlush(allocateMessage(largeMessage)) val dataFrame1 = readFrameOrThrow() assertThat(dataFrame1.data.fromHex()) @@ -416,7 +416,7 @@ abstract class MuxHandlerAbstractTest { handler.ctx.disconnect() assertThrows(Exception::class.java) { - handler.ctx.writeAndFlush("42".fromHex().toByteBuf(allocateBuf())).sync() + handler.ctx.writeAndFlush(allocateMessage("42")).sync() } } @@ -426,7 +426,7 @@ abstract class MuxHandlerAbstractTest { handler.ctx.close() assertThrows(Exception::class.java) { - handler.ctx.writeAndFlush("42".fromHex().toByteBuf(allocateBuf())).sync() + handler.ctx.writeAndFlush(allocateMessage("42")).sync() } } @@ -436,11 +436,28 @@ abstract class MuxHandlerAbstractTest { ech.close().sync() assertThrows(Exception::class.java) { - handler.ctx.writeAndFlush("42".fromHex().toByteBuf(allocateBuf())).sync() + handler.ctx.writeAndFlush(allocateMessage("42")).sync() + } + } + + @Test + fun `test writing to remotely open stream upon activation`() { + activeEventHandlers += TestEventHandler { + val writePromise = it.ctx.writeAndFlush(allocateMessage("42")) + writePromise.sync() } + openStream(33) + + val dataFrame = readFrameOrThrow() + assertThat(dataFrame.streamId).isEqualTo(33) + assertThat(dataFrame.data).isEqualTo("42") + } + + fun interface TestEventHandler { + fun handle(testHandler: TestHandler) } - class TestHandler : ChannelInboundHandlerAdapter() { + inner class TestHandler : ChannelInboundHandlerAdapter() { val inboundMessages = mutableListOf() lateinit var ctx: ChannelHandlerContext var readCompleteEventCount = 0 @@ -477,6 +494,7 @@ abstract class MuxHandlerAbstractTest { assertFalse(isActivated) isActivated = true println("MultiplexHandlerTest.channelActive") + activeEventHandlers.forEach { it.handle(this) } } override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt index b920d1285..d46336713 100644 --- a/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt @@ -14,6 +14,7 @@ import io.netty.channel.ChannelHandlerContext class YamuxHandlerTest : MuxHandlerAbstractTest() { override val maxFrameDataLength = 256 + private val readFrameQueue = ArrayDeque() override fun createMuxHandler(streamHandler: StreamHandler<*>): MuxHandler = object : YamuxHandler( @@ -44,17 +45,24 @@ class YamuxHandlerTest : MuxHandlerAbstractTest() { } override fun readFrame(): AbstractTestMuxFrame? { - val maybeYamuxFrame = ech.readOutbound() - return maybeYamuxFrame?.let { yamuxFrame -> - val flag = when { - yamuxFrame.flags == YamuxFlags.SYN -> Open - yamuxFrame.flags == YamuxFlags.FIN -> Close - yamuxFrame.flags == YamuxFlags.RST -> Reset - yamuxFrame.type == YamuxType.DATA -> Data - else -> throw AssertionError("Unsupported yamux frame: $yamuxFrame") + val yamuxFrame = ech.readOutbound() + if (yamuxFrame != null) { + when (yamuxFrame.flags) { + YamuxFlags.SYN -> readFrameQueue += AbstractTestMuxFrame(yamuxFrame.id.id, Open) + } + + val data = yamuxFrame.data?.readAllBytesAndRelease()?.toHex() ?: "" + when { + yamuxFrame.type == YamuxType.DATA && data.isNotEmpty() -> + readFrameQueue += AbstractTestMuxFrame(yamuxFrame.id.id, Data, data) + } + + when (yamuxFrame.flags) { + YamuxFlags.FIN -> readFrameQueue += AbstractTestMuxFrame(yamuxFrame.id.id, Close) + YamuxFlags.RST -> readFrameQueue += AbstractTestMuxFrame(yamuxFrame.id.id, Reset) } - val sData = yamuxFrame.data?.readAllBytesAndRelease()?.toHex() ?: "" - AbstractTestMuxFrame(yamuxFrame.id.id, flag, sData) } + + return readFrameQueue.removeFirstOrNull() } } From bf15e47a8a934c563f5d4767d5da062b8551ef6b Mon Sep 17 00:00:00 2001 From: Dr Ian Preston Date: Tue, 15 Aug 2023 15:02:16 +0100 Subject: [PATCH 12/16] Fix yamux handling of writes bigger than the window size (#295) * Fix yamux handling of writes bigger than the window size * Fix inefficient window size handling * Release delayed yamux send buffer once sent. * Add Unit test --------- Co-authored-by: Anton Nashatyrev --- .../io/libp2p/mux/yamux/YamuxHandler.kt | 19 ++++-- .../main/kotlin/io/libp2p/protocol/Ping.kt | 9 ++- .../java/io/libp2p/core/HostTestJava.java | 63 +++++++++++++++++++ .../io/libp2p/mux/yamux/YamuxHandlerTest.kt | 29 ++++++++- 4 files changed, 110 insertions(+), 10 deletions(-) diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt index 3f975f441..887745c0a 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt @@ -34,19 +34,26 @@ open class YamuxHandler( private val buffered = ArrayDeque() fun add(data: ByteBuf) { - buffered.add(data) + buffered.add(data.retain()) } fun flush(sendWindow: AtomicInteger, id: MuxId): Int { var written = 0 while (! buffered.isEmpty()) { val buf = buffered.first() - if (buf.readableBytes() + written < sendWindow.get()) { - buffered.removeFirst() + val readableBytes = buf.readableBytes() + if (readableBytes + written < sendWindow.get()) { sendBlocks(ctx, buf, sendWindow, id) - written += buf.readableBytes() - } else + written += readableBytes + buf.release() + buffered.removeFirst() + } else { + // partial write to fit within window + val toRead = sendWindow.get() - written + sendBlocks(ctx, buf.readSlice(toRead), sendWindow, id) + written += toRead break + } } return written } @@ -96,7 +103,7 @@ open class YamuxHandler( } val newWindow = recWindow.addAndGet(-size.toInt()) if (newWindow < INITIAL_WINDOW_SIZE / 2) { - val delta = INITIAL_WINDOW_SIZE / 2 + val delta = INITIAL_WINDOW_SIZE - newWindow recWindow.addAndGet(delta) ctx.write(YamuxFrame(msg.id, YamuxType.WINDOW_UPDATE, 0, delta.toLong())) ctx.flush() diff --git a/libp2p/src/main/kotlin/io/libp2p/protocol/Ping.kt b/libp2p/src/main/kotlin/io/libp2p/protocol/Ping.kt index 616cd6450..7a9c20a0f 100644 --- a/libp2p/src/main/kotlin/io/libp2p/protocol/Ping.kt +++ b/libp2p/src/main/kotlin/io/libp2p/protocol/Ping.kt @@ -22,20 +22,23 @@ interface PingController { fun ping(): CompletableFuture } -class Ping : PingBinding(PingProtocol()) +class Ping(pingSize: Int) : PingBinding(PingProtocol(pingSize)) { + constructor() : this(32) +} open class PingBinding(ping: PingProtocol) : StrictProtocolBinding("/ipfs/ping/1.0.0", ping) class PingTimeoutException : Libp2pException() -open class PingProtocol : ProtocolHandler(Long.MAX_VALUE, Long.MAX_VALUE) { +open class PingProtocol(var pingSize: Int) : ProtocolHandler(Long.MAX_VALUE, Long.MAX_VALUE) { var timeoutScheduler by lazyVar { Executors.newSingleThreadScheduledExecutor() } var curTime: () -> Long = { System.currentTimeMillis() } var random = Random() - var pingSize = 32 var pingTimeout = Duration.ofSeconds(10) + constructor() : this(32) + override fun onStartInitiator(stream: Stream): CompletableFuture { val handler = PingInitiator() stream.pushHandler(handler) diff --git a/libp2p/src/test/java/io/libp2p/core/HostTestJava.java b/libp2p/src/test/java/io/libp2p/core/HostTestJava.java index f8f92ef86..c8713f8ad 100644 --- a/libp2p/src/test/java/io/libp2p/core/HostTestJava.java +++ b/libp2p/src/test/java/io/libp2p/core/HostTestJava.java @@ -93,6 +93,69 @@ void ping() throws Exception { System.out.println("Server stopped"); } + @Test + void largePing() throws Exception { + int pingSize = 200 * 1024; + String localListenAddress = "/ip4/127.0.0.1/tcp/40002"; + + Host clientHost = new HostBuilder() + .transport(TcpTransport::new) + .secureChannel((k, m) -> new TlsSecureChannel(k, m, "ECDSA")) + .muxer(StreamMuxerProtocol::getYamux) + .build(); + + Host serverHost = new HostBuilder() + .transport(TcpTransport::new) + .secureChannel(TlsSecureChannel::new) + .muxer(StreamMuxerProtocol::getYamux) + .protocol(new Ping(pingSize)) + .listen(localListenAddress) + .build(); + + CompletableFuture clientStarted = clientHost.start(); + CompletableFuture serverStarted = serverHost.start(); + clientStarted.get(5, TimeUnit.SECONDS); + System.out.println("Client started"); + serverStarted.get(5, TimeUnit.SECONDS); + System.out.println("Server started"); + + Assertions.assertEquals(0, clientHost.listenAddresses().size()); + Assertions.assertEquals(1, serverHost.listenAddresses().size()); + Assertions.assertEquals( + localListenAddress + "/p2p/" + serverHost.getPeerId(), + serverHost.listenAddresses().get(0).toString() + ); + + StreamPromise ping = + clientHost.getNetwork().connect( + serverHost.getPeerId(), + new Multiaddr(localListenAddress) + ).thenApply( + it -> it.muxerSession().createStream(new Ping(pingSize)) + ) + .join(); + + Stream pingStream = ping.getStream().get(5, TimeUnit.SECONDS); + System.out.println("Ping stream created"); + PingController pingCtr = ping.getController().get(5, TimeUnit.SECONDS); + System.out.println("Ping controller created"); + + for (int i = 0; i < 10; i++) { + long latency = pingCtr.ping().join();//get(5, TimeUnit.SECONDS); + System.out.println("Ping is " + latency); + } + pingStream.close().get(5, TimeUnit.SECONDS); + System.out.println("Ping stream closed"); + + Assertions.assertThrows(ExecutionException.class, () -> + pingCtr.ping().get(5, TimeUnit.SECONDS)); + + clientHost.stop().get(5, TimeUnit.SECONDS); + System.out.println("Client stopped"); + serverHost.stop().get(5, TimeUnit.SECONDS); + System.out.println("Server stopped"); + } + @Test void keyPairGeneration() { Pair pair = KeyKt.generateKeyPair(KEY_TYPE.SECP256K1); diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt index d46336713..4fc35691d 100644 --- a/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt @@ -10,6 +10,8 @@ import io.libp2p.mux.MuxHandlerAbstractTest import io.libp2p.mux.MuxHandlerAbstractTest.AbstractTestMuxFrame.Flag.* import io.libp2p.tools.readAllBytesAndRelease import io.netty.channel.ChannelHandlerContext +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test class YamuxHandlerTest : MuxHandlerAbstractTest() { @@ -27,8 +29,10 @@ class YamuxHandlerTest : MuxHandlerAbstractTest() { } } + private fun Long.toMuxId() = MuxId(parentChannelId, this, true) + override fun writeFrame(frame: AbstractTestMuxFrame) { - val muxId = MuxId(parentChannelId, frame.streamId, true) + val muxId = frame.streamId.toMuxId() val yamuxFrame = when (frame.flag) { Open -> YamuxFrame(muxId, YamuxType.DATA, YamuxFlags.SYN, 0) Data -> YamuxFrame( @@ -65,4 +69,27 @@ class YamuxHandlerTest : MuxHandlerAbstractTest() { return readFrameQueue.removeFirstOrNull() } + + @Test + fun `data should be buffered and sent after window increased from zero`() { + val handler = openStreamByLocal() + val streamId = readFrameOrThrow().streamId + + ech.writeInbound( + YamuxFrame( + streamId.toMuxId(), + YamuxType.WINDOW_UPDATE, + YamuxFlags.ACK, + -INITIAL_WINDOW_SIZE.toLong() + ) + ) + + handler.ctx.writeAndFlush("1984".fromHex().toByteBuf(allocateBuf())) + + assertThat(readFrame()).isNull() + + ech.writeInbound(YamuxFrame(streamId.toMuxId(), YamuxType.WINDOW_UPDATE, YamuxFlags.ACK, 5000)) + val frame = readFrameOrThrow() + assertThat(frame.data).isEqualTo("1984") + } } From 7b853f633dd49da642d0ac599e8d3f52a70044f7 Mon Sep 17 00:00:00 2001 From: Stefan Bratanov Date: Tue, 15 Aug 2023 16:07:42 +0100 Subject: [PATCH 13/16] Add Yamux specific unit tests (#298) * Add Yamux specific unit tests * Minor Yamux refactorings --- .../io/libp2p/etc/util/netty/mux/MuxId.kt | 4 +- .../kotlin/io/libp2p/mux/yamux/YamuxFrame.kt | 14 ++- .../io/libp2p/mux/yamux/YamuxFrameCodec.kt | 30 +++-- .../io/libp2p/mux/yamux/YamuxHandler.kt | 59 +++++---- .../kotlin/io/libp2p/mux/yamux/YamuxType.kt | 2 +- .../io/libp2p/mux/MuxHandlerAbstractTest.kt | 30 ++--- .../io/libp2p/mux/mplex/MplexHandlerTest.kt | 9 +- .../io/libp2p/mux/yamux/YamuxHandlerTest.kt | 117 ++++++++++++++++-- 8 files changed, 193 insertions(+), 72 deletions(-) diff --git a/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/MuxId.kt b/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/MuxId.kt index 0d7051d93..25bb66f2a 100644 --- a/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/MuxId.kt +++ b/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/MuxId.kt @@ -3,8 +3,8 @@ package io.libp2p.etc.util.netty.mux import io.netty.channel.ChannelId data class MuxId(val parentId: ChannelId, val id: Long, val initiator: Boolean) : ChannelId { - override fun asShortText() = "$parentId/$id/$initiator" - override fun asLongText() = asShortText() + override fun asShortText() = "${parentId.asShortText()}/$id/$initiator" + override fun asLongText() = "${parentId.asLongText()}/$id/$initiator" override fun compareTo(other: ChannelId?) = asShortText().compareTo(other?.asShortText() ?: "") override fun toString() = asLongText() } diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrame.kt b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrame.kt index fefdf1aee..32bd32e6a 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrame.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrame.kt @@ -8,16 +8,18 @@ import io.netty.buffer.Unpooled /** * Contains the fields that comprise a yamux frame. - * @param streamId the ID of the stream. - * @param flag the flag value for this frame. + * @param id the ID of the stream. + * @param flags the flags value for this frame. + * @param length the length field for this frame. * @param data the data segment. */ -class YamuxFrame(val id: MuxId, val type: Int, val flags: Int, val lenData: Long, val data: ByteBuf? = null) : +class YamuxFrame(val id: MuxId, val type: Int, val flags: Int, val length: Long, val data: ByteBuf? = null) : DefaultByteBufHolder(data ?: Unpooled.EMPTY_BUFFER) { override fun toString(): String { - if (data == null) - return "YamuxFrame(id=$id, type=$type, flag=$flags)" - return "YamuxFrame(id=$id, type=$type, flag=$flags, data=${String(data.toByteArray())})" + if (data == null) { + return "YamuxFrame(id=$id, type=$type, flags=$flags, length=$length)" + } + return "YamuxFrame(id=$id, type=$type, flags=$flags, length=$length, data=${String(data.toByteArray())})" } } diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrameCodec.kt b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrameCodec.kt index d21fb2d4f..d8a8e2679 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrameCodec.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxFrameCodec.kt @@ -29,7 +29,7 @@ class YamuxFrameCodec( out.writeByte(msg.type) out.writeShort(msg.flags) out.writeInt(msg.id.id.toInt()) - out.writeInt(msg.data?.readableBytes() ?: msg.lenData.toInt()) + out.writeInt(msg.data?.readableBytes() ?: msg.length.toInt()) out.writeBytes(msg.data ?: Unpooled.EMPTY_BUFFER) } @@ -42,32 +42,44 @@ class YamuxFrameCodec( */ override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList) { while (msg.isReadable) { - if (msg.readableBytes() < 12) + if (msg.readableBytes() < 12) { return + } val readerIndex = msg.readerIndex() msg.readByte(); // version always 0 val type = msg.readUnsignedByte() val flags = msg.readUnsignedShort() val streamId = msg.readUnsignedInt() - val lenData = msg.readUnsignedInt() + val length = msg.readUnsignedInt() if (type.toInt() != YamuxType.DATA) { - val yamuxFrame = YamuxFrame(MuxId(ctx.channel().id(), streamId, isInitiator.xor(streamId.mod(2).equals(1)).not()), type.toInt(), flags, lenData) + val yamuxFrame = YamuxFrame( + MuxId(ctx.channel().id(), streamId, isInitiator.xor(streamId.mod(2) == 1).not()), + type.toInt(), + flags, + length + ) out.add(yamuxFrame) continue } - if (lenData > maxFrameDataLength) { + if (length > maxFrameDataLength) { msg.skipBytes(msg.readableBytes()) - throw ProtocolViolationException("Yamux frame is too large: $lenData") + throw ProtocolViolationException("Yamux frame is too large: $length") } - if (msg.readableBytes() < lenData) { + if (msg.readableBytes() < length) { // not enough data to read the frame content // will wait for more ... msg.readerIndex(readerIndex) return } - val data = msg.readSlice(lenData.toInt()) + val data = msg.readSlice(length.toInt()) data.retain() // MessageToMessageCodec releases original buffer, but it needs to be relayed - val yamuxFrame = YamuxFrame(MuxId(ctx.channel().id(), streamId, isInitiator.xor(streamId.mod(2).equals(1)).not()), type.toInt(), flags, lenData, data) + val yamuxFrame = YamuxFrame( + MuxId(ctx.channel().id(), streamId, isInitiator.xor(streamId.mod(2) == 1).not()), + type.toInt(), + flags, + length, + data + ) out.add(yamuxFrame) } } diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt index 887745c0a..072051b74 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxHandler.kt @@ -10,6 +10,7 @@ import io.libp2p.etc.util.netty.mux.MuxId import io.libp2p.mux.MuxHandler import io.netty.buffer.ByteBuf import io.netty.channel.ChannelHandlerContext +import org.slf4j.LoggerFactory import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger @@ -17,6 +18,8 @@ import java.util.concurrent.atomic.AtomicInteger const val INITIAL_WINDOW_SIZE = 256 * 1024 const val MAX_BUFFERED_CONNECTION_WRITES = 1024 * 1024 +private val log = LoggerFactory.getLogger(YamuxHandler::class.java) + open class YamuxHandler( override val multistreamProtocol: MultistreamProtocol, override val maxFrameDataLength: Int, @@ -39,7 +42,7 @@ open class YamuxHandler( fun flush(sendWindow: AtomicInteger, id: MuxId): Int { var written = 0 - while (! buffered.isEmpty()) { + while (!buffered.isEmpty()) { val buf = buffered.first() val readableBytes = buf.readableBytes() if (readableBytes + written < sendWindow.get()) { @@ -65,19 +68,27 @@ open class YamuxHandler( YamuxType.DATA -> handleDataRead(msg) YamuxType.WINDOW_UPDATE -> handleWindowUpdate(msg) YamuxType.PING -> handlePing(msg) - YamuxType.GO_AWAY -> onRemoteClose(msg.id) + YamuxType.GO_AWAY -> handleGoAway(msg) } } - fun handlePing(msg: YamuxFrame) { + private fun handlePing(msg: YamuxFrame) { val ctx = getChannelHandlerContext() when (msg.flags) { - YamuxFlags.SYN -> ctx.writeAndFlush(YamuxFrame(MuxId(msg.id.parentId, 0, msg.id.initiator), YamuxType.PING, YamuxFlags.ACK, msg.lenData)) + YamuxFlags.SYN -> ctx.writeAndFlush( + YamuxFrame( + MuxId(msg.id.parentId, 0, msg.id.initiator), + YamuxType.PING, + YamuxFlags.ACK, + msg.length + ) + ) + YamuxFlags.ACK -> {} } } - fun handleFlags(msg: YamuxFrame) { + private fun handleFlags(msg: YamuxFrame) { val ctx = getChannelHandlerContext() when (msg.flags) { YamuxFlags.SYN -> { @@ -85,18 +96,20 @@ open class YamuxHandler( onRemoteYamuxOpen(msg.id) ctx.writeAndFlush(YamuxFrame(msg.id, YamuxType.WINDOW_UPDATE, YamuxFlags.ACK, 0)) } + YamuxFlags.FIN -> onRemoteDisconnect(msg.id) YamuxFlags.RST -> onRemoteClose(msg.id) } } - fun handleDataRead(msg: YamuxFrame) { + private fun handleDataRead(msg: YamuxFrame) { val ctx = getChannelHandlerContext() - val size = msg.lenData + val size = msg.length handleFlags(msg) - if (size.toInt() == 0) + if (size.toInt() == 0) { return - val recWindow = receiveWindows.get(msg.id) + } + val recWindow = receiveWindows[msg.id] if (recWindow == null) { releaseMessage(msg.data!!) throw Libp2pException("No receive window for " + msg.id) @@ -111,36 +124,38 @@ open class YamuxHandler( childRead(msg.id, msg.data!!) } - fun handleWindowUpdate(msg: YamuxFrame) { + private fun handleWindowUpdate(msg: YamuxFrame) { handleFlags(msg) - val size = msg.lenData.toInt() - if (size == 0) - return - val sendWindow = sendWindows.get(msg.id) - if (sendWindow == null) { + val size = msg.length.toInt() + if (size == 0) { return } + val sendWindow = sendWindows[msg.id] ?: return sendWindow.addAndGet(size) - val buffer = sendBuffers.get(msg.id) + val buffer = sendBuffers[msg.id] if (buffer != null) { val writtenBytes = buffer.flush(sendWindow, msg.id) totalBufferedWrites.addAndGet(-writtenBytes) } } + private fun handleGoAway(msg: YamuxFrame) { + log.debug("Session will be terminated. Go Away message with with error code ${msg.length} has been received.") + onRemoteClose(msg.id) + } + override fun onChildWrite(child: MuxChannel, data: ByteBuf) { val ctx = getChannelHandlerContext() - val sendWindow = sendWindows.get(child.id) - if (sendWindow == null) { - throw Libp2pException("No send window for " + child.id) - } + val sendWindow = sendWindows[child.id] ?: throw Libp2pException("No send window for " + child.id) + if (sendWindow.get() <= 0) { // wait until the window is increased to send more data - val buffer = sendBuffers.getOrPut(child.id, { SendBuffer(ctx) }) + val buffer = sendBuffers.getOrPut(child.id) { SendBuffer(ctx) } buffer.add(data) - if (totalBufferedWrites.addAndGet(data.readableBytes()) > MAX_BUFFERED_CONNECTION_WRITES) + if (totalBufferedWrites.addAndGet(data.readableBytes()) > MAX_BUFFERED_CONNECTION_WRITES) { throw Libp2pException("Overflowed send buffer for connection") + } return } sendBlocks(ctx, data, sendWindow, child.id) diff --git a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxType.kt b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxType.kt index cf66f4b8b..0746c8cf8 100644 --- a/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxType.kt +++ b/libp2p/src/main/kotlin/io/libp2p/mux/yamux/YamuxType.kt @@ -1,7 +1,7 @@ package io.libp2p.mux.yamux /** - * Contains all the permissible values for flags in the yamux protocol. + * Contains all the permissible values for types in the yamux protocol. */ object YamuxType { const val DATA = 0 diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt index 39478f281..83792b559 100644 --- a/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/mux/MuxHandlerAbstractTest.kt @@ -6,9 +6,11 @@ import io.libp2p.core.StreamHandler import io.libp2p.etc.types.fromHex import io.libp2p.etc.types.getX import io.libp2p.etc.types.toHex +import io.libp2p.etc.util.netty.mux.MuxId import io.libp2p.etc.util.netty.mux.RemoteWriteClosed import io.libp2p.etc.util.netty.nettyInitializer import io.libp2p.mux.MuxHandlerAbstractTest.AbstractTestMuxFrame.Flag.* +import io.libp2p.mux.MuxHandlerAbstractTest.TestEventHandler import io.libp2p.tools.TestChannel import io.libp2p.tools.readAllBytesAndRelease import io.netty.buffer.ByteBuf @@ -20,10 +22,7 @@ import io.netty.handler.logging.LoggingHandler import org.assertj.core.api.Assertions.assertThat import org.assertj.core.data.Index import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertThrows -import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.util.concurrent.CompletableFuture @@ -95,10 +94,11 @@ abstract class MuxHandlerAbstractTest { enum class Flag { Open, Data, Close, Reset } } + fun Long.toMuxId() = MuxId(parentChannelId, this, true) + abstract fun writeFrame(frame: AbstractTestMuxFrame) abstract fun readFrame(): AbstractTestMuxFrame? fun readFrameOrThrow() = readFrame() ?: throw AssertionError("No outbound frames") - fun openStream(id: Long) = writeFrame(AbstractTestMuxFrame(id, Open)) fun writeStream(id: Long, msg: String) = writeFrame(AbstractTestMuxFrame(id, Data, msg)) fun closeStream(id: Long) = writeFrame(AbstractTestMuxFrame(id, Close)) @@ -478,7 +478,7 @@ abstract class MuxHandlerAbstractTest { override fun handlerAdded(ctx: ChannelHandlerContext) { assertFalse(isHandlerAdded) isHandlerAdded = true - println("MultiplexHandlerTest.handlerAdded") + println("MuxHandlerAbstractTest.handlerAdded") this.ctx = ctx } @@ -486,58 +486,58 @@ abstract class MuxHandlerAbstractTest { assertTrue(isHandlerAdded) assertFalse(isRegistered) isRegistered = true - println("MultiplexHandlerTest.channelRegistered") + println("MuxHandlerAbstractTest.channelRegistered") } override fun channelActive(ctx: ChannelHandlerContext) { assertTrue(isRegistered) assertFalse(isActivated) isActivated = true - println("MultiplexHandlerTest.channelActive") + println("MuxHandlerAbstractTest.channelActive") activeEventHandlers.forEach { it.handle(this) } } override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { assertTrue(isActivated) - println("MultiplexHandlerTest.channelRead") + println("MuxHandlerAbstractTest.channelRead") msg as ByteBuf inboundMessages += msg.readAllBytesAndRelease().toHex() } override fun channelReadComplete(ctx: ChannelHandlerContext?) { readCompleteEventCount++ - println("MultiplexHandlerTest.channelReadComplete") + println("MuxHandlerAbstractTest.channelReadComplete") } override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { userEvents += evt - println("MultiplexHandlerTest.userEventTriggered: $evt") + println("MuxHandlerAbstractTest.userEventTriggered: $evt") } override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { exceptions += cause - println("MultiplexHandlerTest.exceptionCaught") + println("MuxHandlerAbstractTest.exceptionCaught") } override fun channelInactive(ctx: ChannelHandlerContext) { assertTrue(isActivated) assertFalse(isInactivated) isInactivated = true - println("MultiplexHandlerTest.channelInactive") + println("MuxHandlerAbstractTest.channelInactive") } override fun channelUnregistered(ctx: ChannelHandlerContext?) { assertTrue(isInactivated) assertFalse(isUnregistered) isUnregistered = true - println("MultiplexHandlerTest.channelUnregistered") + println("MuxHandlerAbstractTest.channelUnregistered") } override fun handlerRemoved(ctx: ChannelHandlerContext?) { assertTrue(isUnregistered) assertFalse(isHandlerRemoved) isHandlerRemoved = true - println("MultiplexHandlerTest.handlerRemoved") + println("MuxHandlerAbstractTest.handlerRemoved") } } diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexHandlerTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexHandlerTest.kt index 091107331..bd9fd88fd 100644 --- a/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexHandlerTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/mux/mplex/MplexHandlerTest.kt @@ -4,7 +4,6 @@ import io.libp2p.core.StreamHandler import io.libp2p.core.multistream.MultistreamProtocolV1 import io.libp2p.etc.types.fromHex import io.libp2p.etc.types.toHex -import io.libp2p.etc.util.netty.mux.MuxId import io.libp2p.mux.MuxHandler import io.libp2p.mux.MuxHandlerAbstractTest import io.libp2p.mux.MuxHandlerAbstractTest.AbstractTestMuxFrame.Flag.* @@ -28,6 +27,7 @@ class MplexHandlerTest : MuxHandlerAbstractTest() { } override fun writeFrame(frame: AbstractTestMuxFrame) { + val muxId = frame.streamId.toMuxId() val mplexFlag = when (frame.flag) { Open -> MplexFlag.Type.OPEN Data -> MplexFlag.Type.DATA @@ -39,7 +39,7 @@ class MplexHandlerTest : MuxHandlerAbstractTest() { else -> frame.data.fromHex().toByteBuf(allocateBuf()) } val mplexFrame = - MplexFrame(MuxId(parentChannelId, frame.streamId, true), MplexFlag.getByType(mplexFlag, true), data) + MplexFrame(muxId, MplexFlag.getByType(mplexFlag, true), data) ech.writeInbound(mplexFrame) } @@ -51,10 +51,9 @@ class MplexHandlerTest : MuxHandlerAbstractTest() { MplexFlag.Type.DATA -> Data MplexFlag.Type.CLOSE -> Close MplexFlag.Type.RESET -> Reset - else -> throw AssertionError("Unknown mplex flag: ${mplexFrame.flag}") } - val sData = maybeMplexFrame.data.readAllBytesAndRelease().toHex() - AbstractTestMuxFrame(mplexFrame.id.id, flag, sData) + val data = maybeMplexFrame.data.readAllBytesAndRelease().toHex() + AbstractTestMuxFrame(mplexFrame.id.id, flag, data) } } } diff --git a/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt b/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt index 4fc35691d..640dc8d40 100644 --- a/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt +++ b/libp2p/src/test/kotlin/io/libp2p/mux/yamux/YamuxHandlerTest.kt @@ -4,7 +4,6 @@ import io.libp2p.core.StreamHandler import io.libp2p.core.multistream.MultistreamProtocolV1 import io.libp2p.etc.types.fromHex import io.libp2p.etc.types.toHex -import io.libp2p.etc.util.netty.mux.MuxId import io.libp2p.mux.MuxHandler import io.libp2p.mux.MuxHandlerAbstractTest import io.libp2p.mux.MuxHandlerAbstractTest.AbstractTestMuxFrame.Flag.* @@ -20,7 +19,11 @@ class YamuxHandlerTest : MuxHandlerAbstractTest() { override fun createMuxHandler(streamHandler: StreamHandler<*>): MuxHandler = object : YamuxHandler( - MultistreamProtocolV1, maxFrameDataLength, null, streamHandler, true + MultistreamProtocolV1, + maxFrameDataLength, + null, + streamHandler, + true ) { // MuxHandler consumes the exception. Override this behaviour for testing @Deprecated("Deprecated in Java") @@ -29,19 +32,21 @@ class YamuxHandlerTest : MuxHandlerAbstractTest() { } } - private fun Long.toMuxId() = MuxId(parentChannelId, this, true) - override fun writeFrame(frame: AbstractTestMuxFrame) { val muxId = frame.streamId.toMuxId() val yamuxFrame = when (frame.flag) { Open -> YamuxFrame(muxId, YamuxType.DATA, YamuxFlags.SYN, 0) - Data -> YamuxFrame( - muxId, - YamuxType.DATA, - 0, - frame.data.fromHex().size.toLong(), - frame.data.fromHex().toByteBuf(allocateBuf()) - ) + Data -> { + val data = frame.data.fromHex() + YamuxFrame( + muxId, + YamuxType.DATA, + 0, + data.size.toLong(), + data.toByteBuf(allocateBuf()) + ) + } + Close -> YamuxFrame(muxId, YamuxType.DATA, YamuxFlags.FIN, 0) Reset -> YamuxFrame(muxId, YamuxType.DATA, YamuxFlags.RST, 0) } @@ -49,7 +54,7 @@ class YamuxHandlerTest : MuxHandlerAbstractTest() { } override fun readFrame(): AbstractTestMuxFrame? { - val yamuxFrame = ech.readOutbound() + val yamuxFrame = readYamuxFrame() if (yamuxFrame != null) { when (yamuxFrame.flags) { YamuxFlags.SYN -> readFrameQueue += AbstractTestMuxFrame(yamuxFrame.id.id, Open) @@ -70,6 +75,49 @@ class YamuxHandlerTest : MuxHandlerAbstractTest() { return readFrameQueue.removeFirstOrNull() } + private fun readYamuxFrame(): YamuxFrame? { + return ech.readOutbound() + } + + private fun readYamuxFrameOrThrow() = readYamuxFrame() ?: throw AssertionError("No outbound frames") + + @Test + fun `test ack new stream`() { + // signal opening of new stream + openStream(12) + + writeStream(12, "23") + + val ackFrame = readYamuxFrameOrThrow() + + // receives ack stream + assertThat(ackFrame.flags).isEqualTo(YamuxFlags.ACK) + assertThat(ackFrame.type).isEqualTo(YamuxType.WINDOW_UPDATE) + + closeStream(12) + } + + @Test + fun `test window update`() { + openStream(12) + + val largeMessage = "42".repeat(INITIAL_WINDOW_SIZE + 1) + writeStream(12, largeMessage) + + // ignore ack stream frame + readYamuxFrameOrThrow() + + val windowUpdateFrame = readYamuxFrameOrThrow() + + assertThat(windowUpdateFrame.flags).isZero() + assertThat(windowUpdateFrame.type).isEqualTo(YamuxType.WINDOW_UPDATE) + assertThat(windowUpdateFrame.length).isEqualTo((INITIAL_WINDOW_SIZE + 1).toLong()) + + assertLastMessage(0, 1, largeMessage) + + closeStream(12) + } + @Test fun `data should be buffered and sent after window increased from zero`() { val handler = openStreamByLocal() @@ -92,4 +140,49 @@ class YamuxHandlerTest : MuxHandlerAbstractTest() { val frame = readFrameOrThrow() assertThat(frame.data).isEqualTo("1984") } + + @Test + fun `test ping`() { + val id: Long = 0 + openStream(id) + ech.writeInbound( + YamuxFrame( + id.toMuxId(), + YamuxType.PING, + YamuxFlags.SYN, + // opaque value, echoed back + 3 + ) + ) + + // ignore ack stream frame + readYamuxFrameOrThrow() + + val pingFrame = readYamuxFrameOrThrow() + + assertThat(pingFrame.flags).isEqualTo(YamuxFlags.ACK) + assertThat(pingFrame.type).isEqualTo(YamuxType.PING) + assertThat(pingFrame.length).isEqualTo(3) + + closeStream(id) + } + + @Test + fun `test go away`() { + val id: Long = 0 + openStream(id) + ech.writeInbound( + YamuxFrame( + id.toMuxId(), + YamuxType.GO_AWAY, + 0, + // normal termination + 0x0 + ) + ) + + // verify session termination + assertThat(childHandlers[0].isHandlerRemoved).isTrue() + assertThat(childHandlers[0].isUnregistered).isTrue() + } } From 1664417463776e5f155bac9f20bd8a7f1510d9db Mon Sep 17 00:00:00 2001 From: Stefan Bratanov Date: Thu, 17 Aug 2023 15:13:18 +0100 Subject: [PATCH 14/16] Rename `jvm-libp2p-minimal` to `jvm-libp2p` (#302) --- README.md | 8 ++++---- build.gradle.kts | 2 +- libp2p/gradle.properties | 2 +- settings.gradle | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 593dadd51..83708458b 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Please check the Simulator [README](tools/simulator/README.md) for more details Hosting of artefacts is graciously provided by [Cloudsmith](https://cloudsmith.com). -[![Latest version of 'jvm-libp2p-minimal' @ Cloudsmith](https://api-prd.cloudsmith.io/v1/badges/version/libp2p/jvm-libp2p/maven/jvm-libp2p-minimal/latest/a=noarch;xg=io.libp2p/?render=true&show_latest=true)](https://cloudsmith.io/~libp2p/repos/jvm-libp2p/packages/detail/maven/jvm-libp2p-minimal/latest/a=noarch;xg=io.libp2p/) +[![Latest version of 'jvm-libp2p' @ Cloudsmith](https://api-prd.cloudsmith.io/v1/badges/version/libp2p/jvm-libp2p/maven/jvm-libp2p/latest/a=noarch;xg=io.libp2p/?render=true&show_latest=true)](https://cloudsmith.io/~libp2p/repos/jvm-libp2p/packages/detail/maven/jvm-libp2p/latest/a=noarch;xg=io.libp2p/) As an alternative, artefacts are also available on [JitPack](https://jitpack.io/). @@ -87,7 +87,7 @@ Add the library to the `implementation` part of your Gradle file. ```groovy dependencies { // ... - implementation 'io.libp2p:jvm-libp2p-minimal:X.Y.Z-RELEASE' + implementation 'io.libp2p:jvm-libp2p:X.Y.Z-RELEASE' } ``` ### Using Maven @@ -113,7 +113,7 @@ And then add jvm-libp2p as a dependency: ``` xml io.libp2p - jvm-libp2p-minimal + jvm-libp2p X.Y.Z-RELEASE pom @@ -138,7 +138,7 @@ To build the library from the `jvm-libp2p` folder, run: ./gradlew build ``` -After the build is complete you may find the library `.jar` file here: `jvm-libp2p/build/libs/jvm-libp2p-minimal-0.x.y-RELEASE.jar` +After the build is complete you may find the library `.jar` file here: `jvm-libp2p/build/libs/jvm-libp2p-X.Y.Z-RELEASE.jar` ## License diff --git a/build.gradle.kts b/build.gradle.kts index b6e9a7271..64ce83673 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ import java.net.URL // To publish the release artifact to CloudSmith repo run the following : // ./gradlew publish -PcloudsmithUser= -PcloudsmithApiKey= -description = "a minimal implementation of libp2p for the jvm" +description = "an implementation of libp2p for the jvm" plugins { val kotlinVersion = "1.6.21" diff --git a/libp2p/gradle.properties b/libp2p/gradle.properties index 0ef1e083b..3d6bf87a8 100644 --- a/libp2p/gradle.properties +++ b/libp2p/gradle.properties @@ -1 +1 @@ -mavenArtifactId=jvm-libp2p-minimal \ No newline at end of file +mavenArtifactId=jvm-libp2p \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 3840b0611..3b2d71fae 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,7 +15,7 @@ dependencyResolutionManagement { } } -rootProject.name = 'jvm-libp2p-minimal' +rootProject.name = 'jvm-libp2p' include ':libp2p' include ':tools:schedulers' From 95a2a69f799a6f0e6568933f3122cec743088abe Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Fri, 18 Aug 2023 16:02:32 +0300 Subject: [PATCH 15/16] Update README.md for upcoming v1.0.0 (#301) * Add component status table * Remove outdated roadmap and the old list of components * Add 'Notable Users' section * Add 'Android support' module --- README.md | 111 +++++++++++++++++++++++++++++------------------------- 1 file changed, 59 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 83708458b..a61e2b443 100644 --- a/README.md +++ b/README.md @@ -6,58 +6,45 @@ ![Build Status](https://github.com/libp2p/jvm-libp2p/actions/workflows/build.yml/badge.svg?branch=master) [![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) -> a libp2p implementation for the JVM, written in Kotlin šŸ”„ - -**āš ļø This is heavy work in progress! āš ļø** - -## Roadmap - -The endeavour to build jvm-libp2p is split in two phases: - -* **minimal phase (v0.x):** aims to provide the bare minimum stack that will - allow JVM-based Ethereum 2.0 clients to interoperate with other clients that - rely on fully-fledged libp2p stacks written in other languages. - * To achieve this, we have to be wire-compliant, but don't need to fulfill - the complete catalogue of libp2p abstractions. - * This effort will act as a starting point to evolve this project into a - fully-fledged libp2p stack for JVM environments, including Android - runtimes. - * We are shooting for Aug/early Sept 2019. - * Only Java-friendly faƧade. - -* **maturity phase (v1.x):** upgrades the minimal version to a flexible and - versatile stack adhering to the key design principles of modularity and - pluggability that define the libp2p project. It adds features present in - mature implementations like go-libp2p, rust-libp2p, js-libp2p. - * will offer: pluggable peerstore, connection manager, QUIC transport, - circuit relay, AutoNAT, AutoRelay, NAT traversal, etc. - * Android-friendly. - * Kotlin coroutine-based faƧade, possibly a Reactive Streams faƧade too. - * work will begin after the minimal phase concludes. - -## minimal phase (v0.x): Definition of Done - -We have identified the following components on the path to attaining a minimal -implementation: - -- [X] multistream-select 1.0 -- [X] multiformats: [multiaddr](https://github.com/multiformats/multiaddr) -- [X] crypto (RSA, ed25519, secp256k1) -- [X] [secio](https://github.com/libp2p/specs/pull/106) -- [X] [connection bootstrapping](https://github.com/libp2p/specs/pull/168) -- [X] mplex as a multiplexer -- [X] stream multiplexing -- [X] TCP transport (dialing and listening) -- [X] Identify protocol -- [X] Ping protocol -- [X] [peer ID](https://github.com/libp2p/specs/pull/100) -- [X] noise security protocol -- [X] MDNS -- [X] Gossip 1.1 pubsub - -We are explicitly leaving out the peerstore, DHT, pubsub, connection manager, -etc. and other subsystems or concepts that are internal to implementations and -do not impact the ability to hold communications with other libp2p processes. +[Libp2p](https://libp2p.io/) implementation for the JVM, written in Kotlin šŸ”„ + +## Components + +List of components in the Libp2p spec and their JVM implementation status + +| | Component | Status | +|--------------------------|-------------------------------------------------------------------------------------------------|:----------------:| +| **Transport** | tcp | :green_apple: | +| | [quic](https://github.com/libp2p/specs/tree/master/quic) | :tomato: | +| | websocket | :lemon: | +| | [webtransport](https://github.com/libp2p/specs/tree/master/webtransport) | | +| | [webrtc-browser-to-server](https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md) | | +| | [webrtc-private-to-private](https://github.com/libp2p/specs/blob/master/webrtc/webrtc.md) | | +| **Secure Communication** | [noise](https://github.com/libp2p/specs/blob/master/noise/) | :green_apple: | +| | [tls](https://github.com/libp2p/specs/blob/master/tls/tls.md) | :lemon: | +| | [plaintext](https://github.com/libp2p/specs/blob/master/plaintext/README.md) | :lemon: | +| | [secio](https://github.com/libp2p/specs/blob/master/secio/README.md) **(deprecated)** | :green_apple: | +| **Protocol Select** | [multistream](https://github.com/multiformats/multistream-select) | :green_apple: | +| **Stream Multiplexing** | [yamux](https://github.com/libp2p/specs/blob/master/yamux/README.md) | :lemon: | +| | [mplex](https://github.com/libp2p/specs/blob/master/mplex/README.md) | :green_apple: | +| **NAT Traversal** | [circuit-relay-v2](https://github.com/libp2p/specs/blob/master/relay/circuit-v2.md) | | +| | [autonat](https://github.com/libp2p/specs/tree/master/autonat) | | +| | [hole-punching](https://github.com/libp2p/specs/blob/master/connections/hole-punching.md) | | +| **Discovery** | [bootstrap](https://github.com/libp2p/specs/blob/master/kad-dht/README.md#bootstrap-process) | | +| | random-walk | | +| | [mdns-discovery](https://github.com/libp2p/specs/blob/master/discovery/mdns.md) | :lemon: | +| | [rendezvous](https://github.com/libp2p/specs/blob/master/rendezvous/README.md) | | +| **Peer Routing** | [kad-dht](https://github.com/libp2p/specs/blob/master/kad-dht/README.md) | | +| **Publish/Subscribe** | floodsub | :lemon: | +| | [gossipsub](https://github.com/libp2p/specs/tree/master/pubsub/gossipsub) | :green_apple: | +| **Storage** | record | | +| **Other protocols** | [ping](https://github.com/libp2p/specs/blob/master/ping/ping.md) | :green_apple: | +| | [identify](https://github.com/libp2p/specs/blob/master/identify/README.md) | :green_apple: | + +Legend: +- :green_apple: - tested in production +- :lemon: - prototype or beta, not tested in production +- :tomato: - in progress ## Gossip simulator @@ -65,6 +52,18 @@ Deterministic Gossip simulator which may simulate networks as large as 10000 of Please check the Simulator [README](tools/simulator/README.md) for more details +## Android support + +The library is basically being developed with Android compatibility in mind. +However we are not aware of anyone using it in production. + +The `examples/android-chatter` module contains working sample Android application. This module is ignored by the Gradle +build when no Android SDK is installed. +To include the Android module define a valid SDK location with an `ANDROID_HOME` environment variable +or by setting the `sdk.dir` path in your project's local properties file local.properties. + +Importing the project into Android Studio should work out of the box. + ## Adding as a dependency to your project Hosting of artefacts is graciously provided by [Cloudsmith](https://cloudsmith.com). @@ -140,6 +139,14 @@ To build the library from the `jvm-libp2p` folder, run: After the build is complete you may find the library `.jar` file here: `jvm-libp2p/build/libs/jvm-libp2p-X.Y.Z-RELEASE.jar` +## Notable users + +- [Teku](https://github.com/Consensys/teku) - Ethereum Consensus Layer client +- [Nabu](https://github.com/peergos/nabu) - minimal Java implementation of IPFS +- [Peergos](https://github.com/peergos/peergos) - peer-to-peer encrypted global filesystem + +(Please open a pull request if you want your project to be added here) + ## License Dual-licensed under MIT and ASLv2, by way of the [Permissive License From 4e730882ab880822e33a9ab27ab37957ca144e4b Mon Sep 17 00:00:00 2001 From: Dr Ian Preston Date: Fri, 18 Aug 2023 19:26:23 +0100 Subject: [PATCH 16/16] Add Host.getProtocols() and fix addProtocolHandler (#299) * Add Host.getProtocols() * Fix Host.addProtocolHandler (There was in internal CopyOnWriteArrayList for protocols in the muxer, so it didn't see newly added protocols) * Make a thread-safe shared MutableList for Host and Muxers --------- Co-authored-by: Anton Nashatyrev --- libp2p/src/main/kotlin/io/libp2p/core/Host.kt | 2 + .../kotlin/io/libp2p/core/dsl/Builders.kt | 6 +- .../io/libp2p/core/multistream/Multistream.kt | 2 +- .../main/kotlin/io/libp2p/host/HostImpl.kt | 4 ++ .../io/libp2p/multistream/MultistreamImpl.kt | 6 +- .../java/io/libp2p/core/HostTestJava.java | 63 +++++++++++++++++++ .../kotlin/io/libp2p/tools/NullHost.kt | 4 ++ 7 files changed, 79 insertions(+), 8 deletions(-) diff --git a/libp2p/src/main/kotlin/io/libp2p/core/Host.kt b/libp2p/src/main/kotlin/io/libp2p/core/Host.kt index cfdb3586d..f419787ad 100644 --- a/libp2p/src/main/kotlin/io/libp2p/core/Host.kt +++ b/libp2p/src/main/kotlin/io/libp2p/core/Host.kt @@ -81,6 +81,8 @@ interface Host { */ fun addProtocolHandler(protocolBinding: ProtocolBinding) + fun getProtocols(): List> + /** * Removes the handler added with [addProtocolHandler] */ diff --git a/libp2p/src/main/kotlin/io/libp2p/core/dsl/Builders.kt b/libp2p/src/main/kotlin/io/libp2p/core/dsl/Builders.kt index 5482b5e8e..67dd02fcd 100644 --- a/libp2p/src/main/kotlin/io/libp2p/core/dsl/Builders.kt +++ b/libp2p/src/main/kotlin/io/libp2p/core/dsl/Builders.kt @@ -33,6 +33,7 @@ import io.libp2p.transport.tcp.TcpTransport import io.netty.channel.ChannelHandler import io.netty.handler.logging.LogLevel import io.netty.handler.logging.LoggingHandler +import java.util.concurrent.CopyOnWriteArrayList typealias TransportCtor = (ConnectionUpgrader) -> Transport typealias SecureChannelCtor = (PrivKey, List) -> SecureChannel @@ -173,7 +174,8 @@ open class Builder { } } - val muxers = muxers.map { it.createMuxer(streamMultistreamProtocol, protocols.values) } + val updatableProtocols: MutableList> = CopyOnWriteArrayList(protocols.values) + val muxers = muxers.map { it.createMuxer(streamMultistreamProtocol, updatableProtocols) } val secureChannels = secureChannels.values.map { it(privKey, muxers) } @@ -201,7 +203,7 @@ open class Builder { networkImpl, addressBook, network.listen.map { Multiaddr(it) }, - protocols.values, + updatableProtocols, broadcastConnHandler, streamVisitors ) diff --git a/libp2p/src/main/kotlin/io/libp2p/core/multistream/Multistream.kt b/libp2p/src/main/kotlin/io/libp2p/core/multistream/Multistream.kt index 2a7101334..1851086aa 100644 --- a/libp2p/src/main/kotlin/io/libp2p/core/multistream/Multistream.kt +++ b/libp2p/src/main/kotlin/io/libp2p/core/multistream/Multistream.kt @@ -25,5 +25,5 @@ interface Multistream : P2PChannelHandler { * For _initiator_ role this is the list of protocols the initiator wants to instantiate. * Basically this is either a single protocol or a protocol versions */ - val bindings: MutableList> + val bindings: List> } diff --git a/libp2p/src/main/kotlin/io/libp2p/host/HostImpl.kt b/libp2p/src/main/kotlin/io/libp2p/host/HostImpl.kt index 328f6018e..40df4e491 100644 --- a/libp2p/src/main/kotlin/io/libp2p/host/HostImpl.kt +++ b/libp2p/src/main/kotlin/io/libp2p/host/HostImpl.kt @@ -78,6 +78,10 @@ class HostImpl( protocolHandlers -= protocolBinding } + override fun getProtocols(): List> { + return protocolHandlers + } + override fun addConnectionHandler(handler: ConnectionHandler) { connectionHandlers += handler } diff --git a/libp2p/src/main/kotlin/io/libp2p/multistream/MultistreamImpl.kt b/libp2p/src/main/kotlin/io/libp2p/multistream/MultistreamImpl.kt index 4b7fc626d..8cf242f68 100644 --- a/libp2p/src/main/kotlin/io/libp2p/multistream/MultistreamImpl.kt +++ b/libp2p/src/main/kotlin/io/libp2p/multistream/MultistreamImpl.kt @@ -6,18 +6,14 @@ import io.libp2p.core.multistream.Multistream import io.libp2p.core.multistream.ProtocolBinding import java.time.Duration import java.util.concurrent.CompletableFuture -import java.util.concurrent.CopyOnWriteArrayList class MultistreamImpl( - initList: List> = listOf(), + override val bindings: List>, val preHandler: P2PChannelHandler<*>? = null, val postHandler: P2PChannelHandler<*>? = null, val negotiationTimeLimit: Duration = DEFAULT_NEGOTIATION_TIME_LIMIT ) : Multistream { - override val bindings: MutableList> = - CopyOnWriteArrayList(initList) - override fun initChannel(ch: P2PChannel): CompletableFuture { return with(ch) { preHandler?.also { diff --git a/libp2p/src/test/java/io/libp2p/core/HostTestJava.java b/libp2p/src/test/java/io/libp2p/core/HostTestJava.java index c8713f8ad..f5e63059e 100644 --- a/libp2p/src/test/java/io/libp2p/core/HostTestJava.java +++ b/libp2p/src/test/java/io/libp2p/core/HostTestJava.java @@ -156,6 +156,69 @@ void largePing() throws Exception { System.out.println("Server stopped"); } + @Test + void addPingAfterHostStart() throws Exception { + String localListenAddress = "/ip4/127.0.0.1/tcp/40002"; + + Host clientHost = new HostBuilder() + .transport(TcpTransport::new) + .secureChannel((k, m) -> new TlsSecureChannel(k, m, "ECDSA")) + .muxer(StreamMuxerProtocol::getYamux) + .build(); + + Host serverHost = new HostBuilder() + .transport(TcpTransport::new) + .secureChannel(TlsSecureChannel::new) + .muxer(StreamMuxerProtocol::getYamux) + .listen(localListenAddress) + .build(); + + CompletableFuture clientStarted = clientHost.start(); + CompletableFuture serverStarted = serverHost.start(); + clientStarted.get(5, TimeUnit.SECONDS); + System.out.println("Client started"); + serverStarted.get(5, TimeUnit.SECONDS); + System.out.println("Server started"); + + Assertions.assertEquals(0, clientHost.listenAddresses().size()); + Assertions.assertEquals(1, serverHost.listenAddresses().size()); + Assertions.assertEquals( + localListenAddress + "/p2p/" + serverHost.getPeerId(), + serverHost.listenAddresses().get(0).toString() + ); + + serverHost.addProtocolHandler(new Ping()); + + StreamPromise ping = + clientHost.getNetwork().connect( + serverHost.getPeerId(), + new Multiaddr(localListenAddress) + ).thenApply( + it -> it.muxerSession().createStream(new Ping()) + ) + .get(5, TimeUnit.SECONDS); + + Stream pingStream = ping.getStream().get(5, TimeUnit.SECONDS); + System.out.println("Ping stream created"); + PingController pingCtr = ping.getController().get(5, TimeUnit.SECONDS); + System.out.println("Ping controller created"); + + for (int i = 0; i < 10; i++) { + long latency = pingCtr.ping().get(1, TimeUnit.SECONDS); + System.out.println("Ping is " + latency); + } + pingStream.close().get(5, TimeUnit.SECONDS); + System.out.println("Ping stream closed"); + + Assertions.assertThrows(ExecutionException.class, () -> + pingCtr.ping().get(5, TimeUnit.SECONDS)); + + clientHost.stop().get(5, TimeUnit.SECONDS); + System.out.println("Client stopped"); + serverHost.stop().get(5, TimeUnit.SECONDS); + System.out.println("Server stopped"); + } + @Test void keyPairGeneration() { Pair pair = KeyKt.generateKeyPair(KEY_TYPE.SECP256K1); diff --git a/libp2p/src/testFixtures/kotlin/io/libp2p/tools/NullHost.kt b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/NullHost.kt index 2e3f84c44..476bd0e2e 100644 --- a/libp2p/src/testFixtures/kotlin/io/libp2p/tools/NullHost.kt +++ b/libp2p/src/testFixtures/kotlin/io/libp2p/tools/NullHost.kt @@ -55,6 +55,10 @@ open class NullHost : Host { TODO("not implemented") } + override fun getProtocols(): List> { + TODO("not implemented") + } + override fun addConnectionHandler(handler: ConnectionHandler) { TODO("not implemented") }