diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 85b31a906..8efeac5ab 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -47,46 +52,62 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" + integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== + +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" + "@types/connect" "*" + "@types/node" "*" -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== +"@types/bonjour@^3.5.9": + version "3.5.10" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.10.tgz#0f6aadfe00ea414edc86f5d106357cda9701e275" + integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" + "@types/node" "*" -"@types/component-emitter@^1.2.10": - version "1.2.11" - resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.11.tgz#50d47d42b347253817a39709fef03ce66a108506" - integrity sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ== +"@types/connect-history-api-fallback@^1.3.5": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" + integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" -"@types/cookie@^0.4.0": +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/cookie@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== -"@types/cors@^2.8.8": - version "2.8.12" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" - integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== +"@types/cors@^2.8.12": + version "2.8.13" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.13.tgz#b8ade22ba455a1b8cb3b5d3f35910fd204f84f94" + integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA== + dependencies: + "@types/node" "*" -"@types/eslint-scope@^3.7.0": - version "3.7.3" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224" - integrity sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g== +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== dependencies: "@types/eslint" "*" "@types/estree" "*" @@ -99,15 +120,29 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*": +"@types/estree@*", "@types/estree@^0.0.51": version "0.0.51" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== -"@types/estree@^0.0.50": - version "0.0.50" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" - integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": + version "4.17.31" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f" + integrity sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*", "@types/express@^4.17.13": + version "4.17.14" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.14.tgz#143ea0557249bc1b3b54f15db4c81c3d4eb3569c" + integrity sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.18" + "@types/qs" "*" + "@types/serve-static" "*" "@types/http-proxy@^1.17.8": version "1.17.9" @@ -121,16 +156,60 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + "@types/node@*", "@types/node@>=10.0.0": version "17.0.41" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.41.tgz#1607b2fd3da014ae5d4d1b31bc792a39348dfb9b" integrity sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw== +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + "@types/retry@0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== +"@types/serve-index@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278" + integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.13.10": + version "1.15.0" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155" + integrity sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg== + dependencies: + "@types/mime" "*" + "@types/node" "*" + +"@types/sockjs@^0.3.33": + version "0.3.33" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" + integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== + dependencies: + "@types/node" "*" + +"@types/ws@^8.5.1": + version "8.5.3" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" + integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w== + dependencies: + "@types/node" "*" + "@ungap/promise-all-settled@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" @@ -257,22 +336,22 @@ "@webassemblyjs/ast" "1.11.1" "@xtuc/long" "4.2.2" -"@webpack-cli/configtest@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.1.1.tgz#9f53b1b7946a6efc2a749095a4f450e2932e8356" - integrity sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg== +"@webpack-cli/configtest@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" + integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== -"@webpack-cli/info@^1.4.0": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.4.1.tgz#2360ea1710cbbb97ff156a3f0f24556e0fc1ebea" - integrity sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA== +"@webpack-cli/info@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" + integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== dependencies: envinfo "^7.7.3" -"@webpack-cli/serve@^1.6.0": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.6.1.tgz#0de2875ac31b46b6c5bb1ae0a7d7f0ba5678dffe" - integrity sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw== +"@webpack-cli/serve@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" + integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -284,7 +363,7 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -abab@^2.0.5: +abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== @@ -307,14 +386,6 @@ acorn@^8.4.1, acorn@^8.5.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -369,11 +440,6 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -399,33 +465,16 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== -array-flatten@^2.1.0: +array-flatten@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -async@^2.6.2: - version "2.6.4" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" - integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== - dependencies: - lodash "^4.17.14" - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-arraybuffer@0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812" - integrity sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg== - base64id@2.0.0, base64id@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" @@ -441,7 +490,25 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -body-parser@1.20.0, body-parser@^1.19.0: +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +body-parser@^1.19.0: version "1.20.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== @@ -459,17 +526,15 @@ body-parser@1.20.0, body-parser@^1.19.0: type-is "~1.6.18" unpipe "1.0.0" -bonjour@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" - integrity sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg== +bonjour-service@^1.0.11: + version "1.0.14" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.0.14.tgz#c346f5bc84e87802d08f8d5a60b93f758e514ee7" + integrity sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ== dependencies: - array-flatten "^2.1.0" - deep-equal "^1.0.1" + array-flatten "^2.1.2" dns-equal "^1.0.0" - dns-txt "^2.0.2" - multicast-dns "^6.0.1" - multicast-dns-service-types "^1.1.0" + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" brace-expansion@^1.1.7: version "1.1.11" @@ -479,6 +544,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -507,11 +579,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer-indexof@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" - integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== - bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -522,7 +589,7 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind@^1.0.0, call-bind@^1.0.2: +call-bind@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== @@ -548,22 +615,7 @@ chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chokidar@3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" - integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chokidar@^3.5.1: +chokidar@3.5.3, chokidar@^3.5.1, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -583,11 +635,6 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -623,11 +670,6 @@ colorette@^2.0.10, colorette@^2.0.14: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.17.tgz#5dd4c0d15e2984b7433cb4a9f2ead45063b80c47" integrity sha512-hJo+3Bkn0NCHybn9Tu35fIeoOKGOk5OCC32y4Hz2It+qlCO2Q3DeQ1hRn/tDDMQKRYUEzqsl7jbF6dYKjlE60g== -colors@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" - integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== - commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -638,11 +680,6 @@ commander@^7.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== -component-emitter@~1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - compressible@~2.0.16: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -737,10 +774,10 @@ custom-event@~1.0.0: resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== -date-format@^4.0.10: - version "4.0.11" - resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.11.tgz#ae0d1e069d7f0687938fd06f98c12f3a6276e526" - integrity sha512-VS20KRyorrbMCQmpdl2hg5KaOUsda1RbnsJg461FfrcyCUg+pkd0b40BSW4niQyTheww4DBXQnS7HwSrKkipLw== +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== debug@2.6.9: version "2.6.9" @@ -749,21 +786,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== - dependencies: - ms "2.1.2" - -debug@^3.1.1: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@^4.1.0, debug@^4.3.4, debug@~4.3.1: +debug@4.3.4, debug@^4.1.0, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -775,19 +798,7 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== -deep-equal@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" - integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== - dependencies: - is-arguments "^1.0.4" - is-date-object "^1.0.1" - is-regex "^1.0.4" - object-is "^1.0.1" - object-keys "^1.1.1" - regexp.prototype.flags "^1.2.0" - -default-gateway@^6.0.0: +default-gateway@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== @@ -799,28 +810,6 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" - integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== - dependencies: - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -del@^6.0.0: - version "6.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-6.1.1.tgz#3b70314f1ec0aa325c6b14eb36b95786671edb7a" - integrity sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg== - dependencies: - globby "^11.0.1" - graceful-fs "^4.2.4" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.2" - p-map "^4.0.0" - rimraf "^3.0.2" - slash "^3.0.0" - depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -851,32 +840,17 @@ diff@5.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg== -dns-packet@^1.3.1: - version "1.3.4" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f" - integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA== +dns-packet@^5.2.2: + version "5.4.0" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b" + integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g== dependencies: - ip "^1.1.0" - safe-buffer "^5.0.1" - -dns-txt@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" - integrity sha512-Ix5PrWjphuSoUXV/Zv5gaFHjnaJtb02F2+Si3Ht9dyJ87+Z/lMmy+dpNHtTGraNK958ndXq2i+GLkWsWHcKaBQ== - dependencies: - buffer-indexof "^1.0.0" + "@leichtgewicht/ip-codec" "^2.0.1" dom-serialize@^2.2.1: version "2.2.1" @@ -908,30 +882,31 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -engine.io-parser@~4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.3.tgz#83d3a17acfd4226f19e721bb22a1ee8f7662d2f6" - integrity sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA== - dependencies: - base64-arraybuffer "0.1.4" +engine.io-parser@~5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0" + integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg== -engine.io@~4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-4.1.2.tgz#f96ceb56d4b39cc7ca5bd29a20e9c99c1ad1a765" - integrity sha512-t5z6zjXuVLhXDMiFJPYsPOWEER8B0tIsD3ETgw19S1yg9zryvUfY3Vhtk3Gf4sihw/bQGIqQ//gjvVlu+Ca0bQ== +engine.io@~6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.1.tgz#e3f7826ebc4140db9bbaa9021ad6b1efb175878f" + integrity sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA== dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" accepts "~1.3.4" base64id "2.0.0" cookie "~0.4.1" cors "~2.8.5" debug "~4.3.1" - engine.io-parser "~4.0.0" - ws "~7.4.2" + engine.io-parser "~5.0.3" + ws "~8.2.3" -enhanced-resolve@^5.8.3: - version "5.9.3" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88" - integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow== +enhanced-resolve@^5.9.3: + version "5.12.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" + integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -1021,14 +996,14 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -express@^4.17.1: - version "4.18.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" - integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== +express@^4.17.3: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.0" + body-parser "1.20.1" content-disposition "0.5.4" content-type "~1.0.4" cookie "0.5.0" @@ -1047,7 +1022,7 @@ express@^4.17.1: parseurl "~1.3.3" path-to-regexp "0.1.7" proxy-addr "~2.0.7" - qs "6.10.3" + qs "6.11.0" range-parser "~1.2.1" safe-buffer "5.2.1" send "0.18.0" @@ -1068,17 +1043,6 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.9: - version "3.2.11" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" - integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -1089,13 +1053,6 @@ fastest-levenshtein@^1.0.12: resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== -fastq@^1.6.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" - integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== - dependencies: - reusify "^1.0.4" - faye-websocket@^0.11.3: version "0.11.4" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" @@ -1157,10 +1114,10 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -flatted@^3.2.5: - version "3.2.5" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" - integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== +flatted@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== follow-redirects@^1.0.0: version "1.15.1" @@ -1182,14 +1139,14 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== -fs-extra@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" - integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== dependencies: graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" + jsonfile "^4.0.0" + universalify "^0.1.0" fs-monkey@1.0.3: version "1.0.3" @@ -1211,17 +1168,12 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -functions-have-names@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== - get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: +get-intrinsic@^1.0.2: version "1.1.2" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== @@ -1235,7 +1187,7 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -1247,10 +1199,10 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@7.1.7: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -1271,28 +1223,11 @@ glob@^7.1.3, glob@^7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" -globby@^11.0.1: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -growl@1.10.5: - version "1.10.5" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== - handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -1303,25 +1238,11 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== - dependencies: - get-intrinsic "^1.1.1" - -has-symbols@^1.0.2, has-symbols@^1.0.3: +has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -1380,7 +1301,7 @@ http-parser-js@>=0.5.1: resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.6.tgz#2e02406ab2df8af8a7abfba62e0da01c62b95afd" integrity sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA== -http-proxy-middleware@^2.0.0: +http-proxy-middleware@^2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== @@ -1412,18 +1333,13 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: +iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== - import-local@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" @@ -1432,11 +1348,6 @@ import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1455,32 +1366,12 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== -internal-ip@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-6.2.0.tgz#d5541e79716e406b74ac6b07b856ef18dc1621c1" - integrity sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg== - dependencies: - default-gateway "^6.0.0" - ipaddr.js "^1.9.1" - is-ip "^3.1.0" - p-event "^4.2.0" - interpret@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -ip-regex@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" - integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== - -ip@^1.1.0: - version "1.1.8" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" - integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg== - -ipaddr.js@1.9.1, ipaddr.js@^1.9.1: +ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== @@ -1490,14 +1381,6 @@ ipaddr.js@^2.0.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -1512,13 +1395,6 @@ is-core-module@^2.8.1: dependencies: has "^1.0.3" -is-date-object@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" @@ -1541,28 +1417,11 @@ is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-ip@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-3.1.0.tgz#2ae5ddfafaf05cb8008a62093cf29734f657c5d8" - integrity sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q== - dependencies: - ip-regex "^4.0.0" - is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-inside@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - is-plain-obj@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" @@ -1580,14 +1439,6 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-regex@^1.0.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -1641,10 +1492,10 @@ js-yaml@4.1.0: dependencies: argparse "^2.0.1" -json-parse-better-errors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-schema-traverse@^0.4.1: version "0.4.1" @@ -1656,19 +1507,17 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== optionalDependencies: graceful-fs "^4.1.6" -karma-chrome-launcher@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738" - integrity sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg== +karma-chrome-launcher@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz#baca9cc071b1562a1db241827257bfe5cab597ea" + integrity sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ== dependencies: which "^1.2.1" @@ -1695,15 +1544,15 @@ karma-webpack@5.0.0: minimatch "^3.0.4" webpack-merge "^4.1.5" -karma@6.3.4: - version "6.3.4" - resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.4.tgz#359899d3aab3d6b918ea0f57046fd2a6b68565e6" - integrity sha512-hbhRogUYIulfkBTZT7xoPrCYhRBnBoqbbL4fszWD0ReFGUxU+LYBr3dwKdAluaDQ/ynT9/7C+Lf7pPNW4gSx4Q== +karma@6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.0.tgz#82652dfecdd853ec227b74ed718a997028a99508" + integrity sha512-s8m7z0IF5g/bS5ONT7wsOavhW4i4aFkzD4u4wgzAQWT4HGUeWI3i21cK2Yz6jndMAeHETp5XuNsRoyGJZXVd4w== dependencies: + "@colors/colors" "1.5.0" body-parser "^1.19.0" braces "^3.0.2" chokidar "^3.5.1" - colors "^1.4.0" connect "^3.7.0" di "^0.0.1" dom-serialize "^2.2.1" @@ -1712,16 +1561,17 @@ karma@6.3.4: http-proxy "^1.18.1" isbinaryfile "^4.0.8" lodash "^4.17.21" - log4js "^6.3.0" + log4js "^6.4.1" mime "^2.5.2" minimatch "^3.0.4" + mkdirp "^0.5.5" qjobs "^1.2.0" range-parser "^1.2.1" rimraf "^3.0.2" - socket.io "^3.1.0" + socket.io "^4.4.1" source-map "^0.6.1" tmp "^0.2.1" - ua-parser-js "^0.7.28" + ua-parser-js "^0.7.30" yargs "^16.1.1" kind-of@^6.0.2: @@ -1748,7 +1598,7 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21: +lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1761,16 +1611,16 @@ log-symbols@4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" -log4js@^6.3.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.5.2.tgz#9ae371e5b3cb3a3a209c24686e5547f8670834e5" - integrity sha512-DXtpNtt+KDOMT7RHUDIur/WsSA3rntlUh9Zg4XCdV42wUuMmbFkl38+LZ92Z5QvQA7mD5kAVkLiBSEH/tvUB8A== +log4js@^6.4.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.7.1.tgz#06e12b1ac915dd1067146ffad8215f666f7d2c51" + integrity sha512-lzbd0Eq1HRdWM2abSD7mk6YIVY0AogGJzb/z+lqzRk+8+XJP+M6L1MS5FUSc3jjGru4dbKjEMJmqlsoYYpuivQ== dependencies: - date-format "^4.0.10" + date-format "^4.0.14" debug "^4.3.4" - flatted "^3.2.5" + flatted "^3.2.7" rfdc "^1.3.0" - streamroller "^3.1.1" + streamroller "^3.1.3" media-typer@0.3.0: version "0.3.0" @@ -1794,17 +1644,12 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.2, micromatch@^4.0.4: +micromatch@^4.0.2: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -1844,12 +1689,12 @@ minimalistic-assert@^1.0.0: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== dependencies: - brace-expansion "^1.1.7" + brace-expansion "^2.0.1" minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" @@ -1870,32 +1715,30 @@ mkdirp@^0.5.5: dependencies: minimist "^1.2.6" -mocha@9.1.2: - version "9.1.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.1.2.tgz#93f53175b0f0dc4014bd2d612218fccfcf3534d3" - integrity sha512-ta3LtJ+63RIBP03VBjMGtSqbe6cWXRejF9SyM9Zyli1CKZJZ+vfCTj3oW24V7wAphMJdpOFLoMI3hjJ1LWbs0w== +mocha@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.0.0.tgz#205447d8993ec755335c4b13deba3d3a13c4def9" + integrity sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA== dependencies: "@ungap/promise-all-settled" "1.1.2" ansi-colors "4.1.1" browser-stdout "1.3.1" - chokidar "3.5.2" - debug "4.3.2" + chokidar "3.5.3" + debug "4.3.4" diff "5.0.0" escape-string-regexp "4.0.0" find-up "5.0.0" - glob "7.1.7" - growl "1.10.5" + glob "7.2.0" he "1.2.0" js-yaml "4.1.0" log-symbols "4.1.0" - minimatch "3.0.4" + minimatch "5.0.1" ms "2.1.3" - nanoid "3.1.25" + nanoid "3.3.3" serialize-javascript "6.0.0" strip-json-comments "3.1.1" supports-color "8.1.1" - which "2.0.2" - workerpool "6.1.5" + workerpool "6.2.1" yargs "16.2.0" yargs-parser "20.2.4" yargs-unparser "2.0.0" @@ -1910,28 +1753,23 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -multicast-dns-service-types@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" - integrity sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ== - -multicast-dns@^6.0.1: - version "6.2.3" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" - integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== dependencies: - dns-packet "^1.3.1" + dns-packet "^5.2.2" thunky "^1.0.2" -nanoid@3.1.25: - version "3.1.25" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152" - integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q== +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== negotiator@0.6.3: version "0.6.3" @@ -1943,10 +1781,10 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -node-forge@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" - integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== node-releases@^2.0.5: version "2.0.5" @@ -1975,19 +1813,6 @@ object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== -object-is@^1.0.1: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" @@ -2035,18 +1860,6 @@ open@^8.0.9: is-docker "^2.1.1" is-wsl "^2.2.0" -p-event@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5" - integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ== - dependencies: - p-timeout "^3.1.0" - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== - p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -2075,13 +1888,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - p-retry@^4.5.0: version "4.6.2" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" @@ -2090,13 +1896,6 @@ p-retry@^4.5.0: "@types/retry" "0.12.0" retry "^0.13.1" -p-timeout@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" - integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== - dependencies: - p-finally "^1.0.0" - p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -2132,11 +1931,6 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -2154,15 +1948,6 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -portfinder@^1.0.28: - version "1.0.28" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" - integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== - dependencies: - async "^2.6.2" - debug "^3.1.1" - mkdirp "^0.5.5" - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -2176,11 +1961,6 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== - punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -2198,15 +1978,12 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" randombytes@^2.1.0: version "2.1.0" @@ -2266,15 +2043,6 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" -regexp.prototype.flags@^1.2.0: - version "1.4.3" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" - integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - functions-have-names "^1.2.2" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -2316,11 +2084,6 @@ retry@^0.13.1: resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - rfdc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" @@ -2333,19 +2096,12 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -2379,12 +2135,12 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== -selfsigned@^1.10.11: - version "1.10.14" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.14.tgz#ee51d84d9dcecc61e07e4aba34f229ab525c1574" - integrity sha512-lkjaiAye+wBZDCBsu5BGi0XiLRxeUlsGod5ZP924CRSEoGuZAw/f7y9RKu28rwTfiHVhdavhB0qH0INV6P1lEA== +selfsigned@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.1.1.tgz#18a7613d714c0cd3385c48af0075abf3f266af61" + integrity sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ== dependencies: - node-forge "^0.10.0" + node-forge "^1" send@0.18.0: version "0.18.0" @@ -2478,41 +2234,32 @@ signal-exit@^3.0.3: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +socket.io-adapter@~2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz#b50a4a9ecdd00c34d4c8c808224daa1a786152a6" + integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg== -socket.io-adapter@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz#edc5dc36602f2985918d631c1399215e97a1b527" - integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg== - -socket.io-parser@~4.0.3: - version "4.0.4" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0" - integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g== +socket.io-parser@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5" + integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g== dependencies: - "@types/component-emitter" "^1.2.10" - component-emitter "~1.3.0" + "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" -socket.io@^3.1.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.2.tgz#06e27caa1c4fc9617547acfbb5da9bc1747da39a" - integrity sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw== +socket.io@^4.4.1: + version "4.5.4" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.5.4.tgz#a4513f06e87451c17013b8d13fdfaf8da5a86a90" + integrity sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ== dependencies: - "@types/cookie" "^0.4.0" - "@types/cors" "^2.8.8" - "@types/node" ">=10.0.0" accepts "~1.3.4" base64id "~2.0.0" - debug "~4.3.1" - engine.io "~4.1.0" - socket.io-adapter "~2.1.0" - socket.io-parser "~4.0.3" + debug "~4.3.2" + engine.io "~6.2.1" + socket.io-adapter "~2.4.0" + socket.io-parser "~4.2.1" -sockjs@^0.3.21: +sockjs@^0.3.24: version "0.3.24" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== @@ -2521,29 +2268,21 @@ sockjs@^0.3.21: uuid "^8.3.2" websocket-driver "^0.7.4" -source-map-js@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" - integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -source-map-loader@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.0.tgz#f2a04ee2808ad01c774dea6b7d2639839f3b3049" - integrity sha512-GKGWqWvYr04M7tn8dryIWvb0s8YM41z82iQv01yBtIylgxax0CwvSy6gc2Y02iuXwEfGWRlMicH0nvms9UZphw== +source-map-loader@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-4.0.0.tgz#bdc6b118bc6c87ee4d8d851f2d4efcc5abdb2ef5" + integrity sha512-i3KVgM3+QPAHNbGavK+VBq03YoJl24m9JWNbLgsjTj8aJzXG9M61bantBTNBt7CNwY2FYf+RJRYJ3pzalKjIrw== dependencies: - abab "^2.0.5" - iconv-lite "^0.6.2" - source-map-js "^0.6.2" + abab "^2.0.6" + iconv-lite "^0.6.3" + source-map-js "^1.0.2" -source-map-support@0.5.20: - version "0.5.20" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" - integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@~0.5.20: +source-map-support@0.5.21, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -2589,14 +2328,14 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -streamroller@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.1.tgz#679aae10a4703acdf2740755307df0a05ad752e6" - integrity sha512-iPhtd9unZ6zKdWgMeYGfSBuqCngyJy1B/GPi/lTpwGpa3bajuX30GjUVd0/Tn/Xhg0mr4DOSENozz9Y06qyonQ== +streamroller@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.3.tgz#d95689a8c29b30d093525d0baffe6616fd62ca7e" + integrity sha512-CphIJyFx2SALGHeINanjFRKQ4l7x2c+rXYJ4BMq0gd+ZK0gi4VT8b+eHe2wi58x4UayBAKx4xtHpXT/ea1cz8w== dependencies: - date-format "^4.0.10" + date-format "^4.0.14" debug "^4.3.4" - fs-extra "^10.1.0" + fs-extra "^8.1.0" string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" @@ -2628,13 +2367,6 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" - integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== - dependencies: - ansi-regex "^6.0.1" - strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" @@ -2722,15 +2454,15 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -ua-parser-js@^0.7.28: - version "0.7.31" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" - integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== +ua-parser-js@^0.7.30: + version "0.7.32" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.32.tgz#cd8c639cdca949e30fa68c44b7813ef13e36d211" + integrity sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw== -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" @@ -2744,14 +2476,6 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -2767,11 +2491,6 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -v8-compile-cache@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -2782,7 +2501,7 @@ void-elements@^2.0.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= -watchpack@^2.2.0: +watchpack@^2.3.1: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== @@ -2797,26 +2516,25 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -webpack-cli@4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.9.0.tgz#dc43e6e0f80dd52e89cbf73d5294bcd7ad6eb343" - integrity sha512-n/jZZBMzVEl4PYIBs+auy2WI0WTQ74EnJDiyD98O2JZY6IVIHJNitkYp/uTXOviIOMfgzrNvC9foKv/8o8KSZw== +webpack-cli@4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31" + integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== dependencies: "@discoveryjs/json-ext" "^0.5.0" - "@webpack-cli/configtest" "^1.1.0" - "@webpack-cli/info" "^1.4.0" - "@webpack-cli/serve" "^1.6.0" + "@webpack-cli/configtest" "^1.2.0" + "@webpack-cli/info" "^1.5.0" + "@webpack-cli/serve" "^1.7.0" colorette "^2.0.14" commander "^7.0.0" - execa "^5.0.0" + cross-spawn "^7.0.3" fastest-levenshtein "^1.0.12" import-local "^3.0.2" interpret "^2.2.0" rechoir "^0.7.0" - v8-compile-cache "^2.2.0" webpack-merge "^5.7.3" -webpack-dev-middleware@^5.2.1: +webpack-dev-middleware@^5.3.1: version "5.3.3" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== @@ -2827,36 +2545,40 @@ webpack-dev-middleware@^5.2.1: range-parser "^1.2.1" schema-utils "^4.0.0" -webpack-dev-server@4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.3.1.tgz#759d3337f0fbea297fbd1e433ab04ccfc000076b" - integrity sha512-qNXQCVYo1kYhH9pgLtm8LRNkXX3XzTfHSj/zqzaqYzGPca+Qjr+81wj1jgPMCHhIhso9WEQ+kX9z23iG9PzQ7w== +webpack-dev-server@4.9.2: + version "4.9.2" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.9.2.tgz#c188db28c7bff12f87deda2a5595679ebbc3c9bc" + integrity sha512-H95Ns95dP24ZsEzO6G9iT+PNw4Q7ltll1GfJHV4fKphuHWgKFzGHWi4alTlTnpk1SPPk41X+l2RB7rLfIhnB9Q== dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" + "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.5.1" ansi-html-community "^0.0.8" - bonjour "^3.5.0" - chokidar "^3.5.1" + bonjour-service "^1.0.11" + chokidar "^3.5.3" colorette "^2.0.10" compression "^1.7.4" connect-history-api-fallback "^1.6.0" - del "^6.0.0" - express "^4.17.1" + default-gateway "^6.0.3" + express "^4.17.3" graceful-fs "^4.2.6" html-entities "^2.3.2" - http-proxy-middleware "^2.0.0" - internal-ip "^6.2.0" + http-proxy-middleware "^2.0.3" ipaddr.js "^2.0.1" open "^8.0.9" p-retry "^4.5.0" - portfinder "^1.0.28" - schema-utils "^3.1.0" - selfsigned "^1.10.11" + rimraf "^3.0.2" + schema-utils "^4.0.0" + selfsigned "^2.0.1" serve-index "^1.9.1" - sockjs "^0.3.21" + sockjs "^0.3.24" spdy "^4.0.2" - strip-ansi "^7.0.0" - url "^0.11.0" - webpack-dev-middleware "^5.2.1" - ws "^8.1.0" + webpack-dev-middleware "^5.3.1" + ws "^8.4.2" webpack-merge@^4.1.5: version "4.2.2" @@ -2873,18 +2595,18 @@ webpack-merge@^5.7.3: clone-deep "^4.0.1" wildcard "^2.0.0" -webpack-sources@^3.2.0: +webpack-sources@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@5.57.1: - version "5.57.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.57.1.tgz#ead5ace2c17ecef2ae8126f143bfeaa7f55eab44" - integrity sha512-kHszukYjTPVfCOEyrUthA3jqJwduY/P3eO8I0gMNOZGIQWKAwZftxmp5hq6paophvwo9NoUrcZOecs9ulOyyTg== +webpack@5.73.0: + version "5.73.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.73.0.tgz#bbd17738f8a53ee5760ea2f59dce7f3431d35d38" + integrity sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA== dependencies: - "@types/eslint-scope" "^3.7.0" - "@types/estree" "^0.0.50" + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" "@webassemblyjs/ast" "1.11.1" "@webassemblyjs/wasm-edit" "1.11.1" "@webassemblyjs/wasm-parser" "1.11.1" @@ -2892,21 +2614,21 @@ webpack@5.57.1: acorn-import-assertions "^1.7.6" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.8.3" + enhanced-resolve "^5.9.3" es-module-lexer "^0.9.0" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.4" - json-parse-better-errors "^1.0.2" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" schema-utils "^3.1.0" tapable "^2.1.1" terser-webpack-plugin "^5.1.3" - watchpack "^2.2.0" - webpack-sources "^3.2.0" + watchpack "^2.3.1" + webpack-sources "^3.2.3" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" @@ -2922,13 +2644,6 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== -which@2.0.2, which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - which@^1.2.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -2936,15 +2651,22 @@ which@^1.2.1: dependencies: isexe "^2.0.0" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== -workerpool@6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.5.tgz#0f7cf076b6215fd7e1da903ff6f22ddd1886b581" - integrity sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw== +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== wrap-ansi@^7.0.0: version "7.0.0" @@ -2960,15 +2682,15 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@^8.1.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.7.0.tgz#eaf9d874b433aa00c0e0d8752532444875db3957" - integrity sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg== +ws@^8.4.2: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== -ws@~7.4.2: - version "7.4.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" - integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@~8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== y18n@^5.0.5: version "5.0.8" diff --git a/okhttp/api/okhttp.api b/okhttp/api/okhttp.api index e7ccc19a4..de2bd33bc 100644 --- a/okhttp/api/okhttp.api +++ b/okhttp/api/okhttp.api @@ -693,7 +693,6 @@ public final class okhttp3/HttpUrl { } public final class okhttp3/HttpUrl$Builder { - public static final field Companion Lokhttp3/HttpUrl$Builder$Companion; public fun ()V public final fun addEncodedPathSegment (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun addEncodedPathSegments (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; @@ -724,9 +723,6 @@ public final class okhttp3/HttpUrl$Builder { public final fun username (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; } -public final class okhttp3/HttpUrl$Builder$Companion { -} - public final class okhttp3/HttpUrl$Companion { public final fun -deprecated_get (Ljava/lang/String;)Lokhttp3/HttpUrl; public final fun -deprecated_get (Ljava/net/URI;)Lokhttp3/HttpUrl; diff --git a/okhttp/src/commonMain/kotlin/okhttp3/HttpUrl.kt b/okhttp/src/commonMain/kotlin/okhttp3/HttpUrl.kt new file mode 100644 index 000000000..510a34085 --- /dev/null +++ b/okhttp/src/commonMain/kotlin/okhttp3/HttpUrl.kt @@ -0,0 +1,679 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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 okhttp3 + +import kotlin.jvm.JvmName +import kotlin.jvm.JvmStatic + +/** + * A uniform resource locator (URL) with a scheme of either `http` or `https`. Use this class to + * compose and decompose Internet addresses. For example, this code will compose and print a URL for + * Google search: + * + * ```java + * HttpUrl url = new HttpUrl.Builder() + * .scheme("https") + * .host("www.google.com") + * .addPathSegment("search") + * .addQueryParameter("q", "polar bears") + * .build(); + * System.out.println(url); + * ``` + * + * which prints: + * + * ``` + * https://www.google.com/search?q=polar%20bears + * ``` + * + * As another example, this code prints the human-readable query parameters of a Twitter search: + * + * ```java + * HttpUrl url = HttpUrl.parse("https://twitter.com/search?q=cute%20%23puppies&f=images"); + * for (int i = 0, size = url.querySize(); i < size; i++) { + * System.out.println(url.queryParameterName(i) + ": " + url.queryParameterValue(i)); + * } + * ``` + * + * which prints: + * + * ``` + * q: cute #puppies + * f: images + * ``` + * + * In addition to composing URLs from their component parts and decomposing URLs into their + * component parts, this class implements relative URL resolution: what address you'd reach by + * clicking a relative link on a specified page. For example: + * + * ```java + * HttpUrl base = HttpUrl.parse("https://www.youtube.com/user/WatchTheDaily/videos"); + * HttpUrl link = base.resolve("../../watch?v=cbP2N1BQdYc"); + * System.out.println(link); + * ``` + * + * which prints: + * + * ``` + * https://www.youtube.com/watch?v=cbP2N1BQdYc + * ``` + * + * ## What's in a URL? + * + * A URL has several components. + * + * ### Scheme + * + * Sometimes referred to as *protocol*, A URL's scheme describes what mechanism should be used to + * retrieve the resource. Although URLs have many schemes (`mailto`, `file`, `ftp`), this class only + * supports `http` and `https`. Use [java.net.URI][URI] for URLs with arbitrary schemes. + * + * ### Username and Password + * + * Username and password are either present, or the empty string `""` if absent. This class offers + * no mechanism to differentiate empty from absent. Neither of these components are popular in + * practice. Typically HTTP applications use other mechanisms for user identification and + * authentication. + * + * ### Host + * + * The host identifies the webserver that serves the URL's resource. It is either a hostname like + * `square.com` or `localhost`, an IPv4 address like `192.168.0.1`, or an IPv6 address like `::1`. + * + * Usually a webserver is reachable with multiple identifiers: its IP addresses, registered + * domain names, and even `localhost` when connecting from the server itself. Each of a web server's + * names is a distinct URL and they are not interchangeable. For example, even if + * `http://square.github.io/dagger` and `http://google.github.io/dagger` are served by the same IP + * address, the two URLs identify different resources. + * + * ### Port + * + * The port used to connect to the web server. By default this is 80 for HTTP and 443 for HTTPS. + * This class never returns -1 for the port: if no port is explicitly specified in the URL then the + * scheme's default is used. + * + * ### Path + * + * The path identifies a specific resource on the host. Paths have a hierarchical structure like + * "/square/okhttp/issues/1486" and decompose into a list of segments like `["square", "okhttp", + * "issues", "1486"]`. + * + * This class offers methods to compose and decompose paths by segment. It composes each path + * from a list of segments by alternating between "/" and the encoded segment. For example the + * segments `["a", "b"]` build "/a/b" and the segments `["a", "b", ""]` build "/a/b/". + * + * If a path's last segment is the empty string then the path ends with "/". This class always + * builds non-empty paths: if the path is omitted it defaults to "/". The default path's segment + * list is a single empty string: `[""]`. + * + * ### Query + * + * The query is optional: it can be null, empty, or non-empty. For many HTTP URLs the query string + * is subdivided into a collection of name-value parameters. This class offers methods to set the + * query as the single string, or as individual name-value parameters. With name-value parameters + * the values are optional and names may be repeated. + * + * ### Fragment + * + * The fragment is optional: it can be null, empty, or non-empty. Unlike host, port, path, and + * query the fragment is not sent to the webserver: it's private to the client. + * + * ## Encoding + * + * Each component must be encoded before it is embedded in the complete URL. As we saw above, the + * string `cute #puppies` is encoded as `cute%20%23puppies` when used as a query parameter value. + * + * ### Percent encoding + * + * Percent encoding replaces a character (like `\ud83c\udf69`) with its UTF-8 hex bytes (like + * `%F0%9F%8D%A9`). This approach works for whitespace characters, control characters, non-ASCII + * characters, and characters that already have another meaning in a particular context. + * + * Percent encoding is used in every URL component except for the hostname. But the set of + * characters that need to be encoded is different for each component. For example, the path + * component must escape all of its `?` characters, otherwise it could be interpreted as the + * start of the URL's query. But within the query and fragment components, the `?` character + * doesn't delimit anything and doesn't need to be escaped. + * + * ```java + * HttpUrl url = HttpUrl.parse("http://who-let-the-dogs.out").newBuilder() + * .addPathSegment("_Who?_") + * .query("_Who?_") + * .fragment("_Who?_") + * .build(); + * System.out.println(url); + * ``` + * + * This prints: + * + * ``` + * http://who-let-the-dogs.out/_Who%3F_?_Who?_#_Who?_ + * ``` + * + * When parsing URLs that lack percent encoding where it is required, this class will percent encode + * the offending characters. + * + * ### IDNA Mapping and Punycode encoding + * + * Hostnames have different requirements and use a different encoding scheme. It consists of IDNA + * mapping and Punycode encoding. + * + * In order to avoid confusion and discourage phishing attacks, [IDNA Mapping][idna] transforms + * names to avoid confusing characters. This includes basic case folding: transforming shouting + * `SQUARE.COM` into cool and casual `square.com`. It also handles more exotic characters. For + * example, the Unicode trademark sign (™) could be confused for the letters "TM" in + * `http://ho™ail.com`. To mitigate this, the single character (™) maps to the string (tm). There + * is similar policy for all of the 1.1 million Unicode code points. Note that some code points such + * as "\ud83c\udf69" are not mapped and cannot be used in a hostname. + * + * [Punycode](http://ietf.org/rfc/rfc3492.txt) converts a Unicode string to an ASCII string to make + * international domain names work everywhere. For example, "σ" encodes as "xn--4xa". The encoded + * string is not human readable, but can be used with classes like [InetAddress] to establish + * connections. + * + * ## Why another URL model? + * + * Java includes both [java.net.URL][URL] and [java.net.URI][URI]. We offer a new URL + * model to address problems that the others don't. + * + * ### Different URLs should be different + * + * Although they have different content, `java.net.URL` considers the following two URLs + * equal, and the [equals()][Object.equals] method between them returns true: + * + * * https://example.net/ + * + * * https://example.com/ + * + * This is because those two hosts share the same IP address. This is an old, bad design decision + * that makes `java.net.URL` unusable for many things. It shouldn't be used as a [Map] key or in a + * [Set]. Doing so is both inefficient because equality may require a DNS lookup, and incorrect + * because unequal URLs may be equal because of how they are hosted. + * + * ### Equal URLs should be equal + * + * These two URLs are semantically identical, but `java.net.URI` disagrees: + * + * * http://host:80/ + * + * * http://host + * + * Both the unnecessary port specification (`:80`) and the absent trailing slash (`/`) cause URI to + * bucket the two URLs separately. This harms URI's usefulness in collections. Any application that + * stores information-per-URL will need to either canonicalize manually, or suffer unnecessary + * redundancy for such URLs. + * + * Because they don't attempt canonical form, these classes are surprisingly difficult to use + * securely. Suppose you're building a webservice that checks that incoming paths are prefixed + * "/static/images/" before serving the corresponding assets from the filesystem. + * + * ```java + * String attack = "http://example.com/static/images/../../../../../etc/passwd"; + * System.out.println(new URL(attack).getPath()); + * System.out.println(new URI(attack).getPath()); + * System.out.println(HttpUrl.parse(attack).encodedPath()); + * ``` + * + * By canonicalizing the input paths, they are complicit in directory traversal attacks. Code that + * checks only the path prefix may suffer! + * + * ``` + * /static/images/../../../../../etc/passwd + * /static/images/../../../../../etc/passwd + * /etc/passwd + * ``` + * + * ### If it works on the web, it should work in your application + * + * The `java.net.URI` class is strict around what URLs it accepts. It rejects URLs like + * `http://example.com/abc|def` because the `|` character is unsupported. This class is more + * forgiving: it will automatically percent-encode the `|'` yielding `http://example.com/abc%7Cdef`. + * This kind behavior is consistent with web browsers. `HttpUrl` prefers consistency with major web + * browsers over consistency with obsolete specifications. + * + * ### Paths and Queries should decompose + * + * Neither of the built-in URL models offer direct access to path segments or query parameters. + * Manually using `StringBuilder` to assemble these components is cumbersome: do '+' characters get + * silently replaced with spaces? If a query parameter contains a '&', does that get escaped? + * By offering methods to read and write individual query parameters directly, application + * developers are saved from the hassles of encoding and decoding. + * + * ### Plus a modern API + * + * The URL (JDK1.0) and URI (Java 1.4) classes predate builders and instead use telescoping + * constructors. For example, there's no API to compose a URI with a custom port without also + * providing a query and fragment. + * + * Instances of [HttpUrl] are well-formed and always have a scheme, host, and path. With + * `java.net.URL` it's possible to create an awkward URL like `http:/` with scheme and path but no + * hostname. Building APIs that consume such malformed values is difficult! + * + * This class has a modern API. It avoids punitive checked exceptions: [toHttpUrl] throws + * [IllegalArgumentException] on invalid input or [toHttpUrlOrNull] returns null if the input is an + * invalid URL. You can even be explicit about whether each component has been encoded already. + * + * [idna]: http://www.unicode.org/reports/tr46/#ToASCII + */ +expect class HttpUrl internal constructor( + scheme: String, + username: String, + password: String, + host: String, + port: Int, + pathSegments: List, + queryNamesAndValues: List?, + fragment: String?, + url: String +) { + + /** Either "http" or "https". */ + val scheme: String + + /** + * The decoded username, or an empty string if none is present. + * + * | URL | `username()` | + * | :------------------------------- | :----------- | + * | `http://host/` | `""` | + * | `http://username@host/` | `"username"` | + * | `http://username:password@host/` | `"username"` | + * | `http://a%20b:c%20d@host/` | `"a b"` | + */ + val username: String + + /** + * Returns the decoded password, or an empty string if none is present. + * + * | URL | `password()` | + * | :------------------------------- | :----------- | + * | `http://host/` | `""` | + * | `http://username@host/` | `""` | + * | `http://username:password@host/` | `"password"` | + * | `http://a%20b:c%20d@host/` | `"c d"` | + */ + val password: String + + /** + * The host address suitable for use with [InetAddress.getAllByName]. May be: + * + * * A regular host name, like `android.com`. + * + * * An IPv4 address, like `127.0.0.1`. + * + * * An IPv6 address, like `::1`. Note that there are no square braces. + * + * * An encoded IDN, like `xn--n3h.net`. + * + * | URL | `host()` | + * | :-------------------- | :-------------- | + * | `http://android.com/` | `"android.com"` | + * | `http://127.0.0.1/` | `"127.0.0.1"` | + * | `http://[::1]/` | `"::1"` | + * | `http://xn--n3h.net/` | `"xn--n3h.net"` | + */ + val host: String + + /** + * The explicitly-specified port if one was provided, or the default port for this URL's scheme. + * For example, this returns 8443 for `https://square.com:8443/` and 443 for + * `https://square.com/`. The result is in `[1..65535]`. + * + * | URL | `port()` | + * | :------------------ | :------- | + * | `http://host/` | `80` | + * | `http://host:8000/` | `8000` | + * | `https://host/` | `443` | + */ + val port: Int + + /** + * A list of path segments like `["a", "b", "c"]` for the URL `http://host/a/b/c`. This list is + * never empty though it may contain a single empty string. + * + * | URL | `pathSegments()` | + * | :----------------------- | :------------------ | + * | `http://host/` | `[""]` | + * | `http://host/a/b/c"` | `["a", "b", "c"]` | + * | `http://host/a/b%20c/d"` | `["a", "b c", "d"]` | + */ + val pathSegments: List + + /** + * This URL's fragment, like `"abc"` for `http://host/#abc`. This is null if the URL has no + * fragment. + * + * | URL | `fragment()` | + * | :--------------------- | :----------- | + * | `http://host/` | null | + * | `http://host/#` | `""` | + * | `http://host/#abc` | `"abc"` | + * | `http://host/#abc|def` | `"abc|def"` | + */ + val fragment: String? + + val isHttps: Boolean + + /** + * The username, or an empty string if none is set. + * + * | URL | `encodedUsername()` | + * | :------------------------------- | :------------------ | + * | `http://host/` | `""` | + * | `http://username@host/` | `"username"` | + * | `http://username:password@host/` | `"username"` | + * | `http://a%20b:c%20d@host/` | `"a%20b"` | + */ + val encodedUsername: String + + /** + * The password, or an empty string if none is set. + * + * | URL | `encodedPassword()` | + * | :--------------------------------| :------------------ | + * | `http://host/` | `""` | + * | `http://username@host/` | `""` | + * | `http://username:password@host/` | `"password"` | + * | `http://a%20b:c%20d@host/` | `"c%20d"` | + */ + val encodedPassword: String + + /** + * The number of segments in this URL's path. This is also the number of slashes in this URL's + * path, like 3 in `http://host/a/b/c`. This is always at least 1. + * + * | URL | `pathSize()` | + * | :------------------- | :----------- | + * | `http://host/` | `1` | + * | `http://host/a/b/c` | `3` | + * | `http://host/a/b/c/` | `4` | + */ + val pathSize: Int + + /** + * The entire path of this URL encoded for use in HTTP resource resolution. The returned path will + * start with `"/"`. + * + * | URL | `encodedPath()` | + * | :---------------------- | :-------------- | + * | `http://host/` | `"/"` | + * | `http://host/a/b/c` | `"/a/b/c"` | + * | `http://host/a/b%20c/d` | `"/a/b%20c/d"` | + */ + val encodedPath: String + + /** + * A list of encoded path segments like `["a", "b", "c"]` for the URL `http://host/a/b/c`. This + * list is never empty though it may contain a single empty string. + * + * | URL | `encodedPathSegments()` | + * | :---------------------- | :---------------------- | + * | `http://host/` | `[""]` | + * | `http://host/a/b/c` | `["a", "b", "c"]` | + * | `http://host/a/b%20c/d` | `["a", "b%20c", "d"]` | + */ + val encodedPathSegments: List + + /** + * The query of this URL, encoded for use in HTTP resource resolution. This string may be null + * (for URLs with no query), empty (for URLs with an empty query) or non-empty (all other URLs). + * + * | URL | `encodedQuery()` | + * | :-------------------------------- | :--------------------- | + * | `http://host/` | null | + * | `http://host/?` | `""` | + * | `http://host/?a=apple&k=key+lime` | `"a=apple&k=key+lime"` | + * | `http://host/?a=apple&a=apricot` | `"a=apple&a=apricot"` | + * | `http://host/?a=apple&b` | `"a=apple&b"` | + */ + val encodedQuery: String? + + /** + * This URL's query, like `"abc"` for `http://host/?abc`. Most callers should prefer + * [queryParameterName] and [queryParameterValue] because these methods offer direct access to + * individual query parameters. + * + * | URL | `query()` | + * | :-------------------------------- | :--------------------- | + * | `http://host/` | null | + * | `http://host/?` | `""` | + * | `http://host/?a=apple&k=key+lime` | `"a=apple&k=key lime"` | + * | `http://host/?a=apple&a=apricot` | `"a=apple&a=apricot"` | + * | `http://host/?a=apple&b` | `"a=apple&b"` | + */ + val query: String? + + /** + * The number of query parameters in this URL, like 2 for `http://host/?a=apple&b=banana`. If this + * URL has no query this is 0. Otherwise it is one more than the number of `"&"` separators in the + * query. + * + * | URL | `querySize()` | + * | :-------------------------------- | :------------ | + * | `http://host/` | `0` | + * | `http://host/?` | `1` | + * | `http://host/?a=apple&k=key+lime` | `2` | + * | `http://host/?a=apple&a=apricot` | `2` | + * | `http://host/?a=apple&b` | `2` | + */ + val querySize: Int + + /** + * The first query parameter named `name` decoded using UTF-8, or null if there is no such query + * parameter. + * + * | URL | `queryParameter("a")` | + * | :-------------------------------- | :-------------------- | + * | `http://host/` | null | + * | `http://host/?` | null | + * | `http://host/?a=apple&k=key+lime` | `"apple"` | + * | `http://host/?a=apple&a=apricot` | `"apple"` | + * | `http://host/?a=apple&b` | `"apple"` | + */ + fun queryParameter(name: String): String? + + /** + * The distinct query parameter names in this URL, like `["a", "b"]` for + * `http://host/?a=apple&b=banana`. If this URL has no query this is the empty set. + * + * | URL | `queryParameterNames()` | + * | :-------------------------------- | :---------------------- | + * | `http://host/` | `[]` | + * | `http://host/?` | `[""]` | + * | `http://host/?a=apple&k=key+lime` | `["a", "k"]` | + * | `http://host/?a=apple&a=apricot` | `["a"]` | + * | `http://host/?a=apple&b` | `["a", "b"]` | + */ + val queryParameterNames: Set + + internal val url: String + + internal val queryNamesAndValues: List? + + /** + * Returns all values for the query parameter `name` ordered by their appearance in this + * URL. For example this returns `["banana"]` for `queryParameterValue("b")` on + * `http://host/?a=apple&b=banana`. + * + * | URL | `queryParameterValues("a")` | `queryParameterValues("b")` | + * | :-------------------------------- | :-------------------------- | :-------------------------- | + * | `http://host/` | `[]` | `[]` | + * | `http://host/?` | `[]` | `[]` | + * | `http://host/?a=apple&k=key+lime` | `["apple"]` | `[]` | + * | `http://host/?a=apple&a=apricot` | `["apple", "apricot"]` | `[]` | + * | `http://host/?a=apple&b` | `["apple"]` | `[null]` | + */ + fun queryParameterValues(name: String): List + + /** + * Returns the name of the query parameter at `index`. For example this returns `"a"` + * for `queryParameterName(0)` on `http://host/?a=apple&b=banana`. This throws if + * `index` is not less than the [query size][querySize]. + * + * | URL | `queryParameterName(0)` | `queryParameterName(1)` | + * | :-------------------------------- | :---------------------- | :---------------------- | + * | `http://host/` | exception | exception | + * | `http://host/?` | `""` | exception | + * | `http://host/?a=apple&k=key+lime` | `"a"` | `"k"` | + * | `http://host/?a=apple&a=apricot` | `"a"` | `"a"` | + * | `http://host/?a=apple&b` | `"a"` | `"b"` | + */ + fun queryParameterName(index: Int): String + + /** + * Returns the value of the query parameter at `index`. For example this returns `"apple"` for + * `queryParameterName(0)` on `http://host/?a=apple&b=banana`. This throws if `index` is not less + * than the [query size][querySize]. + * + * | URL | `queryParameterValue(0)` | `queryParameterValue(1)` | + * | :-------------------------------- | :----------------------- | :----------------------- | + * | `http://host/` | exception | exception | + * | `http://host/?` | null | exception | + * | `http://host/?a=apple&k=key+lime` | `"apple"` | `"key lime"` | + * | `http://host/?a=apple&a=apricot` | `"apple"` | `"apricot"` | + * | `http://host/?a=apple&b` | `"apple"` | null | + */ + fun queryParameterValue(index: Int): String? + + /** + * This URL's encoded fragment, like `"abc"` for `http://host/#abc`. This is null if the URL has + * no fragment. + * + * | URL | `encodedFragment()` | + * | :--------------------- | :------------------ | + * | `http://host/` | null | + * | `http://host/#` | `""` | + * | `http://host/#abc` | `"abc"` | + * | `http://host/#abc|def` | `"abc|def"` | + */ + @get:JvmName("encodedFragment") val encodedFragment: String? + + /** + * Returns a string with containing this URL with its username, password, query, and fragment + * stripped, and its path replaced with `/...`. For example, redacting + * `http://username:password@example.com/path` returns `http://example.com/...`. + */ + fun redact(): String + + /** + * Returns the URL that would be retrieved by following `link` from this URL, or null if the + * resulting URL is not well-formed. + */ + fun resolve(link: String): HttpUrl? + + /** + * Returns a builder based on this URL. + */ + fun newBuilder(): Builder + + /** + * Returns a builder for the URL that would be retrieved by following `link` from this URL, + * or null if the resulting URL is not well-formed. + */ + fun newBuilder(link: String): Builder? + + class Builder constructor() { + internal var scheme: String? + internal var encodedUsername: String + internal var encodedPassword: String + internal var host: String? + internal var port: Int + internal val encodedPathSegments: MutableList + internal var encodedQueryNamesAndValues: MutableList? + internal var encodedFragment: String? + + fun scheme(scheme: String): Builder + fun username(username: String): Builder + + fun encodedUsername(encodedUsername: String): Builder + + fun password(password: String): Builder + + fun encodedPassword(encodedPassword: String): Builder + + fun host(host: String): Builder + fun port(port: Int): Builder + + fun addPathSegment(pathSegment: String): Builder + + /** + * Adds a set of path segments separated by a slash (either `\` or `/`). If `pathSegments` + * starts with a slash, the resulting URL will have empty path segment. + */ + fun addPathSegments(pathSegments: String): Builder + + fun addEncodedPathSegment(encodedPathSegment: String): Builder + + /** + * Adds a set of encoded path segments separated by a slash (either `\` or `/`). If + * `encodedPathSegments` starts with a slash, the resulting URL will have empty path segment. + */ + fun addEncodedPathSegments(encodedPathSegments: String): Builder + + fun setPathSegment(index: Int, pathSegment: String): Builder + + fun setEncodedPathSegment(index: Int, encodedPathSegment: String): Builder + + fun removePathSegment(index: Int): Builder + + fun encodedPath(encodedPath: String): Builder + + fun query(query: String?): Builder + + fun encodedQuery(encodedQuery: String?): Builder + + /** Encodes the query parameter using UTF-8 and adds it to this URL's query string. */ + fun addQueryParameter(name: String, value: String?): Builder + + /** Adds the pre-encoded query parameter to this URL's query string. */ + fun addEncodedQueryParameter(encodedName: String, encodedValue: String?): Builder + + fun setQueryParameter(name: String, value: String?): Builder + + fun setEncodedQueryParameter(encodedName: String, encodedValue: String?): Builder + + fun removeAllQueryParameters(name: String): Builder + + fun removeAllEncodedQueryParameters(encodedName: String): Builder + + fun fragment(fragment: String?): Builder + + fun encodedFragment(encodedFragment: String?): Builder + + fun build(): HttpUrl + + internal fun parse(base: HttpUrl?, input: String): Builder + + } + + companion object { + + fun defaultPort(scheme: String): Int + + /** + * Returns a new [HttpUrl] representing this. + * + * @throws IllegalArgumentException If this is not a well-formed HTTP or HTTPS URL. + */ + fun String.toHttpUrl(): HttpUrl + + /** + * Returns a new `HttpUrl` representing `url` if it is a well-formed HTTP or HTTPS URL, or null + * if it isn't. + */ + fun String.toHttpUrlOrNull(): HttpUrl? + } +} diff --git a/okhttp/src/commonMain/kotlin/okhttp3/Request.kt b/okhttp/src/commonMain/kotlin/okhttp3/Request.kt index 88522a13d..578a36889 100644 --- a/okhttp/src/commonMain/kotlin/okhttp3/Request.kt +++ b/okhttp/src/commonMain/kotlin/okhttp3/Request.kt @@ -16,7 +16,6 @@ package okhttp3 import kotlin.reflect.KClass -import okhttp3.internal.HttpUrlRepresentation import okhttp3.internal.commonEmptyRequestBody /** @@ -24,7 +23,7 @@ import okhttp3.internal.commonEmptyRequestBody * immutable. */ expect class Request private constructor(builder: Builder) { - val url: HttpUrlRepresentation + val url: HttpUrl val method: String val headers: Headers val body: RequestBody? @@ -53,7 +52,7 @@ expect class Request private constructor(builder: Builder) { val cacheControl: CacheControl open class Builder { - internal var url: HttpUrlRepresentation? + internal var url: HttpUrl? internal var method: String internal var headers: Headers.Builder internal var body: RequestBody? @@ -68,8 +67,7 @@ expect class Request private constructor(builder: Builder) { // /** A mutable map of tags, or an immutable empty map if we don't have any. */ // internal var tags: MutableMap, Any> = mutableMapOf() - // Wait for HttpUrl - // open fun url(url: HttpUrl): Builder + open fun url(url: HttpUrl): Builder /** * Sets the URL target of this request. diff --git a/okhttp/src/commonMain/kotlin/okhttp3/internal/-HostnamesCommon.kt b/okhttp/src/commonMain/kotlin/okhttp3/internal/-HostnamesCommon.kt index ff144bc6a..9ea0a4145 100644 --- a/okhttp/src/commonMain/kotlin/okhttp3/internal/-HostnamesCommon.kt +++ b/okhttp/src/commonMain/kotlin/okhttp3/internal/-HostnamesCommon.kt @@ -224,3 +224,5 @@ internal fun inet6AddressToAscii(address: ByteArray): String { } return result.readUtf8() } + +expect fun String.toCanonicalHost(): String? diff --git a/okhttp/src/commonMain/kotlin/okhttp3/internal/-HttpUrlCommon.kt b/okhttp/src/commonMain/kotlin/okhttp3/internal/-HttpUrlCommon.kt new file mode 100644 index 000000000..eefdad4ae --- /dev/null +++ b/okhttp/src/commonMain/kotlin/okhttp3/internal/-HttpUrlCommon.kt @@ -0,0 +1,904 @@ +/* + * Copyright (C) 2022 Square, Inc. + * + * 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 okhttp3.internal + +import kotlin.jvm.JvmStatic +import okhttp3.HttpUrl +import okhttp3.internal.HttpUrlCommon.canonicalize +import okhttp3.internal.HttpUrlCommon.writePercentDecoded +import okio.Buffer + +internal expect object HttpUrlCommon { + internal fun Buffer.writePercentDecoded( + encoded: String, + pos: Int, + limit: Int, + plusIsSpace: Boolean + ) + + internal fun String.canonicalize( + pos: Int = 0, + limit: Int = length, + encodeSet: String, + alreadyEncoded: Boolean = false, + strict: Boolean = false, + plusIsSpace: Boolean = false, + unicodeAllowed: Boolean = false, + ): String + +} + +internal object CommonHttpUrl { + + internal val HEX_DIGITS = + charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') + private const val USERNAME_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#" + private const val PASSWORD_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#" + private const val PATH_SEGMENT_ENCODE_SET = " \"<>^`{}|/\\?#" + internal const val PATH_SEGMENT_ENCODE_SET_URI = "[]" + private const val QUERY_ENCODE_SET = " \"'<>#" + private const val QUERY_COMPONENT_REENCODE_SET = " \"'<>#&=" + private const val QUERY_COMPONENT_ENCODE_SET = " !\"#$&'(),/:;<=>?@[]\\^`{|}~" + internal const val QUERY_COMPONENT_ENCODE_SET_URI = "\\^`{|}" + internal const val FORM_ENCODE_SET = " !\"#$&'()+,/:;<=>?@[\\]^`{|}~" + private const val FRAGMENT_ENCODE_SET = "" + internal const val FRAGMENT_ENCODE_SET_URI = " \"#<>\\^`{|}" + + val HttpUrl.commonIsHttps: Boolean + get() = scheme == "https" + + val HttpUrl.commonEncodedUsername: String + get() { + if (username.isEmpty()) return "" + val usernameStart = scheme.length + 3 // "://".length() == 3. + val usernameEnd = url.delimiterOffset(":@", usernameStart, url.length) + return url.substring(usernameStart, usernameEnd) + } + + val HttpUrl.commonEncodedPassword: String + get() { + if (password.isEmpty()) return "" + val passwordStart = url.indexOf(':', scheme.length + 3) + 1 + val passwordEnd = url.indexOf('@') + return url.substring(passwordStart, passwordEnd) + } + + val HttpUrl.commonPathSize: Int get() = pathSegments.size + + val HttpUrl.commonEncodedPath: String + get() { + val pathStart = url.indexOf('/', scheme.length + 3) // "://".length() == 3. + val pathEnd = url.delimiterOffset("?#", pathStart, url.length) + return url.substring(pathStart, pathEnd) + } + + val HttpUrl.commonEncodedPathSegments: List + get() { + val pathStart = url.indexOf('/', scheme.length + 3) + val pathEnd = url.delimiterOffset("?#", pathStart, url.length) + val result = mutableListOf() + var i = pathStart + while (i < pathEnd) { + i++ // Skip the '/'. + val segmentEnd = url.delimiterOffset('/', i, pathEnd) + result.add(url.substring(i, segmentEnd)) + i = segmentEnd + } + return result + } + + val HttpUrl.commonEncodedQuery: String? + get() { + if (queryNamesAndValues == null) return null // No query. + val queryStart = url.indexOf('?') + 1 + val queryEnd = url.delimiterOffset('#', queryStart, url.length) + return url.substring(queryStart, queryEnd) + } + + val HttpUrl.commonQuery: String? + get() { + if (queryNamesAndValues == null) return null // No query. + val result = StringBuilder() + queryNamesAndValues.toQueryString(result) + return result.toString() + } + + val HttpUrl.commonQuerySize: Int + get() { + return if (queryNamesAndValues != null) queryNamesAndValues.size / 2 else 0 + } + + fun HttpUrl.commonQueryParameter(name: String): String? { + if (queryNamesAndValues == null) return null + for (i in 0 until queryNamesAndValues.size step 2) { + if (name == queryNamesAndValues[i]) { + return queryNamesAndValues[i + 1] + } + } + return null + } + + val HttpUrl.commonQueryParameterNames: Set + get() { + if (queryNamesAndValues == null) return emptySet() + val result = LinkedHashSet() + for (i in 0 until queryNamesAndValues.size step 2) { + result.add(queryNamesAndValues[i]!!) + } + return result.readOnly() + } + + fun HttpUrl.commonQueryParameterValues(name: String): List { + if (queryNamesAndValues == null) return emptyList() + val result = mutableListOf() + for (i in 0 until queryNamesAndValues.size step 2) { + if (name == queryNamesAndValues[i]) { + result.add(queryNamesAndValues[i + 1]) + } + } + return result.readOnly() + } + + fun HttpUrl.commonQueryParameterName(index: Int): String { + if (queryNamesAndValues == null) throw IndexOutOfBoundsException() + return queryNamesAndValues[index * 2]!! + } + + fun HttpUrl.commonQueryParameterValue(index: Int): String? { + if (queryNamesAndValues == null) throw IndexOutOfBoundsException() + return queryNamesAndValues[index * 2 + 1] + } + + val HttpUrl.commonEncodedFragment: String? + get() { + if (fragment == null) return null + val fragmentStart = url.indexOf('#') + 1 + return url.substring(fragmentStart) + } + + /** Returns a string for this list of query names and values. */ + internal fun List.toQueryString(out: StringBuilder) { + for (i in 0 until size step 2) { + val name = this[i] + val value = this[i + 1] + if (i > 0) out.append('&') + out.append(name) + if (value != null) { + out.append('=') + out.append(value) + } + } + } + + internal fun HttpUrl.commonRedact(): String { + return newBuilder("/...")!! + .username("") + .password("") + .build() + .toString() + } + + + fun HttpUrl.commonResolve(link: String): HttpUrl? = newBuilder(link)?.build() + + fun HttpUrl.commonNewBuilder(): HttpUrl.Builder { + val result = HttpUrl.Builder() + result.scheme = scheme + result.encodedUsername = encodedUsername + result.encodedPassword = encodedPassword + result.host = host + // If we're set to a default port, unset it in case of a scheme change. + result.port = if (port != commonDefaultPort(scheme)) port else -1 + result.encodedPathSegments.clear() + result.encodedPathSegments.addAll(encodedPathSegments) + result.encodedQuery(encodedQuery) + result.encodedFragment = encodedFragment + return result + } + + fun HttpUrl.commonNewBuilder(link: String): HttpUrl.Builder? { + return try { + HttpUrl.Builder().parse(this, link) + } catch (_: IllegalArgumentException) { + null + } + } + + fun HttpUrl.commonEquals(other: Any?): Boolean { + return other is HttpUrl && other.url == url + } + + fun HttpUrl.commonHashCode(): Int = url.hashCode() + + fun HttpUrl.commonToString(): String = url + + + /** Returns 80 if `scheme.equals("http")`, 443 if `scheme.equals("https")` and -1 otherwise. */ + @JvmStatic + fun commonDefaultPort(scheme: String): Int { + return when (scheme) { + "http" -> 80 + "https" -> 443 + else -> -1 + } + } + + + /** + * @param scheme either "http" or "https". + */ + fun HttpUrl.Builder.commonScheme(scheme: String) = apply { + when { + scheme.equals("http", ignoreCase = true) -> this.scheme = "http" + scheme.equals("https", ignoreCase = true) -> this.scheme = "https" + else -> throw IllegalArgumentException("unexpected scheme: $scheme") + } + } + + fun HttpUrl.Builder.commonUsername(username: String) = apply { + this.encodedUsername = username.canonicalize(encodeSet = USERNAME_ENCODE_SET) + } + + fun HttpUrl.Builder.commonEncodedUsername(encodedUsername: String) = apply { + this.encodedUsername = encodedUsername.canonicalize( + encodeSet = USERNAME_ENCODE_SET, + alreadyEncoded = true + ) + } + + fun HttpUrl.Builder.commonPassword(password: String) = apply { + this.encodedPassword = password.canonicalize(encodeSet = PASSWORD_ENCODE_SET) + } + + fun HttpUrl.Builder.commonEncodedPassword(encodedPassword: String) = apply { + this.encodedPassword = encodedPassword.canonicalize( + encodeSet = PASSWORD_ENCODE_SET, + alreadyEncoded = true + ) + } + + /** + * @param host either a regular hostname, International Domain Name, IPv4 address, or IPv6 + * address. + */ + fun HttpUrl.Builder.commonHost(host: String) = apply { + val encoded = host.percentDecode().toCanonicalHost() ?: throw IllegalArgumentException( + "unexpected host: $host") + this.host = encoded + } + + internal fun String.percentDecode( + pos: Int = 0, + limit: Int = length, + plusIsSpace: Boolean = false + ): String { + for (i in pos until limit) { + val c = this[i] + if (c == '%' || c == '+' && plusIsSpace) { + // Slow path: the character at i requires decoding! + val out = Buffer() + out.writeUtf8(this, pos, i) + out.writePercentDecoded(this, pos = i, limit = limit, plusIsSpace = plusIsSpace) + return out.readUtf8() + } + } + + // Fast path: no characters in [pos..limit) required decoding. + return substring(pos, limit) + } + + fun HttpUrl.Builder.commonPort(port: Int) = apply { + require(port in 1..65535) { "unexpected port: $port" } + this.port = port + } + + + fun HttpUrl.Builder.commonAddPathSegment(pathSegment: String) = apply { + push(pathSegment, 0, pathSegment.length, addTrailingSlash = false, alreadyEncoded = false) + } + + /** + * Adds a set of path segments separated by a slash (either `\` or `/`). If `pathSegments` + * starts with a slash, the resulting URL will have empty path segment. + */ + fun HttpUrl.Builder.commonAddPathSegments(pathSegments: String): HttpUrl.Builder = commonAddPathSegments(pathSegments, false) + + fun HttpUrl.Builder.commonAddEncodedPathSegment(encodedPathSegment: String) = apply { + push(encodedPathSegment, 0, encodedPathSegment.length, addTrailingSlash = false, + alreadyEncoded = true) + } + + /** + * Adds a set of encoded path segments separated by a slash (either `\` or `/`). If + * `encodedPathSegments` starts with a slash, the resulting URL will have empty path segment. + */ + fun HttpUrl.Builder.commonAddEncodedPathSegments(encodedPathSegments: String): HttpUrl.Builder = + commonAddPathSegments(encodedPathSegments, true) + + private fun HttpUrl.Builder.commonAddPathSegments(pathSegments: String, alreadyEncoded: Boolean) = apply { + var offset = 0 + do { + val segmentEnd = pathSegments.delimiterOffset("/\\", offset, pathSegments.length) + val addTrailingSlash = segmentEnd < pathSegments.length + push(pathSegments, offset, segmentEnd, addTrailingSlash, alreadyEncoded) + offset = segmentEnd + 1 + } while (offset <= pathSegments.length) + } + + fun HttpUrl.Builder.commonSetPathSegment(index: Int, pathSegment: String) = apply { + val canonicalPathSegment = pathSegment.canonicalize(encodeSet = PATH_SEGMENT_ENCODE_SET) + require(!isDot(canonicalPathSegment) && !isDotDot(canonicalPathSegment)) { + "unexpected path segment: $pathSegment" + } + encodedPathSegments[index] = canonicalPathSegment + } + + fun HttpUrl.Builder.commonSetEncodedPathSegment(index: Int, encodedPathSegment: String) = apply { + val canonicalPathSegment = encodedPathSegment.canonicalize( + encodeSet = PATH_SEGMENT_ENCODE_SET, + alreadyEncoded = true + ) + encodedPathSegments[index] = canonicalPathSegment + require(!isDot(canonicalPathSegment) && !isDotDot(canonicalPathSegment)) { + "unexpected path segment: $encodedPathSegment" + } + } + + fun HttpUrl.Builder.commonRemovePathSegment(index: Int) = apply { + encodedPathSegments.removeAt(index) + if (encodedPathSegments.isEmpty()) { + encodedPathSegments.add("") // Always leave at least one '/'. + } + } + + fun HttpUrl.Builder.commonEncodedPath(encodedPath: String) = apply { + require(encodedPath.startsWith("/")) { "unexpected encodedPath: $encodedPath" } + resolvePath(encodedPath, 0, encodedPath.length) + } + + fun HttpUrl.Builder.commonQuery(query: String?) = apply { + this.encodedQueryNamesAndValues = query?.canonicalize( + encodeSet = QUERY_ENCODE_SET, + plusIsSpace = true + )?.toQueryNamesAndValues() + } + + fun HttpUrl.Builder.commonEncodedQuery(encodedQuery: String?) = apply { + this.encodedQueryNamesAndValues = encodedQuery?.canonicalize( + encodeSet = QUERY_ENCODE_SET, + alreadyEncoded = true, + plusIsSpace = true + )?.toQueryNamesAndValues() + } + + /** Encodes the query parameter using UTF-8 and adds it to this URL's query string. */ + fun HttpUrl.Builder.commonAddQueryParameter(name: String, value: String?) = apply { + if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = mutableListOf() + encodedQueryNamesAndValues!!.add(name.canonicalize( + encodeSet = QUERY_COMPONENT_ENCODE_SET, + plusIsSpace = true + )) + encodedQueryNamesAndValues!!.add(value?.canonicalize( + encodeSet = QUERY_COMPONENT_ENCODE_SET, + plusIsSpace = true + )) + } + + /** Adds the pre-encoded query parameter to this URL's query string. */ + fun HttpUrl.Builder.commonAddEncodedQueryParameter(encodedName: String, encodedValue: String?) = apply { + if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = mutableListOf() + encodedQueryNamesAndValues!!.add(encodedName.canonicalize( + encodeSet = QUERY_COMPONENT_REENCODE_SET, + alreadyEncoded = true, + plusIsSpace = true + )) + encodedQueryNamesAndValues!!.add(encodedValue?.canonicalize( + encodeSet = QUERY_COMPONENT_REENCODE_SET, + alreadyEncoded = true, + plusIsSpace = true + )) + } + + fun HttpUrl.Builder.commonSetQueryParameter(name: String, value: String?) = apply { + removeAllQueryParameters(name) + addQueryParameter(name, value) + } + + fun HttpUrl.Builder.commonSetEncodedQueryParameter(encodedName: String, encodedValue: String?) = apply { + removeAllEncodedQueryParameters(encodedName) + addEncodedQueryParameter(encodedName, encodedValue) + } + + fun HttpUrl.Builder.commonRemoveAllQueryParameters(name: String) = apply { + if (encodedQueryNamesAndValues == null) return this + val nameToRemove = name.canonicalize( + encodeSet = QUERY_COMPONENT_ENCODE_SET, + plusIsSpace = true + ) + commonRemoveAllCanonicalQueryParameters(nameToRemove) + } + + fun HttpUrl.Builder.commonRemoveAllEncodedQueryParameters(encodedName: String) = apply { + if (encodedQueryNamesAndValues == null) return this + commonRemoveAllCanonicalQueryParameters(encodedName.canonicalize( + encodeSet = QUERY_COMPONENT_REENCODE_SET, + alreadyEncoded = true, + plusIsSpace = true + )) + } + + fun HttpUrl.Builder.commonRemoveAllCanonicalQueryParameters(canonicalName: String) { + for (i in encodedQueryNamesAndValues!!.size - 2 downTo 0 step 2) { + if (canonicalName == encodedQueryNamesAndValues!![i]) { + encodedQueryNamesAndValues!!.removeAt(i + 1) + encodedQueryNamesAndValues!!.removeAt(i) + if (encodedQueryNamesAndValues!!.isEmpty()) { + encodedQueryNamesAndValues = null + return + } + } + } + } + + fun HttpUrl.Builder.commonFragment(fragment: String?) = apply { + this.encodedFragment = fragment?.canonicalize( + encodeSet = FRAGMENT_ENCODE_SET, + unicodeAllowed = true + ) + } + + fun HttpUrl.Builder.commonEncodedFragment(encodedFragment: String?) = apply { + this.encodedFragment = encodedFragment?.canonicalize( + encodeSet = FRAGMENT_ENCODE_SET, + alreadyEncoded = true, + unicodeAllowed = true + ) + } + + + /** Adds a path segment. If the input is ".." or equivalent, this pops a path segment. */ + internal fun HttpUrl.Builder.push( + input: String, + pos: Int, + limit: Int, + addTrailingSlash: Boolean, + alreadyEncoded: Boolean + ) { + val segment = input.canonicalize( + pos = pos, + limit = limit, + encodeSet = PATH_SEGMENT_ENCODE_SET, + alreadyEncoded = alreadyEncoded + ) + if (isDot(segment)) { + return // Skip '.' path segments. + } + if (isDotDot(segment)) { + pop() + return + } + if (encodedPathSegments[encodedPathSegments.size - 1].isEmpty()) { + encodedPathSegments[encodedPathSegments.size - 1] = segment + } else { + encodedPathSegments.add(segment) + } + if (addTrailingSlash) { + encodedPathSegments.add("") + } + } + + internal fun HttpUrl.Builder.isDot(input: String): Boolean { + return input == "." || input.equals("%2e", ignoreCase = true) + } + + internal fun HttpUrl.Builder.isDotDot(input: String): Boolean { + return input == ".." || + input.equals("%2e.", ignoreCase = true) || + input.equals(".%2e", ignoreCase = true) || + input.equals("%2e%2e", ignoreCase = true) + } + + /** + * Removes a path segment. When this method returns the last segment is always "", which means + * the encoded path will have a trailing '/'. + * + * Popping "/a/b/c/" yields "/a/b/". In this case the list of path segments goes from ["a", + * "b", "c", ""] to ["a", "b", ""]. + * + * Popping "/a/b/c" also yields "/a/b/". The list of path segments goes from ["a", "b", "c"] + * to ["a", "b", ""]. + */ + internal fun HttpUrl.Builder.pop() { + val removed = encodedPathSegments.removeAt(encodedPathSegments.size - 1) + + // Make sure the path ends with a '/' by either adding an empty string or clearing a segment. + if (removed.isEmpty() && encodedPathSegments.isNotEmpty()) { + encodedPathSegments[encodedPathSegments.size - 1] = "" + } else { + encodedPathSegments.add("") + } + } + + internal fun HttpUrl.Builder.resolvePath(input: String, startPos: Int, limit: Int) { + var pos = startPos + // Read a delimiter. + if (pos == limit) { + // Empty path: keep the base path as-is. + return + } + val c = input[pos] + if (c == '/' || c == '\\') { + // Absolute path: reset to the default "/". + encodedPathSegments.clear() + encodedPathSegments.add("") + pos++ + } else { + // Relative path: clear everything after the last '/'. + encodedPathSegments[encodedPathSegments.size - 1] = "" + } + + // Read path segments. + var i = pos + while (i < limit) { + val pathSegmentDelimiterOffset = input.delimiterOffset("/\\", i, limit) + val segmentHasTrailingSlash = pathSegmentDelimiterOffset < limit + push(input, i, pathSegmentDelimiterOffset, segmentHasTrailingSlash, true) + i = pathSegmentDelimiterOffset + if (segmentHasTrailingSlash) i++ + } + } + + /** + * Cuts this string up into alternating parameter names and values. This divides a query string + * like `subject=math&easy&problem=5-2=3` into the list `["subject", "math", "easy", null, + * "problem", "5-2=3"]`. Note that values may be null and may contain '=' characters. + */ + internal fun String.toQueryNamesAndValues(): MutableList { + val result = mutableListOf() + var pos = 0 + while (pos <= length) { + var ampersandOffset = indexOf('&', pos) + if (ampersandOffset == -1) ampersandOffset = length + + val equalsOffset = indexOf('=', pos) + if (equalsOffset == -1 || equalsOffset > ampersandOffset) { + result.add(substring(pos, ampersandOffset)) + result.add(null) // No value for this name. + } else { + result.add(substring(pos, equalsOffset)) + result.add(substring(equalsOffset + 1, ampersandOffset)) + } + pos = ampersandOffset + 1 + } + return result + } + + fun HttpUrl.Builder.commonBuild(): HttpUrl { + @Suppress("UNCHECKED_CAST") // percentDecode returns either List or List. + return HttpUrl( + scheme = scheme ?: throw IllegalStateException("scheme == null"), + username = encodedUsername.percentDecode(), + password = encodedPassword.percentDecode(), + host = host ?: throw IllegalStateException("host == null"), + port = effectivePort(), + pathSegments = encodedPathSegments.map { it.percentDecode() }, + queryNamesAndValues = encodedQueryNamesAndValues?.map { it?.percentDecode(plusIsSpace = true) }, + fragment = encodedFragment?.percentDecode(), + url = toString() + ) + } + + internal fun HttpUrl.Builder.effectivePort(): Int { + return if (port != -1) port else HttpUrl.defaultPort(scheme!!) + } + internal fun HttpUrl.Builder.commonToString(): String { + return buildString { + if (scheme != null) { + append(scheme) + append("://") + } else { + append("//") + } + + if (encodedUsername.isNotEmpty() || encodedPassword.isNotEmpty()) { + append(encodedUsername) + if (encodedPassword.isNotEmpty()) { + append(':') + append(encodedPassword) + } + append('@') + } + + if (host != null) { + if (':' in host!!) { + // Host is an IPv6 address. + append('[') + append(host) + append(']') + } else { + append(host) + } + } + + if (port != -1 || scheme != null) { + val effectivePort = effectivePort() + if (scheme == null || effectivePort != HttpUrl.defaultPort(scheme!!)) { + append(':') + append(effectivePort) + } + } + + encodedPathSegments.toPathString(this) + + if (encodedQueryNamesAndValues != null) { + append('?') + encodedQueryNamesAndValues!!.toQueryString(this) + } + + if (encodedFragment != null) { + append('#') + append(encodedFragment) + } + } + } + + /** Returns a path string for this list of path segments. */ + internal fun List.toPathString(out: StringBuilder) { + for (i in 0 until size) { + out.append('/') + out.append(this[i]) + } + } + + internal fun HttpUrl.Builder.commonParse(base: HttpUrl?, input: String): HttpUrl.Builder { + var pos = input.indexOfFirstNonAsciiWhitespace() + val limit = input.indexOfLastNonAsciiWhitespace(pos) + + // Scheme. + val schemeDelimiterOffset = schemeDelimiterOffset(input, pos, limit) + if (schemeDelimiterOffset != -1) { + when { + input.startsWith("https:", ignoreCase = true, startIndex = pos) -> { + this.scheme = "https" + pos += "https:".length + } + input.startsWith("http:", ignoreCase = true, startIndex = pos) -> { + this.scheme = "http" + pos += "http:".length + } + else -> throw IllegalArgumentException("Expected URL scheme 'http' or 'https' but was '" + + input.substring(0, schemeDelimiterOffset) + "'") + } + } else if (base != null) { + this.scheme = base.scheme + } else { + val truncated = if (input.length > 6) input.take(6) + "..." else input + throw IllegalArgumentException( + "Expected URL scheme 'http' or 'https' but no scheme was found for $truncated") + } + + // Authority. + var hasUsername = false + var hasPassword = false + val slashCount = input.slashCount(pos, limit) + if (slashCount >= 2 || base == null || base.scheme != this.scheme) { + // Read an authority if either: + // * The input starts with 2 or more slashes. These follow the scheme if it exists. + // * The input scheme exists and is different from the base URL's scheme. + // + // The structure of an authority is: + // username:password@host:port + // + // Username, password and port are optional. + // [username[:password]@]host[:port] + pos += slashCount + authority@ while (true) { + val componentDelimiterOffset = input.delimiterOffset("@/\\?#", pos, limit) + val c = if (componentDelimiterOffset != limit) { + input[componentDelimiterOffset].code + } else { + -1 + } + when (c) { + '@'.code -> { + // User info precedes. + if (!hasPassword) { + val passwordColonOffset = input.delimiterOffset(':', pos, componentDelimiterOffset) + val canonicalUsername = input.canonicalize( + pos = pos, + limit = passwordColonOffset, + encodeSet = USERNAME_ENCODE_SET, + alreadyEncoded = true + ) + this.encodedUsername = if (hasUsername) { + this.encodedUsername + "%40" + canonicalUsername + } else { + canonicalUsername + } + if (passwordColonOffset != componentDelimiterOffset) { + hasPassword = true + this.encodedPassword = input.canonicalize( + pos = passwordColonOffset + 1, + limit = componentDelimiterOffset, + encodeSet = PASSWORD_ENCODE_SET, + alreadyEncoded = true + ) + } + hasUsername = true + } else { + this.encodedPassword = this.encodedPassword + "%40" + input.canonicalize( + pos = pos, + limit = componentDelimiterOffset, + encodeSet = PASSWORD_ENCODE_SET, + alreadyEncoded = true + ) + } + pos = componentDelimiterOffset + 1 + } + + -1, '/'.code, '\\'.code, '?'.code, '#'.code -> { + // Host info precedes. + val portColonOffset = portColonOffset(input, pos, componentDelimiterOffset) + if (portColonOffset + 1 < componentDelimiterOffset) { + host = input.percentDecode(pos = pos, limit = portColonOffset).toCanonicalHost() + port = parsePort(input, portColonOffset + 1, componentDelimiterOffset) + require(port != -1) { + "Invalid URL port: \"${input.substring(portColonOffset + 1, + componentDelimiterOffset)}\"" + } + } else { + host = input.percentDecode(pos = pos, limit = portColonOffset).toCanonicalHost() + port = HttpUrl.defaultPort(scheme!!) + } + require(host != null) { + "$INVALID_HOST: \"${input.substring(pos, portColonOffset)}\"" + } + pos = componentDelimiterOffset + break@authority + } + } + } + } else { + // This is a relative link. Copy over all authority components. Also maybe the path & query. + this.encodedUsername = base.encodedUsername + this.encodedPassword = base.encodedPassword + this.host = base.host + this.port = base.port + this.encodedPathSegments.clear() + this.encodedPathSegments.addAll(base.encodedPathSegments) + if (pos == limit || input[pos] == '#') { + encodedQuery(base.encodedQuery) + } + } + + // Resolve the relative path. + val pathDelimiterOffset = input.delimiterOffset("?#", pos, limit) + resolvePath(input, pos, pathDelimiterOffset) + pos = pathDelimiterOffset + + // Query. + if (pos < limit && input[pos] == '?') { + val queryDelimiterOffset = input.delimiterOffset('#', pos, limit) + this.encodedQueryNamesAndValues = input.canonicalize( + pos = pos + 1, + limit = queryDelimiterOffset, + encodeSet = QUERY_ENCODE_SET, + alreadyEncoded = true, + plusIsSpace = true + ).toQueryNamesAndValues() + pos = queryDelimiterOffset + } + + // Fragment. + if (pos < limit && input[pos] == '#') { + this.encodedFragment = input.canonicalize( + pos = pos + 1, + limit = limit, + encodeSet = FRAGMENT_ENCODE_SET, + alreadyEncoded = true, + unicodeAllowed = true + ) + } + + return this + } + internal const val INVALID_HOST = "Invalid URL host" + + /** + * Returns the index of the ':' in `input` that is after scheme characters. Returns -1 if + * `input` does not have a scheme that starts at `pos`. + */ + internal fun schemeDelimiterOffset(input: String, pos: Int, limit: Int): Int { + if (limit - pos < 2) return -1 + + val c0 = input[pos] + if ((c0 < 'a' || c0 > 'z') && (c0 < 'A' || c0 > 'Z')) return -1 // Not a scheme start char. + + characters@ for (i in pos + 1 until limit) { + return when (input[i]) { + // Scheme character. Keep going. + in 'a'..'z', in 'A'..'Z', in '0'..'9', '+', '-', '.' -> continue@characters + + // Scheme prefix! + ':' -> i + + // Non-scheme character before the first ':'. + else -> -1 + } + } + + return -1 // No ':'; doesn't start with a scheme. + } + + /** Returns the number of '/' and '\' slashes in this, starting at `pos`. */ + internal fun String.slashCount(pos: Int, limit: Int): Int { + var slashCount = 0 + for (i in pos until limit) { + val c = this[i] + if (c == '\\' || c == '/') { + slashCount++ + } else { + break + } + } + return slashCount + } + + /** Finds the first ':' in `input`, skipping characters between square braces "[...]". */ + internal fun portColonOffset(input: String, pos: Int, limit: Int): Int { + var i = pos + while (i < limit) { + when (input[i]) { + '[' -> { + while (++i < limit) { + if (input[i] == ']') break + } + } + ':' -> return i + } + i++ + } + return limit // No colon. + } + + internal fun parsePort(input: String, pos: Int, limit: Int): Int { + return try { + // Canonicalize the port string to skip '\n' etc. + val portString = input.canonicalize(pos = pos, limit = limit, encodeSet = "") + val i = portString.toInt() + if (i in 1..65535) i else -1 + } catch (_: NumberFormatException) { + -1 // Invalid port. + } + } + + internal fun String.isPercentEncoded(pos: Int, limit: Int): Boolean { + return pos + 2 < limit && + this[pos] == '%' && + this[pos + 1].parseHexDigit() != -1 && + this[pos + 2].parseHexDigit() != -1 + } + + internal fun String.commonToHttpUrl(): HttpUrl = HttpUrl.Builder().parse(null, this).build() + + internal fun String.commonToHttpUrlOrNull(): HttpUrl? { + return try { + commonToHttpUrl() + } catch (_: IllegalArgumentException) { + null + } + } +} diff --git a/okhttp/src/commonMain/kotlin/okhttp3/internal/-UtilCommon.kt b/okhttp/src/commonMain/kotlin/okhttp3/internal/-UtilCommon.kt index 0a3bf3588..d7c68db73 100644 --- a/okhttp/src/commonMain/kotlin/okhttp3/internal/-UtilCommon.kt +++ b/okhttp/src/commonMain/kotlin/okhttp3/internal/-UtilCommon.kt @@ -34,9 +34,6 @@ import okio.Options import okio.Path import okio.use -// Temporary until we have a HttpUrl in common -expect class HttpUrlRepresentation - // TODO: migrate callers to [Regex.matchAt] when that API is not experimental. internal fun Regex.matchAtPolyfill(input: CharSequence, index: Int): MatchResult? { val candidate = find(input, index) ?: return null @@ -394,3 +391,9 @@ internal fun interleave(a: Iterable, b: Iterable): List { } } } + +// TODO check read only options for creating lists +public fun List.readOnly() = this.toList() + +// TODO check read only options for creating lists +public fun Set.readOnly() = this.toSet() diff --git a/okhttp/src/commonTest/kotlin/okhttp3/HttpUrlCommonTest.kt b/okhttp/src/commonTest/kotlin/okhttp3/HttpUrlCommonTest.kt new file mode 100644 index 000000000..195e7d541 --- /dev/null +++ b/okhttp/src/commonTest/kotlin/okhttp3/HttpUrlCommonTest.kt @@ -0,0 +1,1334 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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 okhttp3 + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.hasMessage +import assertk.assertions.isEqualTo +import assertk.assertions.isNull +import assertk.fail +import kotlin.test.Test +import kotlin.test.assertFailsWith +import okhttp3.HttpUrl.Companion.toHttpUrl + +@Suppress("HttpUrlsUsage") // Don't warn if we should be using https://. +open class HttpUrlCommonTest { + protected open fun parse(url: String): HttpUrl { + return url.toHttpUrl() + } + + protected open fun assertInvalid(string: String, exceptionMessage: String?) { + try { + val result = string.toHttpUrl() + if (exceptionMessage != null) { + fail("Expected failure with $exceptionMessage but got $result") + } else { + fail("Expected failure but got $result") + } + } catch(iae: IllegalArgumentException) { + iae.printStackTrace() + if (exceptionMessage != null) { + assertThat(iae).hasMessage(exceptionMessage) + } + } + } + + @Test + fun parseTrimsAsciiWhitespace() { + val expected = parse("http://host/") + // Leading. + assertThat(parse("http://host/\u000c\n\t \r")).isEqualTo(expected) + // Trailing. + assertThat(parse("\r\n\u000c \thttp://host/")).isEqualTo(expected) + // Both. + assertThat(parse(" http://host/ ")).isEqualTo(expected) + // Both. + assertThat(parse(" http://host/ ")).isEqualTo(expected) + assertThat(parse("http://host/").resolve(" ")).isEqualTo(expected) + assertThat(parse("http://host/").resolve(" . ")).isEqualTo(expected) + } + + @Test + fun parseHostAsciiNonPrintable() { + val host = "host\u0001" + assertInvalid("http://$host/", "Invalid URL host: \"host\u0001\"") + // TODO make exception message escape non-printable characters + } + + @Test + fun parseDoesNotTrimOtherWhitespaceCharacters() { + // Whitespace characters list from Google's Guava team: http://goo.gl/IcR9RD + // line tabulation + assertThat(parse("http://h/\u000b").encodedPath).isEqualTo("/%0B") + // information separator 4 + assertThat(parse("http://h/\u001c").encodedPath).isEqualTo("/%1C") + // information separator 3 + assertThat(parse("http://h/\u001d").encodedPath).isEqualTo("/%1D") + // information separator 2 + assertThat(parse("http://h/\u001e").encodedPath).isEqualTo("/%1E") + // information separator 1 + assertThat(parse("http://h/\u001f").encodedPath).isEqualTo("/%1F") + // next line + assertThat(parse("http://h/\u0085").encodedPath).isEqualTo("/%C2%85") + // non-breaking space + assertThat(parse("http://h/\u00a0").encodedPath).isEqualTo("/%C2%A0") + // ogham space mark + assertThat(parse("http://h/\u1680").encodedPath).isEqualTo("/%E1%9A%80") + // mongolian vowel separator + assertThat(parse("http://h/\u180e").encodedPath).isEqualTo("/%E1%A0%8E") + // en quad + assertThat(parse("http://h/\u2000").encodedPath).isEqualTo("/%E2%80%80") + // em quad + assertThat(parse("http://h/\u2001").encodedPath).isEqualTo("/%E2%80%81") + // en space + assertThat(parse("http://h/\u2002").encodedPath).isEqualTo("/%E2%80%82") + // em space + assertThat(parse("http://h/\u2003").encodedPath).isEqualTo("/%E2%80%83") + // three-per-em space + assertThat(parse("http://h/\u2004").encodedPath).isEqualTo("/%E2%80%84") + // four-per-em space + assertThat(parse("http://h/\u2005").encodedPath).isEqualTo("/%E2%80%85") + // six-per-em space + assertThat(parse("http://h/\u2006").encodedPath).isEqualTo("/%E2%80%86") + // figure space + assertThat(parse("http://h/\u2007").encodedPath).isEqualTo("/%E2%80%87") + // punctuation space + assertThat(parse("http://h/\u2008").encodedPath).isEqualTo("/%E2%80%88") + // thin space + assertThat(parse("http://h/\u2009").encodedPath).isEqualTo("/%E2%80%89") + // hair space + assertThat(parse("http://h/\u200a").encodedPath).isEqualTo("/%E2%80%8A") + // zero-width space + assertThat(parse("http://h/\u200b").encodedPath).isEqualTo("/%E2%80%8B") + // zero-width non-joiner + assertThat(parse("http://h/\u200c").encodedPath).isEqualTo("/%E2%80%8C") + // zero-width joiner + assertThat(parse("http://h/\u200d").encodedPath).isEqualTo("/%E2%80%8D") + // left-to-right mark + assertThat(parse("http://h/\u200e").encodedPath).isEqualTo("/%E2%80%8E") + // right-to-left mark + assertThat(parse("http://h/\u200f").encodedPath).isEqualTo("/%E2%80%8F") + // line separator + assertThat(parse("http://h/\u2028").encodedPath).isEqualTo("/%E2%80%A8") + // paragraph separator + assertThat(parse("http://h/\u2029").encodedPath).isEqualTo("/%E2%80%A9") + // narrow non-breaking space + assertThat(parse("http://h/\u202f").encodedPath).isEqualTo("/%E2%80%AF") + // medium mathematical space + assertThat(parse("http://h/\u205f").encodedPath).isEqualTo("/%E2%81%9F") + // ideographic space + assertThat(parse("http://h/\u3000").encodedPath).isEqualTo("/%E3%80%80") + } + + @Test + fun newBuilderResolve() { + // Non-exhaustive tests because implementation is the same as resolve. + val base = parse("http://host/a/b") + assertThat(base.newBuilder("https://host2")!!.build()) + .isEqualTo(parse("https://host2/")) + assertThat(base.newBuilder("//host2")!!.build()) + .isEqualTo(parse("http://host2/")) + assertThat(base.newBuilder("/path")!!.build()) + .isEqualTo(parse("http://host/path")) + assertThat(base.newBuilder("path")!!.build()) + .isEqualTo(parse("http://host/a/path")) + assertThat(base.newBuilder("?query")!!.build()) + .isEqualTo(parse("http://host/a/b?query")) + assertThat(base.newBuilder("#fragment")!!.build()) + .isEqualTo(parse("http://host/a/b#fragment")) + assertThat(base.newBuilder("")!!.build()).isEqualTo(parse("http://host/a/b")) + assertThat(base.newBuilder("ftp://b")).isNull() + assertThat(base.newBuilder("ht+tp://b")).isNull() + assertThat(base.newBuilder("ht-tp://b")).isNull() + assertThat(base.newBuilder("ht.tp://b")).isNull() + } + + @Test + fun redactedUrl() { + val baseWithPasswordAndUsername = parse("http://username:password@host/a/b#fragment") + val baseWithUsernameOnly = parse("http://username@host/a/b#fragment") + val baseWithPasswordOnly = parse("http://password@host/a/b#fragment") + assertThat(baseWithPasswordAndUsername.redact()).isEqualTo("http://host/...") + assertThat(baseWithUsernameOnly.redact()).isEqualTo("http://host/...") + assertThat(baseWithPasswordOnly.redact()).isEqualTo("http://host/...") + } + + @Test + fun resolveNoScheme() { + val base = parse("http://host/a/b") + assertThat(base.resolve("//host2")).isEqualTo(parse("http://host2/")) + assertThat(base.resolve("/path")).isEqualTo(parse("http://host/path")) + assertThat(base.resolve("path")).isEqualTo(parse("http://host/a/path")) + assertThat(base.resolve("?query")).isEqualTo(parse("http://host/a/b?query")) + assertThat(base.resolve("#fragment")) + .isEqualTo(parse("http://host/a/b#fragment")) + assertThat(base.resolve("")).isEqualTo(parse("http://host/a/b")) + assertThat(base.resolve("\\path")).isEqualTo(parse("http://host/path")) + } + + @Test + fun resolveUnsupportedScheme() { + val base = parse("http://a/") + assertThat(base.resolve("ftp://b")).isNull() + assertThat(base.resolve("ht+tp://b")).isNull() + assertThat(base.resolve("ht-tp://b")).isNull() + assertThat(base.resolve("ht.tp://b")).isNull() + } + + @Test + fun resolveSchemeLikePath() { + val base = parse("http://a/") + assertThat(base.resolve("http//b/")).isEqualTo(parse("http://a/http//b/")) + assertThat(base.resolve("ht+tp//b/")).isEqualTo(parse("http://a/ht+tp//b/")) + assertThat(base.resolve("ht-tp//b/")).isEqualTo(parse("http://a/ht-tp//b/")) + assertThat(base.resolve("ht.tp//b/")).isEqualTo(parse("http://a/ht.tp//b/")) + } + + /** + * https://tools.ietf.org/html/rfc3986#section-5.4.1 + */ + @Test + fun rfc3886NormalExamples() { + val url = parse("http://a/b/c/d;p?q") + // No 'g:' scheme in HttpUrl. + assertThat(url.resolve("g:h")).isNull() + assertThat(url.resolve("g")).isEqualTo(parse("http://a/b/c/g")) + assertThat(url.resolve("./g")).isEqualTo(parse("http://a/b/c/g")) + assertThat(url.resolve("g/")).isEqualTo(parse("http://a/b/c/g/")) + assertThat(url.resolve("/g")).isEqualTo(parse("http://a/g")) + assertThat(url.resolve("//g")).isEqualTo(parse("http://g")) + assertThat(url.resolve("?y")).isEqualTo(parse("http://a/b/c/d;p?y")) + assertThat(url.resolve("g?y")).isEqualTo(parse("http://a/b/c/g?y")) + assertThat(url.resolve("#s")).isEqualTo(parse("http://a/b/c/d;p?q#s")) + assertThat(url.resolve("g#s")).isEqualTo(parse("http://a/b/c/g#s")) + assertThat(url.resolve("g?y#s")).isEqualTo(parse("http://a/b/c/g?y#s")) + assertThat(url.resolve(";x")).isEqualTo(parse("http://a/b/c/;x")) + assertThat(url.resolve("g;x")).isEqualTo(parse("http://a/b/c/g;x")) + assertThat(url.resolve("g;x?y#s")).isEqualTo(parse("http://a/b/c/g;x?y#s")) + assertThat(url.resolve("")).isEqualTo(parse("http://a/b/c/d;p?q")) + assertThat(url.resolve(".")).isEqualTo(parse("http://a/b/c/")) + assertThat(url.resolve("./")).isEqualTo(parse("http://a/b/c/")) + assertThat(url.resolve("..")).isEqualTo(parse("http://a/b/")) + assertThat(url.resolve("../")).isEqualTo(parse("http://a/b/")) + assertThat(url.resolve("../g")).isEqualTo(parse("http://a/b/g")) + assertThat(url.resolve("../..")).isEqualTo(parse("http://a/")) + assertThat(url.resolve("../../")).isEqualTo(parse("http://a/")) + assertThat(url.resolve("../../g")).isEqualTo(parse("http://a/g")) + } + + /** + * https://tools.ietf.org/html/rfc3986#section-5.4.2 + */ + @Test + fun rfc3886AbnormalExamples() { + val url = parse("http://a/b/c/d;p?q") + assertThat(url.resolve("../../../g")).isEqualTo(parse("http://a/g")) + assertThat(url.resolve("../../../../g")).isEqualTo(parse("http://a/g")) + assertThat(url.resolve("/./g")).isEqualTo(parse("http://a/g")) + assertThat(url.resolve("/../g")).isEqualTo(parse("http://a/g")) + assertThat(url.resolve("g.")).isEqualTo(parse("http://a/b/c/g.")) + assertThat(url.resolve(".g")).isEqualTo(parse("http://a/b/c/.g")) + assertThat(url.resolve("g..")).isEqualTo(parse("http://a/b/c/g..")) + assertThat(url.resolve("..g")).isEqualTo(parse("http://a/b/c/..g")) + assertThat(url.resolve("./../g")).isEqualTo(parse("http://a/b/g")) + assertThat(url.resolve("./g/.")).isEqualTo(parse("http://a/b/c/g/")) + assertThat(url.resolve("g/./h")).isEqualTo(parse("http://a/b/c/g/h")) + assertThat(url.resolve("g/../h")).isEqualTo(parse("http://a/b/c/h")) + assertThat(url.resolve("g;x=1/./y")).isEqualTo(parse("http://a/b/c/g;x=1/y")) + assertThat(url.resolve("g;x=1/../y")).isEqualTo(parse("http://a/b/c/y")) + assertThat(url.resolve("g?y/./x")).isEqualTo(parse("http://a/b/c/g?y/./x")) + assertThat(url.resolve("g?y/../x")).isEqualTo(parse("http://a/b/c/g?y/../x")) + assertThat(url.resolve("g#s/./x")).isEqualTo(parse("http://a/b/c/g#s/./x")) + assertThat(url.resolve("g#s/../x")).isEqualTo(parse("http://a/b/c/g#s/../x")) + // "http:g" also okay. + assertThat(url.resolve("http:g")).isEqualTo(parse("http://a/b/c/g")) + } + + @Test + fun parseAuthoritySlashCountDoesntMatter() { + assertThat(parse("http:host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http:/host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http:\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http://host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http:\\/host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http:/\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http:\\\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http:///host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http:\\//host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http:/\\/host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http://\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http:\\\\/host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http:/\\\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http:\\\\\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http:////host/path")) + .isEqualTo(parse("http://host/path")) + } + + @Test + fun resolveAuthoritySlashCountDoesntMatterWithDifferentScheme() { + val base = parse("https://a/b/c") + assertThat(base.resolve("http:host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:/host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http://host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:\\/host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:/\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:\\\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:///host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:\\//host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:/\\/host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http://\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:\\\\/host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:/\\\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:\\\\\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:////host/path")) + .isEqualTo(parse("http://host/path")) + } + + @Test + fun resolveAuthoritySlashCountMattersWithSameScheme() { + val base = parse("http://a/b/c") + assertThat(base.resolve("http:host/path")) + .isEqualTo(parse("http://a/b/host/path")) + assertThat(base.resolve("http:/host/path")) + .isEqualTo(parse("http://a/host/path")) + assertThat(base.resolve("http:\\host/path")) + .isEqualTo(parse("http://a/host/path")) + assertThat(base.resolve("http://host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:\\/host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:/\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:\\\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:///host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:\\//host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:/\\/host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http://\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:\\\\/host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:/\\\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:\\\\\\host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(base.resolve("http:////host/path")) + .isEqualTo(parse("http://host/path")) + } + + @Test + fun username() { + assertThat(parse("http://@host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http://user@host/path")) + .isEqualTo(parse("http://user@host/path")) + } + + /** + * Given multiple '@' characters, the last one is the delimiter. + */ + @Test + fun authorityWithMultipleAtSigns() { + val httpUrl = parse("http://foo@bar@baz/path") + assertThat(httpUrl.username).isEqualTo("foo@bar") + assertThat(httpUrl.password).isEqualTo("") + assertThat(httpUrl).isEqualTo(parse("http://foo%40bar@baz/path")) + } + + /** + * Given multiple ':' characters, the first one is the delimiter. + */ + @Test + fun authorityWithMultipleColons() { + val httpUrl = parse("http://foo:pass1@bar:pass2@baz/path") + assertThat(httpUrl.username).isEqualTo("foo") + assertThat(httpUrl.password).isEqualTo("pass1@bar:pass2") + assertThat(httpUrl).isEqualTo(parse("http://foo:pass1%40bar%3Apass2@baz/path")) + } + + @Test + fun usernameAndPassword() { + assertThat(parse("http://username:password@host/path")) + .isEqualTo(parse("http://username:password@host/path")) + assertThat(parse("http://username:@host/path")) + .isEqualTo(parse("http://username@host/path")) + } + + @Test + fun passwordWithEmptyUsername() { + // Chrome doesn't mind, but Firefox rejects URLs with empty usernames and non-empty passwords. + assertThat(parse("http://:@host/path")) + .isEqualTo(parse("http://host/path")) + assertThat(parse("http://:password@@host/path").encodedPassword) + .isEqualTo("password%40") + } + + @Test + fun unprintableCharactersArePercentEncoded() { + assertThat(parse("http://host/\u0000").encodedPath).isEqualTo("/%00") + assertThat(parse("http://host/\u0008").encodedPath).isEqualTo("/%08") + assertThat(parse("http://host/\ufffd").encodedPath).isEqualTo("/%EF%BF%BD") + } + + @Test + fun hostContainsIllegalCharacter() { + assertInvalid("http://\n/", "Invalid URL host: \"\n\"") + assertInvalid("http:// /", "Invalid URL host: \" \"") + assertInvalid("http://%20/", "Invalid URL host: \"%20\"") + } + + @Test + fun hostIpv6() { + // Square braces are absent from host()... + assertThat(parse("http://[::1]/").host).isEqualTo("::1") + + // ... but they're included in toString(). + assertThat(parse("http://[::1]/").toString()).isEqualTo("http://[::1]/") + + // IPv6 colons don't interfere with port numbers or passwords. + assertThat(parse("http://[::1]:8080/").port).isEqualTo(8080) + assertThat(parse("http://user:password@[::1]/").password).isEqualTo("password") + assertThat(parse("http://user:password@[::1]:8080/").host).isEqualTo("::1") + + // Permit the contents of IPv6 addresses to be percent-encoded... + assertThat(parse("http://[%3A%3A%31]/").host).isEqualTo("::1") + + // Including the Square braces themselves! (This is what Chrome does.) + assertThat(parse("http://%5B%3A%3A1%5D/").host).isEqualTo("::1") + } + + @Test + fun hostIpv6AddressDifferentFormats() { + // Multiple representations of the same address; see http://tools.ietf.org/html/rfc5952. + val a3 = "2001:db8::1:0:0:1" + assertThat(parse("http://[2001:db8:0:0:1:0:0:1]").host).isEqualTo(a3) + assertThat(parse("http://[2001:0db8:0:0:1:0:0:1]").host).isEqualTo(a3) + assertThat(parse("http://[2001:db8::1:0:0:1]").host).isEqualTo(a3) + assertThat(parse("http://[2001:db8::0:1:0:0:1]").host).isEqualTo(a3) + assertThat(parse("http://[2001:0db8::1:0:0:1]").host).isEqualTo(a3) + assertThat(parse("http://[2001:db8:0:0:1::1]").host).isEqualTo(a3) + assertThat(parse("http://[2001:db8:0000:0:1::1]").host).isEqualTo(a3) + assertThat(parse("http://[2001:DB8:0:0:1::1]").host).isEqualTo(a3) + } + + @Test + fun hostIpv6AddressLeadingCompression() { + assertThat(parse("http://[::0001]").host).isEqualTo("::1") + assertThat(parse("http://[0000::0001]").host).isEqualTo("::1") + assertThat(parse("http://[0000:0000:0000:0000:0000:0000:0000:0001]").host) + .isEqualTo("::1") + assertThat(parse("http://[0000:0000:0000:0000:0000:0000::0001]").host) + .isEqualTo("::1") + } + + @Test + fun hostIpv6AddressTrailingCompression() { + assertThat(parse("http://[0001:0000::]").host).isEqualTo("1::") + assertThat(parse("http://[0001::0000]").host).isEqualTo("1::") + assertThat(parse("http://[0001::]").host).isEqualTo("1::") + assertThat(parse("http://[1::]").host).isEqualTo("1::") + } + + @Test + fun hostIpv6AddressTooManyDigitsInGroup() { + assertInvalid( + "http://[00000:0000:0000:0000:0000:0000:0000:0001]", + "Invalid URL host: \"[00000:0000:0000:0000:0000:0000:0000:0001]\"" + ) + assertInvalid("http://[::00001]", "Invalid URL host: \"[::00001]\"") + } + + @Test + fun hostIpv6AddressMisplacedColons() { + assertInvalid( + "http://[:0000:0000:0000:0000:0000:0000:0000:0001]", + "Invalid URL host: \"[:0000:0000:0000:0000:0000:0000:0000:0001]\"" + ) + assertInvalid( + "http://[:::0000:0000:0000:0000:0000:0000:0000:0001]", + "Invalid URL host: \"[:::0000:0000:0000:0000:0000:0000:0000:0001]\"" + ) + assertInvalid("http://[:1]", "Invalid URL host: \"[:1]\"") + assertInvalid("http://[:::1]", "Invalid URL host: \"[:::1]\"") + assertInvalid( + "http://[0000:0000:0000:0000:0000:0000:0001:]", + "Invalid URL host: \"[0000:0000:0000:0000:0000:0000:0001:]\"" + ) + assertInvalid( + "http://[0000:0000:0000:0000:0000:0000:0000:0001:]", + "Invalid URL host: \"[0000:0000:0000:0000:0000:0000:0000:0001:]\"" + ) + assertInvalid( + "http://[0000:0000:0000:0000:0000:0000:0000:0001::]", + "Invalid URL host: \"[0000:0000:0000:0000:0000:0000:0000:0001::]\"" + ) + assertInvalid( + "http://[0000:0000:0000:0000:0000:0000:0000:0001:::]", + "Invalid URL host: \"[0000:0000:0000:0000:0000:0000:0000:0001:::]\"" + ) + assertInvalid("http://[1:]", "Invalid URL host: \"[1:]\"") + assertInvalid("http://[1:::]", "Invalid URL host: \"[1:::]\"") + assertInvalid("http://[1:::1]", "Invalid URL host: \"[1:::1]\"") + assertInvalid( + "http://[0000:0000:0000:0000::0000:0000:0000:0001]", + "Invalid URL host: \"[0000:0000:0000:0000::0000:0000:0000:0001]\"" + ) + } + + @Test + fun hostIpv6AddressTooManyGroups() { + assertInvalid( + "http://[0000:0000:0000:0000:0000:0000:0000:0000:0001]", + "Invalid URL host: \"[0000:0000:0000:0000:0000:0000:0000:0000:0001]\"" + ) + } + + @Test + fun hostIpv6AddressTooMuchCompression() { + assertInvalid( + "http://[0000::0000:0000:0000:0000::0001]", + "Invalid URL host: \"[0000::0000:0000:0000:0000::0001]\"" + ) + assertInvalid( + "http://[::0000:0000:0000:0000::0001]", + "Invalid URL host: \"[::0000:0000:0000:0000::0001]\"" + ) + } + + @Test + fun hostIpv6ScopedAddress() { + // java.net.InetAddress parses scoped addresses. These aren't valid in URLs. + assertInvalid("http://[::1%2544]", "Invalid URL host: \"[::1%2544]\"") + } + + @Test + fun hostIpv6AddressTooManyLeadingZeros() { + // Guava's been buggy on this case. https://github.com/google/guava/issues/3116 + assertInvalid( + "http://[2001:db8:0:0:1:0:0:00001]", + "Invalid URL host: \"[2001:db8:0:0:1:0:0:00001]\"" + ) + } + + @Test + fun hostIpv6WithIpv4Suffix() { + assertThat(parse("http://[::1:255.255.255.255]/").host) + .isEqualTo("::1:ffff:ffff") + assertThat(parse("http://[0:0:0:0:0:1:0.0.0.0]/").host).isEqualTo("::1:0:0") + } + + @Test + fun hostIpv6WithIpv4SuffixWithOctalPrefix() { + // Chrome interprets a leading '0' as octal; Firefox rejects them. (We reject them.) + assertInvalid( + "http://[0:0:0:0:0:1:0.0.0.000000]/", + "Invalid URL host: \"[0:0:0:0:0:1:0.0.0.000000]\"" + ) + assertInvalid( + "http://[0:0:0:0:0:1:0.010.0.010]/", + "Invalid URL host: \"[0:0:0:0:0:1:0.010.0.010]\"" + ) + assertInvalid( + "http://[0:0:0:0:0:1:0.0.0.000001]/", + "Invalid URL host: \"[0:0:0:0:0:1:0.0.0.000001]\"" + ) + } + + @Test + fun hostIpv6WithIpv4SuffixWithHexadecimalPrefix() { + // Chrome interprets a leading '0x' as hexadecimal; Firefox rejects them. (We reject them.) + assertInvalid( + "http://[0:0:0:0:0:1:0.0x10.0.0x10]/", + "Invalid URL host: \"[0:0:0:0:0:1:0.0x10.0.0x10]\"" + ) + } + + @Test + fun hostIpv6WithMalformedIpv4Suffix() { + assertInvalid( + "http://[0:0:0:0:0:1:0.0:0.0]/", + "Invalid URL host: \"[0:0:0:0:0:1:0.0:0.0]\"" + ) + assertInvalid( + "http://[0:0:0:0:0:1:0.0-0.0]/", + "Invalid URL host: \"[0:0:0:0:0:1:0.0-0.0]\"" + ) + assertInvalid( + "http://[0:0:0:0:0:1:.255.255.255]/", + "Invalid URL host: \"[0:0:0:0:0:1:.255.255.255]\"" + ) + assertInvalid( + "http://[0:0:0:0:0:1:255..255.255]/", + "Invalid URL host: \"[0:0:0:0:0:1:255..255.255]\"" + ) + assertInvalid( + "http://[0:0:0:0:0:1:255.255..255]/", + "Invalid URL host: \"[0:0:0:0:0:1:255.255..255]\"" + ) + assertInvalid( + "http://[0:0:0:0:0:0:1:255.255]/", + "Invalid URL host: \"[0:0:0:0:0:0:1:255.255]\"" + ) + assertInvalid( + "http://[0:0:0:0:0:1:256.255.255.255]/", + "Invalid URL host: \"[0:0:0:0:0:1:256.255.255.255]\"" + ) + assertInvalid( + "http://[0:0:0:0:0:1:ff.255.255.255]/", + "Invalid URL host: \"[0:0:0:0:0:1:ff.255.255.255]\"" + ) + assertInvalid( + "http://[0:0:0:0:0:0:1:255.255.255.255]/", + "Invalid URL host: \"[0:0:0:0:0:0:1:255.255.255.255]\"" + ) + assertInvalid( + "http://[0:0:0:0:1:255.255.255.255]/", + "Invalid URL host: \"[0:0:0:0:1:255.255.255.255]\"" + ) + assertInvalid( + "http://[0:0:0:0:1:0.0.0.0:1]/", + "Invalid URL host: \"[0:0:0:0:1:0.0.0.0:1]\"" + ) + assertInvalid( + "http://[0:0.0.0.0:1:0:0:0:0:1]/", + "Invalid URL host: \"[0:0.0.0.0:1:0:0:0:0:1]\"" + ) + assertInvalid( + "http://[0.0.0.0:0:0:0:0:0:1]/", + "Invalid URL host: \"[0.0.0.0:0:0:0:0:0:1]\"" + ) + } + + @Test + fun hostIpv6WithIncompleteIpv4Suffix() { + // To Chrome & Safari these are well-formed; Firefox disagrees. (We're consistent with Firefox). + assertInvalid( + "http://[0:0:0:0:0:1:255.255.255.]/", + "Invalid URL host: \"[0:0:0:0:0:1:255.255.255.]\"" + ) + assertInvalid( + "http://[0:0:0:0:0:1:255.255.255]/", + "Invalid URL host: \"[0:0:0:0:0:1:255.255.255]\"" + ) + } + + @Test + fun hostIpv6Malformed() { + assertInvalid("http://[::g]/", "Invalid URL host: \"[::g]\"") + } + + /** + * The builder permits square braces but does not require them. + */ + @Test + fun hostIpv6Builder() { + val base = parse("http://example.com/") + assertThat(base.newBuilder().host("[::1]").build().toString()) + .isEqualTo("http://[::1]/") + assertThat(base.newBuilder().host("[::0001]").build().toString()) + .isEqualTo("http://[::1]/") + assertThat(base.newBuilder().host("::1").build().toString()) + .isEqualTo("http://[::1]/") + assertThat(base.newBuilder().host("::0001").build().toString()) + .isEqualTo("http://[::1]/") + } + + @Test + fun relativePath() { + val base = parse("http://host/a/b/c") + assertThat(base.resolve("d/e/f")).isEqualTo(parse("http://host/a/b/d/e/f")) + assertThat(base.resolve("../../d/e/f")).isEqualTo(parse("http://host/d/e/f")) + assertThat(base.resolve("..")).isEqualTo(parse("http://host/a/")) + assertThat(base.resolve("../..")).isEqualTo(parse("http://host/")) + assertThat(base.resolve("../../..")).isEqualTo(parse("http://host/")) + assertThat(base.resolve(".")).isEqualTo(parse("http://host/a/b/")) + assertThat(base.resolve("././..")).isEqualTo(parse("http://host/a/")) + assertThat(base.resolve("c/d/../e/../")).isEqualTo(parse("http://host/a/b/c/")) + assertThat(base.resolve("..e/")).isEqualTo(parse("http://host/a/b/..e/")) + assertThat(base.resolve("e/f../")).isEqualTo(parse("http://host/a/b/e/f../")) + assertThat(base.resolve("%2E.")).isEqualTo(parse("http://host/a/")) + assertThat(base.resolve(".%2E")).isEqualTo(parse("http://host/a/")) + assertThat(base.resolve("%2E%2E")).isEqualTo(parse("http://host/a/")) + assertThat(base.resolve("%2e.")).isEqualTo(parse("http://host/a/")) + assertThat(base.resolve(".%2e")).isEqualTo(parse("http://host/a/")) + assertThat(base.resolve("%2e%2e")).isEqualTo(parse("http://host/a/")) + assertThat(base.resolve("%2E")).isEqualTo(parse("http://host/a/b/")) + assertThat(base.resolve("%2e")).isEqualTo(parse("http://host/a/b/")) + } + + @Test + fun relativePathWithTrailingSlash() { + val base = parse("http://host/a/b/c/") + assertThat(base.resolve("..")).isEqualTo(parse("http://host/a/b/")) + assertThat(base.resolve("../")).isEqualTo(parse("http://host/a/b/")) + assertThat(base.resolve("../..")).isEqualTo(parse("http://host/a/")) + assertThat(base.resolve("../../")).isEqualTo(parse("http://host/a/")) + assertThat(base.resolve("../../..")).isEqualTo(parse("http://host/")) + assertThat(base.resolve("../../../")).isEqualTo(parse("http://host/")) + assertThat(base.resolve("../../../..")).isEqualTo(parse("http://host/")) + assertThat(base.resolve("../../../../")).isEqualTo(parse("http://host/")) + assertThat(base.resolve("../../../../a")).isEqualTo(parse("http://host/a")) + assertThat(base.resolve("../../../../a/..")).isEqualTo(parse("http://host/")) + assertThat(base.resolve("../../../../a/b/..")).isEqualTo(parse("http://host/a/")) + } + + @Test + fun pathWithBackslash() { + val base = parse("http://host/a/b/c") + assertThat(base.resolve("d\\e\\f")).isEqualTo(parse("http://host/a/b/d/e/f")) + assertThat(base.resolve("../..\\d\\e\\f")) + .isEqualTo(parse("http://host/d/e/f")) + assertThat(base.resolve("..\\..")).isEqualTo(parse("http://host/")) + } + + @Test + fun relativePathWithSameScheme() { + val base = parse("http://host/a/b/c") + assertThat(base.resolve("http:d/e/f")).isEqualTo(parse("http://host/a/b/d/e/f")) + assertThat(base.resolve("http:../../d/e/f")) + .isEqualTo(parse("http://host/d/e/f")) + } + + @Test + fun decodeUsername() { + assertThat(parse("http://user@host/").username).isEqualTo("user") + assertThat(parse("http://%F0%9F%8D%A9@host/").username).isEqualTo("\uD83C\uDF69") + } + + @Test + fun decodePassword() { + assertThat(parse("http://user:password@host/").password).isEqualTo("password") + assertThat(parse("http://user:@host/").password).isEqualTo("") + assertThat(parse("http://user:%F0%9F%8D%A9@host/").password) + .isEqualTo("\uD83C\uDF69") + } + + @Test + fun decodeSlashCharacterInDecodedPathSegment() { + assertThat(parse("http://host/a%2Fb%2Fc").pathSegments).containsExactly("a/b/c") + } + + @Test + fun decodeEmptyPathSegments() { + assertThat(parse("http://host/").pathSegments).containsExactly("") + } + + @Test + fun percentDecode() { + assertThat(parse("http://host/%00").pathSegments).containsExactly("\u0000") + assertThat(parse("http://host/a/%E2%98%83/c").pathSegments) + .containsExactly("a", "\u2603", "c") + assertThat(parse("http://host/a/%F0%9F%8D%A9/c").pathSegments) + .containsExactly("a", "\uD83C\uDF69", "c") + assertThat(parse("http://host/a/%62/c").pathSegments) + .containsExactly("a", "b", "c") + assertThat(parse("http://host/a/%7A/c").pathSegments) + .containsExactly("a", "z", "c") + assertThat(parse("http://host/a/%7a/c").pathSegments) + .containsExactly("a", "z", "c") + } + + @Test + fun malformedPercentEncoding() { + assertThat(parse("http://host/a%f/b").pathSegments).containsExactly("a%f", "b") + assertThat(parse("http://host/%/b").pathSegments).containsExactly("%", "b") + assertThat(parse("http://host/%").pathSegments).containsExactly("%") + assertThat(parse("http://github.com/%%30%30").pathSegments) + .containsExactly("%00") + } + + @Test + fun malformedUtf8Encoding() { + // Replace a partial UTF-8 sequence with the Unicode replacement character. + assertThat(parse("http://host/a/%E2%98x/c").pathSegments) + .containsExactly("a", "\ufffdx", "c") + } + + @Test + fun incompleteUrlComposition() { + val noHost = assertFailsWith { + HttpUrl.Builder().scheme("http").build() + } + assertThat(noHost.message).isEqualTo("host == null") + val noScheme = assertFailsWith { + HttpUrl.Builder().host("host").build() + } + assertThat(noScheme.message).isEqualTo("scheme == null") + } + + @Test + fun builderToString() { + assertThat(parse("https://host.com/path").newBuilder().toString()) + .isEqualTo("https://host.com/path") + } + + @Test + fun incompleteBuilderToString() { + assertThat(HttpUrl.Builder().scheme("https").encodedPath("/path").toString()) + .isEqualTo("https:///path") + assertThat(HttpUrl.Builder().host("host.com").encodedPath("/path").toString()) + .isEqualTo("//host.com/path") + assertThat( + HttpUrl.Builder().host("host.com").encodedPath("/path").port(8080).toString() + ) + .isEqualTo("//host.com:8080/path") + } + + @Test + fun changingSchemeChangesDefaultPort() { + assertThat( + parse("http://example.com") + .newBuilder() + .scheme("https") + .build().port + ).isEqualTo(443) + assertThat( + parse("https://example.com") + .newBuilder() + .scheme("http") + .build().port + ).isEqualTo(80) + assertThat( + parse("https://example.com:1234") + .newBuilder() + .scheme("http") + .build().port + ).isEqualTo(1234) + } + + @Test + fun composeWithEncodedPath() { + val url = HttpUrl.Builder() + .scheme("http") + .host("host") + .encodedPath("/a%2Fb/c") + .build() + assertThat(url.toString()).isEqualTo("http://host/a%2Fb/c") + assertThat(url.encodedPath).isEqualTo("/a%2Fb/c") + assertThat(url.pathSegments).containsExactly("a/b", "c") + } + + @Test + fun composeMixingPathSegments() { + val url = HttpUrl.Builder() + .scheme("http") + .host("host") + .encodedPath("/a%2fb/c") + .addPathSegment("d%25e") + .addEncodedPathSegment("f%25g") + .build() + assertThat(url.toString()).isEqualTo("http://host/a%2fb/c/d%2525e/f%25g") + assertThat(url.encodedPath).isEqualTo("/a%2fb/c/d%2525e/f%25g") + assertThat(url.encodedPathSegments) + .containsExactly("a%2fb", "c", "d%2525e", "f%25g") + assertThat(url.pathSegments).containsExactly("a/b", "c", "d%25e", "f%g") + } + + @Test + fun composeWithAddSegment() { + val base = parse("http://host/a/b/c") + assertThat(base.newBuilder() + .addPathSegment("") + .build().encodedPath) + .isEqualTo("/a/b/c/") + assertThat(base.newBuilder() + .addPathSegment("") + .addPathSegment("d") + .build().encodedPath) + .isEqualTo("/a/b/c/d") + assertThat(base.newBuilder() + .addPathSegment("..") + .build().encodedPath) + .isEqualTo("/a/b/") + assertThat(base.newBuilder() + .addPathSegment("") + .addPathSegment("..") + .build().encodedPath) + .isEqualTo("/a/b/") + assertThat(base.newBuilder() + .addPathSegment("") + .addPathSegment("") + .build().encodedPath) + .isEqualTo("/a/b/c/") + } + + @Test + fun addPathSegments() { + val base = parse("http://host/a/b/c") + + // Add a string with zero slashes: resulting URL gains one slash. + assertThat(base.newBuilder().addPathSegments("").build().encodedPath) + .isEqualTo("/a/b/c/") + assertThat(base.newBuilder().addPathSegments("d").build().encodedPath) + .isEqualTo("/a/b/c/d") + + // Add a string with one slash: resulting URL gains two slashes. + assertThat(base.newBuilder().addPathSegments("/").build().encodedPath) + .isEqualTo("/a/b/c//") + assertThat(base.newBuilder().addPathSegments("d/").build().encodedPath) + .isEqualTo("/a/b/c/d/") + assertThat(base.newBuilder().addPathSegments("/d").build().encodedPath) + .isEqualTo("/a/b/c//d") + + // Add a string with two slashes: resulting URL gains three slashes. + assertThat(base.newBuilder().addPathSegments("//").build().encodedPath) + .isEqualTo("/a/b/c///") + assertThat(base.newBuilder().addPathSegments("/d/").build().encodedPath) + .isEqualTo("/a/b/c//d/") + assertThat(base.newBuilder().addPathSegments("d//").build().encodedPath) + .isEqualTo("/a/b/c/d//") + assertThat(base.newBuilder().addPathSegments("//d").build().encodedPath) + .isEqualTo("/a/b/c///d") + assertThat(base.newBuilder().addPathSegments("d/e/f").build().encodedPath) + .isEqualTo("/a/b/c/d/e/f") + } + + @Test + fun addPathSegmentsOntoTrailingSlash() { + val base = parse("http://host/a/b/c/") + + // Add a string with zero slashes: resulting URL gains zero slashes. + assertThat(base.newBuilder().addPathSegments("").build().encodedPath) + .isEqualTo("/a/b/c/") + assertThat(base.newBuilder().addPathSegments("d").build().encodedPath) + .isEqualTo("/a/b/c/d") + + // Add a string with one slash: resulting URL gains one slash. + assertThat(base.newBuilder().addPathSegments("/").build().encodedPath) + .isEqualTo("/a/b/c//") + assertThat(base.newBuilder().addPathSegments("d/").build().encodedPath) + .isEqualTo("/a/b/c/d/") + assertThat(base.newBuilder().addPathSegments("/d").build().encodedPath) + .isEqualTo("/a/b/c//d") + + // Add a string with two slashes: resulting URL gains two slashes. + assertThat(base.newBuilder().addPathSegments("//").build().encodedPath) + .isEqualTo("/a/b/c///") + assertThat(base.newBuilder().addPathSegments("/d/").build().encodedPath) + .isEqualTo("/a/b/c//d/") + assertThat(base.newBuilder().addPathSegments("d//").build().encodedPath) + .isEqualTo("/a/b/c/d//") + assertThat(base.newBuilder().addPathSegments("//d").build().encodedPath) + .isEqualTo("/a/b/c///d") + assertThat(base.newBuilder().addPathSegments("d/e/f").build().encodedPath) + .isEqualTo("/a/b/c/d/e/f") + } + + @Test + fun addPathSegmentsWithBackslash() { + val base = parse("http://host/") + assertThat(base.newBuilder().addPathSegments("d\\e").build().encodedPath) + .isEqualTo("/d/e") + assertThat(base.newBuilder().addEncodedPathSegments("d\\e").build().encodedPath) + .isEqualTo("/d/e") + } + + @Test + fun addPathSegmentsWithEmptyPaths() { + val base = parse("http://host/a/b/c") + assertThat(base.newBuilder().addPathSegments("/d/e///f").build().encodedPath) + .isEqualTo("/a/b/c//d/e///f") + } + + @Test + fun addEncodedPathSegments() { + val base = parse("http://host/a/b/c") + assertThat( + base.newBuilder().addEncodedPathSegments("d/e/%20/\n").build().encodedPath as Any + ).isEqualTo("/a/b/c/d/e/%20/") + } + + @Test + fun addPathSegmentDotDoesNothing() { + val base = parse("http://host/a/b/c") + assertThat(base.newBuilder().addPathSegment(".").build().encodedPath) + .isEqualTo("/a/b/c") + } + + @Test + fun addPathSegmentEncodes() { + val base = parse("http://host/a/b/c") + assertThat(base.newBuilder().addPathSegment("%2e").build().encodedPath) + .isEqualTo("/a/b/c/%252e") + assertThat(base.newBuilder().addPathSegment("%2e%2e").build().encodedPath) + .isEqualTo("/a/b/c/%252e%252e") + } + + @Test + fun addPathSegmentDotDotPopsDirectory() { + val base = parse("http://host/a/b/c") + assertThat(base.newBuilder().addPathSegment("..").build().encodedPath) + .isEqualTo("/a/b/") + } + + @Test + fun addPathSegmentDotAndIgnoredCharacter() { + val base = parse("http://host/a/b/c") + assertThat(base.newBuilder().addPathSegment(".\n").build().encodedPath) + .isEqualTo("/a/b/c/.%0A") + } + + @Test + fun addEncodedPathSegmentDotAndIgnoredCharacter() { + val base = parse("http://host/a/b/c") + assertThat(base.newBuilder().addEncodedPathSegment(".\n").build().encodedPath) + .isEqualTo("/a/b/c") + } + + @Test + fun addEncodedPathSegmentDotDotAndIgnoredCharacter() { + val base = parse("http://host/a/b/c") + assertThat(base.newBuilder().addEncodedPathSegment("..\n").build().encodedPath) + .isEqualTo("/a/b/") + } + + @Test + fun setPathSegment() { + val base = parse("http://host/a/b/c") + assertThat(base.newBuilder().setPathSegment(0, "d").build().encodedPath) + .isEqualTo("/d/b/c") + assertThat(base.newBuilder().setPathSegment(1, "d").build().encodedPath) + .isEqualTo("/a/d/c") + assertThat(base.newBuilder().setPathSegment(2, "d").build().encodedPath) + .isEqualTo("/a/b/d") + } + + @Test + fun setPathSegmentAcceptsEmpty() { + val base = parse("http://host/a/b/c") + assertThat(base.newBuilder().setPathSegment(0, "").build().encodedPath) + .isEqualTo("//b/c") + assertThat(base.newBuilder().setPathSegment(2, "").build().encodedPath) + .isEqualTo("/a/b/") + } + + @Test + fun setPathSegmentRejectsDot() { + val base = parse("http://host/a/b/c") + assertFailsWith { + base.newBuilder().setPathSegment(0, ".") + } + } + + @Test + fun setPathSegmentRejectsDotDot() { + val base = parse("http://host/a/b/c") + assertFailsWith { + base.newBuilder().setPathSegment(0, "..") + } + } + + @Test + fun setPathSegmentOutOfBounds() { + assertFailsWith { + HttpUrl.Builder().setPathSegment(1, "a") + } + } + + @Test + fun setEncodedPathSegmentEncodes() { + val base = parse("http://host/a/b/c") + assertThat(base.newBuilder().setEncodedPathSegment(0, "%25").build().encodedPath) + .isEqualTo("/%25/b/c") + } + + @Test + fun setEncodedPathSegmentRejectsDot() { + val base = parse("http://host/a/b/c") + assertFailsWith { + base.newBuilder().setEncodedPathSegment(0, ".") + } + } + + @Test + fun setEncodedPathSegmentRejectsDotDot() { + val base = parse("http://host/a/b/c") + assertFailsWith { + base.newBuilder().setEncodedPathSegment(0, "..") + } + } + + @Test + fun setEncodedPathSegmentOutOfBounds() { + assertFailsWith { + HttpUrl.Builder().setEncodedPathSegment(1, "a") + } + } + + @Test + fun removePathSegment() { + val base = parse("http://host/a/b/c") + val url = base.newBuilder() + .removePathSegment(0) + .build() + assertThat(url.encodedPath).isEqualTo("/b/c") + } + + @Test + fun removePathSegmentDoesntRemovePath() { + val base = parse("http://host/a/b/c") + val url = base.newBuilder() + .removePathSegment(0) + .removePathSegment(0) + .removePathSegment(0) + .build() + assertThat(url.pathSegments).containsExactly("") + assertThat(url.encodedPath).isEqualTo("/") + } + + @Test + fun removePathSegmentOutOfBounds() { + assertFailsWith { + HttpUrl.Builder().removePathSegment(1) + } + } + + /** + * When callers use `addEncodedQueryParameter()` we only encode what's strictly required. We + * retain the encoded (or non-encoded) state of the input. + */ + @Test + fun queryCharactersNotReencodedWhenComposedWithAddEncoded() { + val url = HttpUrl.Builder() + .scheme("http") + .host("host") + .addEncodedQueryParameter("a", "!$(),/:;?@[]\\^`{|}~") + .build() + assertThat(url.toString()).isEqualTo("http://host/?a=!$(),/:;?@[]\\^`{|}~") + assertThat(url.queryParameter("a")).isEqualTo("!$(),/:;?@[]\\^`{|}~") + } + + /** + * When callers parse a URL with query components that aren't encoded, we shouldn't convert them + * into a canonical form because doing so could be semantically different. + */ + @Test + fun queryCharactersNotReencodedWhenParsed() { + val url = parse("http://host/?a=!$(),/:;?@[]\\^`{|}~") + assertThat(url.toString()).isEqualTo("http://host/?a=!$(),/:;?@[]\\^`{|}~") + assertThat(url.queryParameter("a")).isEqualTo("!$(),/:;?@[]\\^`{|}~") + } + + @Test + fun composeQueryRemoveQueryParameter() { + val url = parse("http://host/").newBuilder() + .addQueryParameter("a+=& b", "c+=& d") + .removeAllQueryParameters("a+=& b") + .build() + assertThat(url.toString()).isEqualTo("http://host/") + assertThat(url.queryParameter("a+=& b")).isNull() + } + + @Test + fun composeQueryRemoveEncodedQueryParameter() { + val url = parse("http://host/").newBuilder() + .addEncodedQueryParameter("a+=& b", "c+=& d") + .removeAllEncodedQueryParameters("a+=& b") + .build() + assertThat(url.toString()).isEqualTo("http://host/") + assertThat(url.queryParameter("a =& b")).isNull() + } + + @Test + fun absentQueryIsZeroNameValuePairs() { + val url = parse("http://host/").newBuilder() + .query(null) + .build() + assertThat(url.querySize).isEqualTo(0) + } + + @Test + fun emptyQueryIsSingleNameValuePairWithEmptyKey() { + val url = parse("http://host/").newBuilder() + .query("") + .build() + assertThat(url.querySize).isEqualTo(1) + assertThat(url.queryParameterName(0)).isEqualTo("") + assertThat(url.queryParameterValue(0)).isNull() + } + + @Test + fun ampersandQueryIsTwoNameValuePairsWithEmptyKeys() { + val url = parse("http://host/").newBuilder() + .query("&") + .build() + assertThat(url.querySize).isEqualTo(2) + assertThat(url.queryParameterName(0)).isEqualTo("") + assertThat(url.queryParameterValue(0)).isNull() + assertThat(url.queryParameterName(1)).isEqualTo("") + assertThat(url.queryParameterValue(1)).isNull() + } + + @Test + fun removeAllDoesNotRemoveQueryIfNoParametersWereRemoved() { + val url = parse("http://host/").newBuilder() + .query("") + .removeAllQueryParameters("a") + .build() + assertThat(url.toString()).isEqualTo("http://host/?") + } + + @Test + fun queryParametersWithRepeatedName() { + val url = parse("http://host/?foo[]=1&foo[]=2&foo[]=3") + assertThat(url.querySize).isEqualTo(3) + assertThat(url.queryParameterNames).isEqualTo(setOf("foo[]")) + assertThat(url.queryParameterValue(0)).isEqualTo("1") + assertThat(url.queryParameterValue(1)).isEqualTo("2") + assertThat(url.queryParameterValue(2)).isEqualTo("3") + assertThat(url.queryParameterValues("foo[]")).containsExactly("1", "2", "3") + } + + @Test + fun queryParameterLookupWithNonCanonicalEncoding() { + val url = parse("http://host/?%6d=m&+=%20") + assertThat(url.queryParameterName(0)).isEqualTo("m") + assertThat(url.queryParameterName(1)).isEqualTo(" ") + assertThat(url.queryParameter("m")).isEqualTo("m") + assertThat(url.queryParameter(" ")).isEqualTo(" ") + } + + @Test + fun parsedQueryDoesntIncludeFragment() { + val url = parse("http://host/?#fragment") + assertThat(url.fragment).isEqualTo("fragment") + assertThat(url.query).isEqualTo("") + assertThat(url.encodedQuery).isEqualTo("") + } + + /** + * Although HttpUrl prefers percent-encodings in uppercase, it should preserve the exact structure + * of the original encoding. + */ + @Test + fun rawEncodingRetained() { + val urlString = "http://%6d%6D:%6d%6D@host/%6d%6D?%6d%6D#%6d%6D" + val url = parse(urlString) + assertThat(url.encodedUsername).isEqualTo("%6d%6D") + assertThat(url.encodedPassword).isEqualTo("%6d%6D") + assertThat(url.encodedPath).isEqualTo("/%6d%6D") + assertThat(url.encodedPathSegments).containsExactly("%6d%6D") + assertThat(url.encodedQuery).isEqualTo("%6d%6D") + assertThat(url.encodedFragment).isEqualTo("%6d%6D") + assertThat(url.toString()).isEqualTo(urlString) + assertThat(url.newBuilder().build().toString()).isEqualTo(urlString) + assertThat(url.resolve("").toString()) + .isEqualTo("http://%6d%6D:%6d%6D@host/%6d%6D?%6d%6D") + } + + @Test + fun clearFragment() { + val url = parse("http://host/#fragment") + .newBuilder() + .fragment(null) + .build() + assertThat(url.toString()).isEqualTo("http://host/") + assertThat(url.fragment).isNull() + assertThat(url.encodedFragment).isNull() + } + + @Test + fun clearEncodedFragment() { + val url = parse("http://host/#fragment") + .newBuilder() + .encodedFragment(null) + .build() + assertThat(url.toString()).isEqualTo("http://host/") + assertThat(url.fragment).isNull() + assertThat(url.encodedFragment).isNull() + } + + @Test + fun unparseableTopPrivateDomain() { + assertInvalid("http://a../", "Invalid URL host: \"a..\"") + assertInvalid("http://..a/", "Invalid URL host: \"..a\"") + assertInvalid("http://a..b/", "Invalid URL host: \"a..b\"") + assertInvalid("http://.a/", "Invalid URL host: \".a\"") + assertInvalid("http://../", "Invalid URL host: \"..\"") + } + + @Test + fun trailingDotIsOkay() { + val name251 = "a.".repeat(125) + "a" + assertThat(parse("http://a./").toString()).isEqualTo("http://a./") + assertThat(parse("http://${name251}a./").toString()).isEqualTo("http://${name251}a./") + assertThat(parse("http://${name251}aa/").toString()).isEqualTo("http://${name251}aa/") + assertInvalid("http://${name251}aa./", "Invalid URL host: \"${name251}aa.\"") + } + + @Test + fun labelIsEmpty() { + assertInvalid("http:///", "Invalid URL host: \"\"") + assertInvalid("http://a..b/", "Invalid URL host: \"a..b\"") + assertInvalid("http://.a/", "Invalid URL host: \".a\"") + assertInvalid("http://./", "Invalid URL host: \".\"") + assertInvalid("http://../", "Invalid URL host: \"..\"") + assertInvalid("http://.../", "Invalid URL host: \"...\"") + assertInvalid("http://…/", "Invalid URL host: \"…\"") + } + + @Test + fun labelTooLong() { + val a63 = "a".repeat(63) + assertThat(parse("http://$a63/").toString()).isEqualTo("http://$a63/") + assertThat(parse("http://a.$a63/").toString()).isEqualTo("http://a.$a63/") + assertThat(parse("http://$a63.a/").toString()).isEqualTo("http://$a63.a/") + assertInvalid("http://a$a63/", "Invalid URL host: \"a$a63\"") + assertInvalid("http://a.a$a63/", "Invalid URL host: \"a.a$a63\"") + assertInvalid("http://a$a63.a/", "Invalid URL host: \"a$a63.a\"") + } + + @Test + fun hostnameTooLong() { + val dotA126 = "a.".repeat(126) + assertThat(parse("http://a$dotA126/").toString()) + .isEqualTo("http://a$dotA126/") + assertInvalid("http://aa$dotA126/", "Invalid URL host: \"aa$dotA126\"") + } +} diff --git a/okhttp/src/jsTest/kotlin/okhttp3/MediaTypeJsTest.kt b/okhttp/src/jsTest/kotlin/okhttp3/MediaTypeJsTest.kt index 04165eb21..40f44458a 100644 --- a/okhttp/src/jsTest/kotlin/okhttp3/MediaTypeJsTest.kt +++ b/okhttp/src/jsTest/kotlin/okhttp3/MediaTypeJsTest.kt @@ -20,7 +20,7 @@ import kotlin.test.assertEquals import okhttp3.MediaType.Companion.toMediaType class MediaTypeJsTest { - open fun MediaType.charsetName(): String? = parameter("charset") + fun MediaType.charsetName(): String? = parameter("charset") @Test fun testIllegalCharsetName() { diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/FormBody.kt b/okhttp/src/jvmMain/kotlin/okhttp3/FormBody.kt index 747086a8d..117b3cff9 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/FormBody.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/FormBody.kt @@ -17,10 +17,10 @@ package okhttp3 import java.io.IOException import java.nio.charset.Charset -import okhttp3.HttpUrl.Companion.FORM_ENCODE_SET -import okhttp3.HttpUrl.Companion.canonicalize -import okhttp3.HttpUrl.Companion.percentDecode import okhttp3.MediaType.Companion.toMediaType +import okhttp3.internal.CommonHttpUrl.FORM_ENCODE_SET +import okhttp3.internal.CommonHttpUrl.percentDecode +import okhttp3.internal.JvmHttpUrl.canonicalizeWithCharset import okhttp3.internal.toImmutableList import okio.Buffer import okio.BufferedSink @@ -90,12 +90,12 @@ class FormBody internal constructor( private val values = mutableListOf() fun add(name: String, value: String) = apply { - names += name.canonicalize( + names += name.canonicalizeWithCharset( encodeSet = FORM_ENCODE_SET, plusIsSpace = false, // plus is encoded as `%2B`, space is encoded as plus. charset = charset ) - values += value.canonicalize( + values += value.canonicalizeWithCharset( encodeSet = FORM_ENCODE_SET, plusIsSpace = false, // plus is encoded as `%2B`, space is encoded as plus. charset = charset @@ -103,13 +103,13 @@ class FormBody internal constructor( } fun addEncoded(name: String, value: String) = apply { - names += name.canonicalize( + names += name.canonicalizeWithCharset( encodeSet = FORM_ENCODE_SET, alreadyEncoded = true, plusIsSpace = true, charset = charset ) - values += value.canonicalize( + values += value.canonicalizeWithCharset( encodeSet = FORM_ENCODE_SET, alreadyEncoded = true, plusIsSpace = true, diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/HttpUrl.kt b/okhttp/src/jvmMain/kotlin/okhttp3/HttpUrl.kt index 2b8723e82..f584c1f75 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/HttpUrl.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/HttpUrl.kt @@ -20,19 +20,58 @@ import java.net.MalformedURLException import java.net.URI import java.net.URISyntaxException import java.net.URL -import java.nio.charset.Charset -import java.util.Collections -import kotlin.text.Charsets.UTF_8 import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.internal.CommonHttpUrl.FRAGMENT_ENCODE_SET_URI +import okhttp3.internal.CommonHttpUrl.PATH_SEGMENT_ENCODE_SET_URI +import okhttp3.internal.CommonHttpUrl.QUERY_COMPONENT_ENCODE_SET_URI +import okhttp3.internal.CommonHttpUrl.commonAddEncodedPathSegment +import okhttp3.internal.CommonHttpUrl.commonAddEncodedPathSegments +import okhttp3.internal.CommonHttpUrl.commonAddEncodedQueryParameter +import okhttp3.internal.CommonHttpUrl.commonAddPathSegment +import okhttp3.internal.CommonHttpUrl.commonAddPathSegments +import okhttp3.internal.CommonHttpUrl.commonAddQueryParameter +import okhttp3.internal.CommonHttpUrl.commonBuild +import okhttp3.internal.CommonHttpUrl.commonDefaultPort +import okhttp3.internal.CommonHttpUrl.commonEncodedFragment +import okhttp3.internal.CommonHttpUrl.commonEncodedPassword +import okhttp3.internal.CommonHttpUrl.commonEncodedPath +import okhttp3.internal.CommonHttpUrl.commonEncodedPathSegments +import okhttp3.internal.CommonHttpUrl.commonEncodedQuery +import okhttp3.internal.CommonHttpUrl.commonEncodedUsername +import okhttp3.internal.CommonHttpUrl.commonEquals +import okhttp3.internal.CommonHttpUrl.commonFragment +import okhttp3.internal.CommonHttpUrl.commonHashCode +import okhttp3.internal.CommonHttpUrl.commonHost +import okhttp3.internal.CommonHttpUrl.commonNewBuilder +import okhttp3.internal.CommonHttpUrl.commonParse +import okhttp3.internal.CommonHttpUrl.commonPassword +import okhttp3.internal.CommonHttpUrl.commonPathSize +import okhttp3.internal.CommonHttpUrl.commonPort +import okhttp3.internal.CommonHttpUrl.commonQuery +import okhttp3.internal.CommonHttpUrl.commonQueryParameter +import okhttp3.internal.CommonHttpUrl.commonQueryParameterName +import okhttp3.internal.CommonHttpUrl.commonQueryParameterNames +import okhttp3.internal.CommonHttpUrl.commonQueryParameterValue +import okhttp3.internal.CommonHttpUrl.commonQueryParameterValues +import okhttp3.internal.CommonHttpUrl.commonQuerySize +import okhttp3.internal.CommonHttpUrl.commonRedact +import okhttp3.internal.CommonHttpUrl.commonRemoveAllEncodedQueryParameters +import okhttp3.internal.CommonHttpUrl.commonRemoveAllQueryParameters +import okhttp3.internal.CommonHttpUrl.commonRemovePathSegment +import okhttp3.internal.CommonHttpUrl.commonResolve +import okhttp3.internal.CommonHttpUrl.commonScheme +import okhttp3.internal.CommonHttpUrl.commonSetEncodedPathSegment +import okhttp3.internal.CommonHttpUrl.commonSetEncodedQueryParameter +import okhttp3.internal.CommonHttpUrl.commonSetPathSegment +import okhttp3.internal.CommonHttpUrl.commonSetQueryParameter +import okhttp3.internal.CommonHttpUrl.commonToHttpUrl +import okhttp3.internal.CommonHttpUrl.commonToHttpUrlOrNull +import okhttp3.internal.CommonHttpUrl.commonToString +import okhttp3.internal.CommonHttpUrl.commonUsername +import okhttp3.internal.HttpUrlCommon.canonicalize import okhttp3.internal.canParseAsIpAddress -import okhttp3.internal.delimiterOffset -import okhttp3.internal.indexOfFirstNonAsciiWhitespace -import okhttp3.internal.indexOfLastNonAsciiWhitespace -import okhttp3.internal.parseHexDigit import okhttp3.internal.publicsuffix.PublicSuffixDatabase -import okhttp3.internal.toCanonicalHost -import okio.Buffer /** * A uniform resource locator (URL) with a scheme of either `http` or `https`. Use this class to @@ -284,103 +323,33 @@ import okio.Buffer * * [idna]: http://www.unicode.org/reports/tr46/#ToASCII */ -class HttpUrl internal constructor( - /** Either "http" or "https". */ - @get:JvmName("scheme") val scheme: String, +actual class HttpUrl internal actual constructor( + @get:JvmName("scheme") actual val scheme: String, - /** - * The decoded username, or an empty string if none is present. - * - * | URL | `username()` | - * | :------------------------------- | :----------- | - * | `http://host/` | `""` | - * | `http://username@host/` | `"username"` | - * | `http://username:password@host/` | `"username"` | - * | `http://a%20b:c%20d@host/` | `"a b"` | - */ - @get:JvmName("username") val username: String, + @get:JvmName("username") actual val username: String, - /** - * Returns the decoded password, or an empty string if none is present. - * - * | URL | `password()` | - * | :------------------------------- | :----------- | - * | `http://host/` | `""` | - * | `http://username@host/` | `""` | - * | `http://username:password@host/` | `"password"` | - * | `http://a%20b:c%20d@host/` | `"c d"` | - */ - @get:JvmName("password") val password: String, + @get:JvmName("password") actual val password: String, - /** - * The host address suitable for use with [InetAddress.getAllByName]. May be: - * - * * A regular host name, like `android.com`. - * - * * An IPv4 address, like `127.0.0.1`. - * - * * An IPv6 address, like `::1`. Note that there are no square braces. - * - * * An encoded IDN, like `xn--n3h.net`. - * - * | URL | `host()` | - * | :-------------------- | :-------------- | - * | `http://android.com/` | `"android.com"` | - * | `http://127.0.0.1/` | `"127.0.0.1"` | - * | `http://[::1]/` | `"::1"` | - * | `http://xn--n3h.net/` | `"xn--n3h.net"` | - */ - @get:JvmName("host") val host: String, + @get:JvmName("host") actual val host: String, - /** - * The explicitly-specified port if one was provided, or the default port for this URL's scheme. - * For example, this returns 8443 for `https://square.com:8443/` and 443 for - * `https://square.com/`. The result is in `[1..65535]`. - * - * | URL | `port()` | - * | :------------------ | :------- | - * | `http://host/` | `80` | - * | `http://host:8000/` | `8000` | - * | `https://host/` | `443` | - */ - @get:JvmName("port") val port: Int, + @get:JvmName("port") actual val port: Int, - /** - * A list of path segments like `["a", "b", "c"]` for the URL `http://host/a/b/c`. This list is - * never empty though it may contain a single empty string. - * - * | URL | `pathSegments()` | - * | :----------------------- | :------------------ | - * | `http://host/` | `[""]` | - * | `http://host/a/b/c"` | `["a", "b", "c"]` | - * | `http://host/a/b%20c/d"` | `["a", "b c", "d"]` | - */ - @get:JvmName("pathSegments") val pathSegments: List, + @get:JvmName("pathSegments") actual val pathSegments: List, /** * Alternating, decoded query names and values, or null for no query. Names may be empty or * non-empty, but never null. Values are null if the name has no corresponding '=' separator, or * empty, or non-empty. */ - private val queryNamesAndValues: List?, + internal actual val queryNamesAndValues: List?, - /** - * This URL's fragment, like `"abc"` for `http://host/#abc`. This is null if the URL has no - * fragment. - * - * | URL | `fragment()` | - * | :--------------------- | :----------- | - * | `http://host/` | null | - * | `http://host/#` | `""` | - * | `http://host/#abc` | `"abc"` | - * | `http://host/#abc|def` | `"abc|def"` | - */ - @get:JvmName("fragment") val fragment: String?, + @get:JvmName("fragment") actual val fragment: String?, /** Canonical URL. */ - private val url: String + internal actual val url: String ) { - val isHttps: Boolean = scheme == "https" + actual val isHttps: Boolean + get() = scheme == "https" /** Returns this URL as a [java.net.URL][URL]. */ @JvmName("url") fun toUrl(): URL { @@ -419,333 +388,59 @@ class HttpUrl internal constructor( } } - /** - * The username, or an empty string if none is set. - * - * | URL | `encodedUsername()` | - * | :------------------------------- | :------------------ | - * | `http://host/` | `""` | - * | `http://username@host/` | `"username"` | - * | `http://username:password@host/` | `"username"` | - * | `http://a%20b:c%20d@host/` | `"a%20b"` | - */ - @get:JvmName("encodedUsername") val encodedUsername: String - get() { - if (username.isEmpty()) return "" - val usernameStart = scheme.length + 3 // "://".length() == 3. - val usernameEnd = url.delimiterOffset(":@", usernameStart, url.length) - return url.substring(usernameStart, usernameEnd) - } + @get:JvmName("encodedUsername") actual val encodedUsername: String + get() = commonEncodedUsername - /** - * The password, or an empty string if none is set. - * - * | URL | `encodedPassword()` | - * | :--------------------------------| :------------------ | - * | `http://host/` | `""` | - * | `http://username@host/` | `""` | - * | `http://username:password@host/` | `"password"` | - * | `http://a%20b:c%20d@host/` | `"c%20d"` | - */ - @get:JvmName("encodedPassword") val encodedPassword: String - get() { - if (password.isEmpty()) return "" - val passwordStart = url.indexOf(':', scheme.length + 3) + 1 - val passwordEnd = url.indexOf('@') - return url.substring(passwordStart, passwordEnd) - } + @get:JvmName("encodedPassword") actual val encodedPassword: String + get() = commonEncodedPassword - /** - * The number of segments in this URL's path. This is also the number of slashes in this URL's - * path, like 3 in `http://host/a/b/c`. This is always at least 1. - * - * | URL | `pathSize()` | - * | :------------------- | :----------- | - * | `http://host/` | `1` | - * | `http://host/a/b/c` | `3` | - * | `http://host/a/b/c/` | `4` | - */ - @get:JvmName("pathSize") val pathSize: Int get() = pathSegments.size + @get:JvmName("pathSize") + actual val pathSize: Int + get() = commonPathSize - /** - * The entire path of this URL encoded for use in HTTP resource resolution. The returned path will - * start with `"/"`. - * - * | URL | `encodedPath()` | - * | :---------------------- | :-------------- | - * | `http://host/` | `"/"` | - * | `http://host/a/b/c` | `"/a/b/c"` | - * | `http://host/a/b%20c/d` | `"/a/b%20c/d"` | - */ - @get:JvmName("encodedPath") val encodedPath: String - get() { - val pathStart = url.indexOf('/', scheme.length + 3) // "://".length() == 3. - val pathEnd = url.delimiterOffset("?#", pathStart, url.length) - return url.substring(pathStart, pathEnd) - } + @get:JvmName("encodedPath") actual val encodedPath: String + get() = commonEncodedPath - /** - * A list of encoded path segments like `["a", "b", "c"]` for the URL `http://host/a/b/c`. This - * list is never empty though it may contain a single empty string. - * - * | URL | `encodedPathSegments()` | - * | :---------------------- | :---------------------- | - * | `http://host/` | `[""]` | - * | `http://host/a/b/c` | `["a", "b", "c"]` | - * | `http://host/a/b%20c/d` | `["a", "b%20c", "d"]` | - */ - @get:JvmName("encodedPathSegments") val encodedPathSegments: List - get() { - val pathStart = url.indexOf('/', scheme.length + 3) - val pathEnd = url.delimiterOffset("?#", pathStart, url.length) - val result = mutableListOf() - var i = pathStart - while (i < pathEnd) { - i++ // Skip the '/'. - val segmentEnd = url.delimiterOffset('/', i, pathEnd) - result.add(url.substring(i, segmentEnd)) - i = segmentEnd - } - return result - } + @get:JvmName("encodedPathSegments") actual val encodedPathSegments: List + get() = commonEncodedPathSegments - /** - * The query of this URL, encoded for use in HTTP resource resolution. This string may be null - * (for URLs with no query), empty (for URLs with an empty query) or non-empty (all other URLs). - * - * | URL | `encodedQuery()` | - * | :-------------------------------- | :--------------------- | - * | `http://host/` | null | - * | `http://host/?` | `""` | - * | `http://host/?a=apple&k=key+lime` | `"a=apple&k=key+lime"` | - * | `http://host/?a=apple&a=apricot` | `"a=apple&a=apricot"` | - * | `http://host/?a=apple&b` | `"a=apple&b"` | - */ - @get:JvmName("encodedQuery") val encodedQuery: String? - get() { - if (queryNamesAndValues == null) return null // No query. - val queryStart = url.indexOf('?') + 1 - val queryEnd = url.delimiterOffset('#', queryStart, url.length) - return url.substring(queryStart, queryEnd) - } + @get:JvmName("encodedQuery") actual val encodedQuery: String? + get() = commonEncodedQuery - /** - * This URL's query, like `"abc"` for `http://host/?abc`. Most callers should prefer - * [queryParameterName] and [queryParameterValue] because these methods offer direct access to - * individual query parameters. - * - * | URL | `query()` | - * | :-------------------------------- | :--------------------- | - * | `http://host/` | null | - * | `http://host/?` | `""` | - * | `http://host/?a=apple&k=key+lime` | `"a=apple&k=key lime"` | - * | `http://host/?a=apple&a=apricot` | `"a=apple&a=apricot"` | - * | `http://host/?a=apple&b` | `"a=apple&b"` | - */ - @get:JvmName("query") val query: String? - get() { - if (queryNamesAndValues == null) return null // No query. - val result = StringBuilder() - queryNamesAndValues.toQueryString(result) - return result.toString() - } + @get:JvmName("query") actual val query: String? + get() = commonQuery - /** - * The number of query parameters in this URL, like 2 for `http://host/?a=apple&b=banana`. If this - * URL has no query this is 0. Otherwise it is one more than the number of `"&"` separators in the - * query. - * - * | URL | `querySize()` | - * | :-------------------------------- | :------------ | - * | `http://host/` | `0` | - * | `http://host/?` | `1` | - * | `http://host/?a=apple&k=key+lime` | `2` | - * | `http://host/?a=apple&a=apricot` | `2` | - * | `http://host/?a=apple&b` | `2` | - */ - @get:JvmName("querySize") val querySize: Int - get() { - return if (queryNamesAndValues != null) queryNamesAndValues.size / 2 else 0 - } + @get:JvmName("querySize") actual val querySize: Int + get() = commonQuerySize - /** - * The first query parameter named `name` decoded using UTF-8, or null if there is no such query - * parameter. - * - * | URL | `queryParameter("a")` | - * | :-------------------------------- | :-------------------- | - * | `http://host/` | null | - * | `http://host/?` | null | - * | `http://host/?a=apple&k=key+lime` | `"apple"` | - * | `http://host/?a=apple&a=apricot` | `"apple"` | - * | `http://host/?a=apple&b` | `"apple"` | - */ - fun queryParameter(name: String): String? { - if (queryNamesAndValues == null) return null - for (i in 0 until queryNamesAndValues.size step 2) { - if (name == queryNamesAndValues[i]) { - return queryNamesAndValues[i + 1] - } - } - return null - } + actual fun queryParameter(name: String): String? = commonQueryParameter(name) - /** - * The distinct query parameter names in this URL, like `["a", "b"]` for - * `http://host/?a=apple&b=banana`. If this URL has no query this is the empty set. - * - * | URL | `queryParameterNames()` | - * | :-------------------------------- | :---------------------- | - * | `http://host/` | `[]` | - * | `http://host/?` | `[""]` | - * | `http://host/?a=apple&k=key+lime` | `["a", "k"]` | - * | `http://host/?a=apple&a=apricot` | `["a"]` | - * | `http://host/?a=apple&b` | `["a", "b"]` | - */ - @get:JvmName("queryParameterNames") val queryParameterNames: Set - get() { - if (queryNamesAndValues == null) return emptySet() - val result = LinkedHashSet() - for (i in 0 until queryNamesAndValues.size step 2) { - result.add(queryNamesAndValues[i]!!) - } - return Collections.unmodifiableSet(result) - } + actual @get:JvmName("queryParameterNames") val queryParameterNames: Set + get() = commonQueryParameterNames - /** - * Returns all values for the query parameter `name` ordered by their appearance in this - * URL. For example this returns `["banana"]` for `queryParameterValue("b")` on - * `http://host/?a=apple&b=banana`. - * - * | URL | `queryParameterValues("a")` | `queryParameterValues("b")` | - * | :-------------------------------- | :-------------------------- | :-------------------------- | - * | `http://host/` | `[]` | `[]` | - * | `http://host/?` | `[]` | `[]` | - * | `http://host/?a=apple&k=key+lime` | `["apple"]` | `[]` | - * | `http://host/?a=apple&a=apricot` | `["apple", "apricot"]` | `[]` | - * | `http://host/?a=apple&b` | `["apple"]` | `[null]` | - */ - fun queryParameterValues(name: String): List { - if (queryNamesAndValues == null) return emptyList() - val result = mutableListOf() - for (i in 0 until queryNamesAndValues.size step 2) { - if (name == queryNamesAndValues[i]) { - result.add(queryNamesAndValues[i + 1]) - } - } - return Collections.unmodifiableList(result) - } + actual fun queryParameterValues(name: String): List = commonQueryParameterValues(name) - /** - * Returns the name of the query parameter at `index`. For example this returns `"a"` - * for `queryParameterName(0)` on `http://host/?a=apple&b=banana`. This throws if - * `index` is not less than the [query size][querySize]. - * - * | URL | `queryParameterName(0)` | `queryParameterName(1)` | - * | :-------------------------------- | :---------------------- | :---------------------- | - * | `http://host/` | exception | exception | - * | `http://host/?` | `""` | exception | - * | `http://host/?a=apple&k=key+lime` | `"a"` | `"k"` | - * | `http://host/?a=apple&a=apricot` | `"a"` | `"a"` | - * | `http://host/?a=apple&b` | `"a"` | `"b"` | - */ - fun queryParameterName(index: Int): String { - if (queryNamesAndValues == null) throw IndexOutOfBoundsException() - return queryNamesAndValues[index * 2]!! - } + actual fun queryParameterName(index: Int): String = commonQueryParameterName(index) - /** - * Returns the value of the query parameter at `index`. For example this returns `"apple"` for - * `queryParameterName(0)` on `http://host/?a=apple&b=banana`. This throws if `index` is not less - * than the [query size][querySize]. - * - * | URL | `queryParameterValue(0)` | `queryParameterValue(1)` | - * | :-------------------------------- | :----------------------- | :----------------------- | - * | `http://host/` | exception | exception | - * | `http://host/?` | null | exception | - * | `http://host/?a=apple&k=key+lime` | `"apple"` | `"key lime"` | - * | `http://host/?a=apple&a=apricot` | `"apple"` | `"apricot"` | - * | `http://host/?a=apple&b` | `"apple"` | null | - */ - fun queryParameterValue(index: Int): String? { - if (queryNamesAndValues == null) throw IndexOutOfBoundsException() - return queryNamesAndValues[index * 2 + 1] - } + actual fun queryParameterValue(index: Int): String? = commonQueryParameterValue(index) - /** - * This URL's encoded fragment, like `"abc"` for `http://host/#abc`. This is null if the URL has - * no fragment. - * - * | URL | `encodedFragment()` | - * | :--------------------- | :------------------ | - * | `http://host/` | null | - * | `http://host/#` | `""` | - * | `http://host/#abc` | `"abc"` | - * | `http://host/#abc|def` | `"abc|def"` | - */ - @get:JvmName("encodedFragment") val encodedFragment: String? - get() { - if (fragment == null) return null - val fragmentStart = url.indexOf('#') + 1 - return url.substring(fragmentStart) - } + @get:JvmName("encodedFragment") + actual val encodedFragment: String? + get() = commonEncodedFragment - /** - * Returns a string with containing this URL with its username, password, query, and fragment - * stripped, and its path replaced with `/...`. For example, redacting - * `http://username:password@example.com/path` returns `http://example.com/...`. - */ - fun redact(): String { - return newBuilder("/...")!! - .username("") - .password("") - .build() - .toString() - } + actual fun redact(): String = commonRedact() - /** - * Returns the URL that would be retrieved by following `link` from this URL, or null if the - * resulting URL is not well-formed. - */ - fun resolve(link: String): HttpUrl? = newBuilder(link)?.build() + actual fun resolve(link: String): HttpUrl? = commonResolve(link) - /** - * Returns a builder based on this URL. - */ - fun newBuilder(): Builder { - val result = Builder() - result.scheme = scheme - result.encodedUsername = encodedUsername - result.encodedPassword = encodedPassword - result.host = host - // If we're set to a default port, unset it in case of a scheme change. - result.port = if (port != defaultPort(scheme)) port else -1 - result.encodedPathSegments.clear() - result.encodedPathSegments.addAll(encodedPathSegments) - result.encodedQuery(encodedQuery) - result.encodedFragment = encodedFragment - return result - } + actual fun newBuilder(): Builder = commonNewBuilder() - /** - * Returns a builder for the URL that would be retrieved by following `link` from this URL, - * or null if the resulting URL is not well-formed. - */ - fun newBuilder(link: String): Builder? { - return try { - Builder().parse(this, link) - } catch (_: IllegalArgumentException) { - null - } - } + actual fun newBuilder(link: String): Builder? = commonNewBuilder(link) - override fun equals(other: Any?): Boolean { - return other is HttpUrl && other.url == url - } + override fun equals(other: Any?): Boolean = commonEquals(other) - override fun hashCode(): Int = url.hashCode() + override fun hashCode(): Int = commonHashCode() - override fun toString(): String = url + override fun toString(): String = commonToString() /** * Returns the domain name of this URL's [host] that is one level beneath the public suffix by @@ -905,233 +600,83 @@ class HttpUrl internal constructor( level = DeprecationLevel.ERROR) fun fragment(): String? = fragment - class Builder { - internal var scheme: String? = null - internal var encodedUsername = "" - internal var encodedPassword = "" - internal var host: String? = null - internal var port = -1 - internal val encodedPathSegments = mutableListOf() - internal var encodedQueryNamesAndValues: MutableList? = null - internal var encodedFragment: String? = null - - init { - encodedPathSegments.add("") // The default path is '/' which needs a trailing space. - } + actual class Builder { + internal actual var scheme: String? = null + internal actual var encodedUsername = "" + internal actual var encodedPassword = "" + internal actual var host: String? = null + internal actual var port = -1 + internal actual val encodedPathSegments = mutableListOf("") + internal actual var encodedQueryNamesAndValues: MutableList? = null + internal actual var encodedFragment: String? = null /** * @param scheme either "http" or "https". */ - fun scheme(scheme: String) = apply { - when { - scheme.equals("http", ignoreCase = true) -> this.scheme = "http" - scheme.equals("https", ignoreCase = true) -> this.scheme = "https" - else -> throw IllegalArgumentException("unexpected scheme: $scheme") - } - } + actual fun scheme(scheme: String) = commonScheme(scheme) - fun username(username: String) = apply { - this.encodedUsername = username.canonicalize(encodeSet = USERNAME_ENCODE_SET) - } + actual fun username(username: String) = commonUsername(username) - fun encodedUsername(encodedUsername: String) = apply { - this.encodedUsername = encodedUsername.canonicalize( - encodeSet = USERNAME_ENCODE_SET, - alreadyEncoded = true - ) - } + actual fun encodedUsername(encodedUsername: String) = commonEncodedUsername(encodedUsername) - fun password(password: String) = apply { - this.encodedPassword = password.canonicalize(encodeSet = PASSWORD_ENCODE_SET) - } + actual fun password(password: String) = commonPassword(password) - fun encodedPassword(encodedPassword: String) = apply { - this.encodedPassword = encodedPassword.canonicalize( - encodeSet = PASSWORD_ENCODE_SET, - alreadyEncoded = true - ) - } + actual fun encodedPassword(encodedPassword: String) = commonEncodedPassword(encodedPassword) /** * @param host either a regular hostname, International Domain Name, IPv4 address, or IPv6 * address. */ - fun host(host: String) = apply { - val encoded = host.percentDecode().toCanonicalHost() ?: throw IllegalArgumentException( - "unexpected host: $host") - this.host = encoded - } + actual fun host(host: String) = commonHost(host) - fun port(port: Int) = apply { - require(port in 1..65535) { "unexpected port: $port" } - this.port = port - } + actual fun port(port: Int) = commonPort(port) - private fun effectivePort(): Int { - return if (port != -1) port else defaultPort(scheme!!) - } - - fun addPathSegment(pathSegment: String) = apply { - push(pathSegment, 0, pathSegment.length, addTrailingSlash = false, alreadyEncoded = false) - } + actual fun addPathSegment(pathSegment: String) = commonAddPathSegment(pathSegment) /** * Adds a set of path segments separated by a slash (either `\` or `/`). If `pathSegments` * starts with a slash, the resulting URL will have empty path segment. */ - fun addPathSegments(pathSegments: String): Builder = addPathSegments(pathSegments, false) + actual fun addPathSegments(pathSegments: String): Builder = commonAddPathSegments(pathSegments) - fun addEncodedPathSegment(encodedPathSegment: String) = apply { - push(encodedPathSegment, 0, encodedPathSegment.length, addTrailingSlash = false, - alreadyEncoded = true) - } + actual fun addEncodedPathSegment(encodedPathSegment: String) = commonAddEncodedPathSegment(encodedPathSegment) /** * Adds a set of encoded path segments separated by a slash (either `\` or `/`). If * `encodedPathSegments` starts with a slash, the resulting URL will have empty path segment. */ - fun addEncodedPathSegments(encodedPathSegments: String): Builder = - addPathSegments(encodedPathSegments, true) + actual fun addEncodedPathSegments(encodedPathSegments: String): Builder = commonAddEncodedPathSegments(encodedPathSegments) - private fun addPathSegments(pathSegments: String, alreadyEncoded: Boolean) = apply { - var offset = 0 - do { - val segmentEnd = pathSegments.delimiterOffset("/\\", offset, pathSegments.length) - val addTrailingSlash = segmentEnd < pathSegments.length - push(pathSegments, offset, segmentEnd, addTrailingSlash, alreadyEncoded) - offset = segmentEnd + 1 - } while (offset <= pathSegments.length) - } - fun setPathSegment(index: Int, pathSegment: String) = apply { - val canonicalPathSegment = pathSegment.canonicalize(encodeSet = PATH_SEGMENT_ENCODE_SET) - require(!isDot(canonicalPathSegment) && !isDotDot(canonicalPathSegment)) { - "unexpected path segment: $pathSegment" - } - encodedPathSegments[index] = canonicalPathSegment - } + actual fun setPathSegment(index: Int, pathSegment: String) = commonSetPathSegment(index, pathSegment) - fun setEncodedPathSegment(index: Int, encodedPathSegment: String) = apply { - val canonicalPathSegment = encodedPathSegment.canonicalize( - encodeSet = PATH_SEGMENT_ENCODE_SET, - alreadyEncoded = true - ) - encodedPathSegments[index] = canonicalPathSegment - require(!isDot(canonicalPathSegment) && !isDotDot(canonicalPathSegment)) { - "unexpected path segment: $encodedPathSegment" - } - } + actual fun setEncodedPathSegment(index: Int, encodedPathSegment: String) = commonSetEncodedPathSegment(index, encodedPathSegment) - fun removePathSegment(index: Int) = apply { - encodedPathSegments.removeAt(index) - if (encodedPathSegments.isEmpty()) { - encodedPathSegments.add("") // Always leave at least one '/'. - } - } + actual fun removePathSegment(index: Int) = commonRemovePathSegment(index) - fun encodedPath(encodedPath: String) = apply { - require(encodedPath.startsWith("/")) { "unexpected encodedPath: $encodedPath" } - resolvePath(encodedPath, 0, encodedPath.length) - } + actual fun encodedPath(encodedPath: String) = commonEncodedPath(encodedPath) - fun query(query: String?) = apply { - this.encodedQueryNamesAndValues = query?.canonicalize( - encodeSet = QUERY_ENCODE_SET, - plusIsSpace = true - )?.toQueryNamesAndValues() - } + actual fun query(query: String?) = commonQuery(query) - fun encodedQuery(encodedQuery: String?) = apply { - this.encodedQueryNamesAndValues = encodedQuery?.canonicalize( - encodeSet = QUERY_ENCODE_SET, - alreadyEncoded = true, - plusIsSpace = true - )?.toQueryNamesAndValues() - } + actual fun encodedQuery(encodedQuery: String?) = commonEncodedQuery(encodedQuery) /** Encodes the query parameter using UTF-8 and adds it to this URL's query string. */ - fun addQueryParameter(name: String, value: String?) = apply { - if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = mutableListOf() - encodedQueryNamesAndValues!!.add(name.canonicalize( - encodeSet = QUERY_COMPONENT_ENCODE_SET, - plusIsSpace = true - )) - encodedQueryNamesAndValues!!.add(value?.canonicalize( - encodeSet = QUERY_COMPONENT_ENCODE_SET, - plusIsSpace = true - )) - } + actual fun addQueryParameter(name: String, value: String?) = commonAddQueryParameter(name, value) /** Adds the pre-encoded query parameter to this URL's query string. */ - fun addEncodedQueryParameter(encodedName: String, encodedValue: String?) = apply { - if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = mutableListOf() - encodedQueryNamesAndValues!!.add(encodedName.canonicalize( - encodeSet = QUERY_COMPONENT_REENCODE_SET, - alreadyEncoded = true, - plusIsSpace = true - )) - encodedQueryNamesAndValues!!.add(encodedValue?.canonicalize( - encodeSet = QUERY_COMPONENT_REENCODE_SET, - alreadyEncoded = true, - plusIsSpace = true - )) - } + actual fun addEncodedQueryParameter(encodedName: String, encodedValue: String?) = commonAddEncodedQueryParameter(encodedName, encodedValue) - fun setQueryParameter(name: String, value: String?) = apply { - removeAllQueryParameters(name) - addQueryParameter(name, value) - } + actual fun setQueryParameter(name: String, value: String?) = commonSetQueryParameter(name, value) - fun setEncodedQueryParameter(encodedName: String, encodedValue: String?) = apply { - removeAllEncodedQueryParameters(encodedName) - addEncodedQueryParameter(encodedName, encodedValue) - } + actual fun setEncodedQueryParameter(encodedName: String, encodedValue: String?) = commonSetEncodedQueryParameter(encodedName, encodedValue) - fun removeAllQueryParameters(name: String) = apply { - if (encodedQueryNamesAndValues == null) return this - val nameToRemove = name.canonicalize( - encodeSet = QUERY_COMPONENT_ENCODE_SET, - plusIsSpace = true - ) - removeAllCanonicalQueryParameters(nameToRemove) - } + actual fun removeAllQueryParameters(name: String) = commonRemoveAllQueryParameters(name) - fun removeAllEncodedQueryParameters(encodedName: String) = apply { - if (encodedQueryNamesAndValues == null) return this - removeAllCanonicalQueryParameters(encodedName.canonicalize( - encodeSet = QUERY_COMPONENT_REENCODE_SET, - alreadyEncoded = true, - plusIsSpace = true - )) - } + actual fun removeAllEncodedQueryParameters(encodedName: String) = commonRemoveAllEncodedQueryParameters(encodedName) - private fun removeAllCanonicalQueryParameters(canonicalName: String) { - for (i in encodedQueryNamesAndValues!!.size - 2 downTo 0 step 2) { - if (canonicalName == encodedQueryNamesAndValues!![i]) { - encodedQueryNamesAndValues!!.removeAt(i + 1) - encodedQueryNamesAndValues!!.removeAt(i) - if (encodedQueryNamesAndValues!!.isEmpty()) { - encodedQueryNamesAndValues = null - return - } - } - } - } + actual fun fragment(fragment: String?) = commonFragment(fragment) - fun fragment(fragment: String?) = apply { - this.encodedFragment = fragment?.canonicalize( - encodeSet = FRAGMENT_ENCODE_SET, - unicodeAllowed = true - ) - } - - fun encodedFragment(encodedFragment: String?) = apply { - this.encodedFragment = encodedFragment?.canonicalize( - encodeSet = FRAGMENT_ENCODE_SET, - alreadyEncoded = true, - unicodeAllowed = true - ) - } + actual fun encodedFragment(encodedFragment: String?) = commonEncodedFragment(encodedFragment) /** * Re-encodes the components of this URL so that it satisfies (obsolete) RFC 2396, which is @@ -1168,482 +713,23 @@ class HttpUrl internal constructor( ) } - fun build(): HttpUrl { - @Suppress("UNCHECKED_CAST") // percentDecode returns either List or List. - return HttpUrl( - scheme = scheme ?: throw IllegalStateException("scheme == null"), - username = encodedUsername.percentDecode(), - password = encodedPassword.percentDecode(), - host = host ?: throw IllegalStateException("host == null"), - port = effectivePort(), - pathSegments = encodedPathSegments.map { it.percentDecode() }, - queryNamesAndValues = encodedQueryNamesAndValues?.map { it?.percentDecode(plusIsSpace = true) }, - fragment = encodedFragment?.percentDecode(), - url = toString() - ) - } + actual fun build(): HttpUrl = commonBuild() - override fun toString(): String { - return buildString { - if (scheme != null) { - append(scheme) - append("://") - } else { - append("//") - } + override fun toString(): String = commonToString() - if (encodedUsername.isNotEmpty() || encodedPassword.isNotEmpty()) { - append(encodedUsername) - if (encodedPassword.isNotEmpty()) { - append(':') - append(encodedPassword) - } - append('@') - } - - if (host != null) { - if (':' in host!!) { - // Host is an IPv6 address. - append('[') - append(host) - append(']') - } else { - append(host) - } - } - - if (port != -1 || scheme != null) { - val effectivePort = effectivePort() - if (scheme == null || effectivePort != defaultPort(scheme!!)) { - append(':') - append(effectivePort) - } - } - - encodedPathSegments.toPathString(this) - - if (encodedQueryNamesAndValues != null) { - append('?') - encodedQueryNamesAndValues!!.toQueryString(this) - } - - if (encodedFragment != null) { - append('#') - append(encodedFragment) - } - } - } - - internal fun parse(base: HttpUrl?, input: String): Builder { - var pos = input.indexOfFirstNonAsciiWhitespace() - val limit = input.indexOfLastNonAsciiWhitespace(pos) - - // Scheme. - val schemeDelimiterOffset = schemeDelimiterOffset(input, pos, limit) - if (schemeDelimiterOffset != -1) { - when { - input.startsWith("https:", ignoreCase = true, startIndex = pos) -> { - this.scheme = "https" - pos += "https:".length - } - input.startsWith("http:", ignoreCase = true, startIndex = pos) -> { - this.scheme = "http" - pos += "http:".length - } - else -> throw IllegalArgumentException("Expected URL scheme 'http' or 'https' but was '" + - input.substring(0, schemeDelimiterOffset) + "'") - } - } else if (base != null) { - this.scheme = base.scheme - } else { - val truncated = if (input.length > 6) input.take(6) + "..." else input - throw IllegalArgumentException( - "Expected URL scheme 'http' or 'https' but no scheme was found for $truncated") - } - - // Authority. - var hasUsername = false - var hasPassword = false - val slashCount = input.slashCount(pos, limit) - if (slashCount >= 2 || base == null || base.scheme != this.scheme) { - // Read an authority if either: - // * The input starts with 2 or more slashes. These follow the scheme if it exists. - // * The input scheme exists and is different from the base URL's scheme. - // - // The structure of an authority is: - // username:password@host:port - // - // Username, password and port are optional. - // [username[:password]@]host[:port] - pos += slashCount - authority@ while (true) { - val componentDelimiterOffset = input.delimiterOffset("@/\\?#", pos, limit) - val c = if (componentDelimiterOffset != limit) { - input[componentDelimiterOffset].code - } else { - -1 - } - when (c) { - '@'.code -> { - // User info precedes. - if (!hasPassword) { - val passwordColonOffset = input.delimiterOffset(':', pos, componentDelimiterOffset) - val canonicalUsername = input.canonicalize( - pos = pos, - limit = passwordColonOffset, - encodeSet = USERNAME_ENCODE_SET, - alreadyEncoded = true - ) - this.encodedUsername = if (hasUsername) { - this.encodedUsername + "%40" + canonicalUsername - } else { - canonicalUsername - } - if (passwordColonOffset != componentDelimiterOffset) { - hasPassword = true - this.encodedPassword = input.canonicalize( - pos = passwordColonOffset + 1, - limit = componentDelimiterOffset, - encodeSet = PASSWORD_ENCODE_SET, - alreadyEncoded = true - ) - } - hasUsername = true - } else { - this.encodedPassword = this.encodedPassword + "%40" + input.canonicalize( - pos = pos, - limit = componentDelimiterOffset, - encodeSet = PASSWORD_ENCODE_SET, - alreadyEncoded = true - ) - } - pos = componentDelimiterOffset + 1 - } - - -1, '/'.code, '\\'.code, '?'.code, '#'.code -> { - // Host info precedes. - val portColonOffset = portColonOffset(input, pos, componentDelimiterOffset) - if (portColonOffset + 1 < componentDelimiterOffset) { - host = input.percentDecode(pos = pos, limit = portColonOffset).toCanonicalHost() - port = parsePort(input, portColonOffset + 1, componentDelimiterOffset) - require(port != -1) { - "Invalid URL port: \"${input.substring(portColonOffset + 1, - componentDelimiterOffset)}\"" - } - } else { - host = input.percentDecode(pos = pos, limit = portColonOffset).toCanonicalHost() - port = defaultPort(scheme!!) - } - require(host != null) { - "$INVALID_HOST: \"${input.substring(pos, portColonOffset)}\"" - } - pos = componentDelimiterOffset - break@authority - } - } - } - } else { - // This is a relative link. Copy over all authority components. Also maybe the path & query. - this.encodedUsername = base.encodedUsername - this.encodedPassword = base.encodedPassword - this.host = base.host - this.port = base.port - this.encodedPathSegments.clear() - this.encodedPathSegments.addAll(base.encodedPathSegments) - if (pos == limit || input[pos] == '#') { - encodedQuery(base.encodedQuery) - } - } - - // Resolve the relative path. - val pathDelimiterOffset = input.delimiterOffset("?#", pos, limit) - resolvePath(input, pos, pathDelimiterOffset) - pos = pathDelimiterOffset - - // Query. - if (pos < limit && input[pos] == '?') { - val queryDelimiterOffset = input.delimiterOffset('#', pos, limit) - this.encodedQueryNamesAndValues = input.canonicalize( - pos = pos + 1, - limit = queryDelimiterOffset, - encodeSet = QUERY_ENCODE_SET, - alreadyEncoded = true, - plusIsSpace = true - ).toQueryNamesAndValues() - pos = queryDelimiterOffset - } - - // Fragment. - if (pos < limit && input[pos] == '#') { - this.encodedFragment = input.canonicalize( - pos = pos + 1, - limit = limit, - encodeSet = FRAGMENT_ENCODE_SET, - alreadyEncoded = true, - unicodeAllowed = true - ) - } - - return this - } - - private fun resolvePath(input: String, startPos: Int, limit: Int) { - var pos = startPos - // Read a delimiter. - if (pos == limit) { - // Empty path: keep the base path as-is. - return - } - val c = input[pos] - if (c == '/' || c == '\\') { - // Absolute path: reset to the default "/". - encodedPathSegments.clear() - encodedPathSegments.add("") - pos++ - } else { - // Relative path: clear everything after the last '/'. - encodedPathSegments[encodedPathSegments.size - 1] = "" - } - - // Read path segments. - var i = pos - while (i < limit) { - val pathSegmentDelimiterOffset = input.delimiterOffset("/\\", i, limit) - val segmentHasTrailingSlash = pathSegmentDelimiterOffset < limit - push(input, i, pathSegmentDelimiterOffset, segmentHasTrailingSlash, true) - i = pathSegmentDelimiterOffset - if (segmentHasTrailingSlash) i++ - } - } - - /** Adds a path segment. If the input is ".." or equivalent, this pops a path segment. */ - private fun push( - input: String, - pos: Int, - limit: Int, - addTrailingSlash: Boolean, - alreadyEncoded: Boolean - ) { - val segment = input.canonicalize( - pos = pos, - limit = limit, - encodeSet = PATH_SEGMENT_ENCODE_SET, - alreadyEncoded = alreadyEncoded - ) - if (isDot(segment)) { - return // Skip '.' path segments. - } - if (isDotDot(segment)) { - pop() - return - } - if (encodedPathSegments[encodedPathSegments.size - 1].isEmpty()) { - encodedPathSegments[encodedPathSegments.size - 1] = segment - } else { - encodedPathSegments.add(segment) - } - if (addTrailingSlash) { - encodedPathSegments.add("") - } - } - - private fun isDot(input: String): Boolean { - return input == "." || input.equals("%2e", ignoreCase = true) - } - - private fun isDotDot(input: String): Boolean { - return input == ".." || - input.equals("%2e.", ignoreCase = true) || - input.equals(".%2e", ignoreCase = true) || - input.equals("%2e%2e", ignoreCase = true) - } - - /** - * Removes a path segment. When this method returns the last segment is always "", which means - * the encoded path will have a trailing '/'. - * - * Popping "/a/b/c/" yields "/a/b/". In this case the list of path segments goes from ["a", - * "b", "c", ""] to ["a", "b", ""]. - * - * Popping "/a/b/c" also yields "/a/b/". The list of path segments goes from ["a", "b", "c"] - * to ["a", "b", ""]. - */ - private fun pop() { - val removed = encodedPathSegments.removeAt(encodedPathSegments.size - 1) - - // Make sure the path ends with a '/' by either adding an empty string or clearing a segment. - if (removed.isEmpty() && encodedPathSegments.isNotEmpty()) { - encodedPathSegments[encodedPathSegments.size - 1] = "" - } else { - encodedPathSegments.add("") - } - } - - companion object { - internal const val INVALID_HOST = "Invalid URL host" - - /** - * Returns the index of the ':' in `input` that is after scheme characters. Returns -1 if - * `input` does not have a scheme that starts at `pos`. - */ - private fun schemeDelimiterOffset(input: String, pos: Int, limit: Int): Int { - if (limit - pos < 2) return -1 - - val c0 = input[pos] - if ((c0 < 'a' || c0 > 'z') && (c0 < 'A' || c0 > 'Z')) return -1 // Not a scheme start char. - - characters@ for (i in pos + 1 until limit) { - return when (input[i]) { - // Scheme character. Keep going. - in 'a'..'z', in 'A'..'Z', in '0'..'9', '+', '-', '.' -> continue@characters - - // Scheme prefix! - ':' -> i - - // Non-scheme character before the first ':'. - else -> -1 - } - } - - return -1 // No ':'; doesn't start with a scheme. - } - - /** Returns the number of '/' and '\' slashes in this, starting at `pos`. */ - private fun String.slashCount(pos: Int, limit: Int): Int { - var slashCount = 0 - for (i in pos until limit) { - val c = this[i] - if (c == '\\' || c == '/') { - slashCount++ - } else { - break - } - } - return slashCount - } - - /** Finds the first ':' in `input`, skipping characters between square braces "[...]". */ - private fun portColonOffset(input: String, pos: Int, limit: Int): Int { - var i = pos - while (i < limit) { - when (input[i]) { - '[' -> { - while (++i < limit) { - if (input[i] == ']') break - } - } - ':' -> return i - } - i++ - } - return limit // No colon. - } - - private fun parsePort(input: String, pos: Int, limit: Int): Int { - return try { - // Canonicalize the port string to skip '\n' etc. - val portString = input.canonicalize(pos = pos, limit = limit, encodeSet = "") - val i = portString.toInt() - if (i in 1..65535) i else -1 - } catch (_: NumberFormatException) { - -1 // Invalid port. - } - } - } + internal actual fun parse(base: HttpUrl?, input: String): Builder = commonParse(base, input) } - companion object { - private val HEX_DIGITS = - charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') - internal const val USERNAME_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#" - internal const val PASSWORD_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#" - internal const val PATH_SEGMENT_ENCODE_SET = " \"<>^`{}|/\\?#" - internal const val PATH_SEGMENT_ENCODE_SET_URI = "[]" - internal const val QUERY_ENCODE_SET = " \"'<>#" - internal const val QUERY_COMPONENT_REENCODE_SET = " \"'<>#&=" - internal const val QUERY_COMPONENT_ENCODE_SET = " !\"#$&'(),/:;<=>?@[]\\^`{|}~" - internal const val QUERY_COMPONENT_ENCODE_SET_URI = "\\^`{|}" - internal const val FORM_ENCODE_SET = " !\"#$&'()+,/:;<=>?@[\\]^`{|}~" - internal const val FRAGMENT_ENCODE_SET = "" - internal const val FRAGMENT_ENCODE_SET_URI = " \"#<>\\^`{|}" - - /** Returns 80 if `scheme.equals("http")`, 443 if `scheme.equals("https")` and -1 otherwise. */ + actual companion object { @JvmStatic - fun defaultPort(scheme: String): Int { - return when (scheme) { - "http" -> 80 - "https" -> 443 - else -> -1 - } - } + actual fun defaultPort(scheme: String): Int = commonDefaultPort(scheme) - /** Returns a path string for this list of path segments. */ - internal fun List.toPathString(out: StringBuilder) { - for (i in 0 until size) { - out.append('/') - out.append(this[i]) - } - } - - /** Returns a string for this list of query names and values. */ - internal fun List.toQueryString(out: StringBuilder) { - for (i in 0 until size step 2) { - val name = this[i] - val value = this[i + 1] - if (i > 0) out.append('&') - out.append(name) - if (value != null) { - out.append('=') - out.append(value) - } - } - } - - /** - * Cuts this string up into alternating parameter names and values. This divides a query string - * like `subject=math&easy&problem=5-2=3` into the list `["subject", "math", "easy", null, - * "problem", "5-2=3"]`. Note that values may be null and may contain '=' characters. - */ - internal fun String.toQueryNamesAndValues(): MutableList { - val result = mutableListOf() - var pos = 0 - while (pos <= length) { - var ampersandOffset = indexOf('&', pos) - if (ampersandOffset == -1) ampersandOffset = length - - val equalsOffset = indexOf('=', pos) - if (equalsOffset == -1 || equalsOffset > ampersandOffset) { - result.add(substring(pos, ampersandOffset)) - result.add(null) // No value for this name. - } else { - result.add(substring(pos, equalsOffset)) - result.add(substring(equalsOffset + 1, ampersandOffset)) - } - pos = ampersandOffset + 1 - } - return result - } - - /** - * Returns a new [HttpUrl] representing this. - * - * @throws IllegalArgumentException If this is not a well-formed HTTP or HTTPS URL. - */ @JvmStatic - @JvmName("get") fun String.toHttpUrl(): HttpUrl = Builder().parse(null, this).build() + @JvmName("get") actual fun String.toHttpUrl(): HttpUrl = commonToHttpUrl() - /** - * Returns a new `HttpUrl` representing `url` if it is a well-formed HTTP or HTTPS URL, or null - * if it isn't. - */ @JvmStatic - @JvmName("parse") fun String.toHttpUrlOrNull(): HttpUrl? { - return try { - toHttpUrl() - } catch (_: IllegalArgumentException) { - null - } - } + @JvmName("parse") + actual fun String.toHttpUrlOrNull(): HttpUrl? = commonToHttpUrlOrNull() /** * Returns an [HttpUrl] for this if its protocol is `http` or `https`, or null if it has any @@ -1690,181 +776,5 @@ class HttpUrl internal constructor( imports = ["okhttp3.HttpUrl.Companion.toHttpUrlOrNull"]), level = DeprecationLevel.ERROR) fun get(uri: URI): HttpUrl? = uri.toHttpUrlOrNull() - - internal fun String.percentDecode( - pos: Int = 0, - limit: Int = length, - plusIsSpace: Boolean = false - ): String { - for (i in pos until limit) { - val c = this[i] - if (c == '%' || c == '+' && plusIsSpace) { - // Slow path: the character at i requires decoding! - val out = Buffer() - out.writeUtf8(this, pos, i) - out.writePercentDecoded(this, pos = i, limit = limit, plusIsSpace = plusIsSpace) - return out.readUtf8() - } - } - - // Fast path: no characters in [pos..limit) required decoding. - return substring(pos, limit) - } - - private fun Buffer.writePercentDecoded( - encoded: String, - pos: Int, - limit: Int, - plusIsSpace: Boolean - ) { - var codePoint: Int - var i = pos - while (i < limit) { - codePoint = encoded.codePointAt(i) - if (codePoint == '%'.code && i + 2 < limit) { - val d1 = encoded[i + 1].parseHexDigit() - val d2 = encoded[i + 2].parseHexDigit() - if (d1 != -1 && d2 != -1) { - writeByte((d1 shl 4) + d2) - i += 2 - i += Character.charCount(codePoint) - continue - } - } else if (codePoint == '+'.code && plusIsSpace) { - writeByte(' '.code) - i++ - continue - } - writeUtf8CodePoint(codePoint) - i += Character.charCount(codePoint) - } - } - - private fun String.isPercentEncoded(pos: Int, limit: Int): Boolean { - return pos + 2 < limit && - this[pos] == '%' && - this[pos + 1].parseHexDigit() != -1 && - this[pos + 2].parseHexDigit() != -1 - } - - /** - * Returns a substring of `input` on the range `[pos..limit)` with the following - * transformations: - * - * * Tabs, newlines, form feeds and carriage returns are skipped. - * - * * In queries, ' ' is encoded to '+' and '+' is encoded to "%2B". - * - * * Characters in `encodeSet` are percent-encoded. - * - * * Control characters and non-ASCII characters are percent-encoded. - * - * * All other characters are copied without transformation. - * - * @param alreadyEncoded true to leave '%' as-is; false to convert it to '%25'. - * @param strict true to encode '%' if it is not the prefix of a valid percent encoding. - * @param plusIsSpace true to encode '+' as "%2B" if it is not already encoded. - * @param unicodeAllowed true to leave non-ASCII codepoint unencoded. - * @param charset which charset to use, null equals UTF-8. - */ - internal fun String.canonicalize( - pos: Int = 0, - limit: Int = length, - encodeSet: String, - alreadyEncoded: Boolean = false, - strict: Boolean = false, - plusIsSpace: Boolean = false, - unicodeAllowed: Boolean = false, - charset: Charset? = null - ): String { - var codePoint: Int - var i = pos - while (i < limit) { - codePoint = codePointAt(i) - if (codePoint < 0x20 || - codePoint == 0x7f || - codePoint >= 0x80 && !unicodeAllowed || - codePoint.toChar() in encodeSet || - codePoint == '%'.code && - (!alreadyEncoded || strict && !isPercentEncoded(i, limit)) || - codePoint == '+'.code && plusIsSpace) { - // Slow path: the character at i requires encoding! - val out = Buffer() - out.writeUtf8(this, pos, i) - out.writeCanonicalized( - input = this, - pos = i, - limit = limit, - encodeSet = encodeSet, - alreadyEncoded = alreadyEncoded, - strict = strict, - plusIsSpace = plusIsSpace, - unicodeAllowed = unicodeAllowed, - charset = charset - ) - return out.readUtf8() - } - i += Character.charCount(codePoint) - } - - // Fast path: no characters in [pos..limit) required encoding. - return substring(pos, limit) - } - - private fun Buffer.writeCanonicalized( - input: String, - pos: Int, - limit: Int, - encodeSet: String, - alreadyEncoded: Boolean, - strict: Boolean, - plusIsSpace: Boolean, - unicodeAllowed: Boolean, - charset: Charset? - ) { - var encodedCharBuffer: Buffer? = null // Lazily allocated. - var codePoint: Int - var i = pos - while (i < limit) { - codePoint = input.codePointAt(i) - if (alreadyEncoded && (codePoint == '\t'.code || codePoint == '\n'.code || - codePoint == '\u000c'.code || codePoint == '\r'.code)) { - // Skip this character. - } else if (codePoint == ' '.code && encodeSet === FORM_ENCODE_SET) { - // Encode ' ' as '+'. - writeUtf8("+") - } else if (codePoint == '+'.code && plusIsSpace) { - // Encode '+' as '%2B' since we permit ' ' to be encoded as either '+' or '%20'. - writeUtf8(if (alreadyEncoded) "+" else "%2B") - } else if (codePoint < 0x20 || - codePoint == 0x7f || - codePoint >= 0x80 && !unicodeAllowed || - codePoint.toChar() in encodeSet || - codePoint == '%'.code && - (!alreadyEncoded || strict && !input.isPercentEncoded(i, limit))) { - // Percent encode this character. - if (encodedCharBuffer == null) { - encodedCharBuffer = Buffer() - } - - if (charset == null || charset == UTF_8) { - encodedCharBuffer.writeUtf8CodePoint(codePoint) - } else { - encodedCharBuffer.writeString(input, i, i + Character.charCount(codePoint), charset) - } - - while (!encodedCharBuffer.exhausted()) { - val b = encodedCharBuffer.readByte().toInt() and 0xff - writeByte('%'.code) - writeByte(HEX_DIGITS[b shr 4 and 0xf].code) - writeByte(HEX_DIGITS[b and 0xf].code) - } - } else { - // This character doesn't need encoding. Just copy it over. - writeUtf8CodePoint(codePoint) - } - i += Character.charCount(codePoint) - } - } } } diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/Request.kt b/okhttp/src/jvmMain/kotlin/okhttp3/Request.kt index 034cc0ee5..081fb6a30 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/Request.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/Request.kt @@ -201,7 +201,7 @@ actual class Request internal actual constructor(builder: Builder) { this.headers = request.headers.newBuilder() } - open fun url(url: HttpUrl): Builder = apply { + actual open fun url(url: HttpUrl): Builder = apply { this.url = url } diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/internal/-HostnamesJvm.kt b/okhttp/src/jvmMain/kotlin/okhttp3/internal/-HostnamesJvm.kt index eedcd2058..44ba8aa7e 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/internal/-HostnamesJvm.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/internal/-HostnamesJvm.kt @@ -27,7 +27,7 @@ import java.util.Locale * `null` will be returned if the host cannot be ToASCII encoded or if the result contains * unsupported ASCII characters. */ -fun String.toCanonicalHost(): String? { +actual fun String.toCanonicalHost(): String? { val host: String = this // If the input contains a :, it’s an IPv6 address. diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/internal/-HttpUrlJvm.kt b/okhttp/src/jvmMain/kotlin/okhttp3/internal/-HttpUrlJvm.kt new file mode 100644 index 000000000..adc5d7e86 --- /dev/null +++ b/okhttp/src/jvmMain/kotlin/okhttp3/internal/-HttpUrlJvm.kt @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2022 Square, Inc. + * + * 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 okhttp3.internal + +import java.nio.charset.Charset +import okhttp3.internal.CommonHttpUrl.FORM_ENCODE_SET +import okhttp3.internal.CommonHttpUrl.HEX_DIGITS +import okhttp3.internal.CommonHttpUrl.isPercentEncoded +import okhttp3.internal.JvmHttpUrl.canonicalizeWithCharset +import okio.Buffer + +internal object JvmHttpUrl { + + internal fun Buffer.writeCanonicalized( + input: String, + pos: Int, + limit: Int, + encodeSet: String, + alreadyEncoded: Boolean, + strict: Boolean, + plusIsSpace: Boolean, + unicodeAllowed: Boolean, + charset: Charset? + ) { + var encodedCharBuffer: Buffer? = null // Lazily allocated. + var codePoint: Int + var i = pos + while (i < limit) { + codePoint = input.codePointAt(i) + if (alreadyEncoded && (codePoint == '\t'.code || codePoint == '\n'.code || + codePoint == '\u000c'.code || codePoint == '\r'.code)) { + // Skip this character. + } else if (codePoint == ' '.code && encodeSet === FORM_ENCODE_SET) { + // Encode ' ' as '+'. + writeUtf8("+") + } else if (codePoint == '+'.code && plusIsSpace) { + // Encode '+' as '%2B' since we permit ' ' to be encoded as either '+' or '%20'. + writeUtf8(if (alreadyEncoded) "+" else "%2B") + } else if (codePoint < 0x20 || + codePoint == 0x7f || + codePoint >= 0x80 && !unicodeAllowed || + codePoint.toChar() in encodeSet || + codePoint == '%'.code && + (!alreadyEncoded || strict && !input.isPercentEncoded(i, limit))) { + // Percent encode this character. + if (encodedCharBuffer == null) { + encodedCharBuffer = Buffer() + } + + if (charset == null || charset == Charsets.UTF_8) { + encodedCharBuffer.writeUtf8CodePoint(codePoint) + } else { + encodedCharBuffer.writeString(input, i, i + Character.charCount(codePoint), charset) + } + + while (!encodedCharBuffer.exhausted()) { + val b = encodedCharBuffer.readByte().toInt() and 0xff + writeByte('%'.code) + writeByte(HEX_DIGITS[b shr 4 and 0xf].code) + writeByte(HEX_DIGITS[b and 0xf].code) + } + } else { + // This character doesn't need encoding. Just copy it over. + writeUtf8CodePoint(codePoint) + } + i += Character.charCount(codePoint) + } + } + + /** + * Returns a substring of `input` on the range `[pos..limit)` with the following + * transformations: + * + * * Tabs, newlines, form feeds and carriage returns are skipped. + * + * * In queries, ' ' is encoded to '+' and '+' is encoded to "%2B". + * + * * Characters in `encodeSet` are percent-encoded. + * + * * Control characters and non-ASCII characters are percent-encoded. + * + * * All other characters are copied without transformation. + * + * @param alreadyEncoded true to leave '%' as-is; false to convert it to '%25'. + * @param strict true to encode '%' if it is not the prefix of a valid percent encoding. + * @param plusIsSpace true to encode '+' as "%2B" if it is not already encoded. + * @param unicodeAllowed true to leave non-ASCII codepoint unencoded. + * @param charset which charset to use, null equals UTF-8. + */ + internal fun String.canonicalizeWithCharset( + pos: Int = 0, + limit: Int = length, + encodeSet: String, + alreadyEncoded: Boolean = false, + strict: Boolean = false, + plusIsSpace: Boolean = false, + unicodeAllowed: Boolean = false, + charset: Charset? = null + ): String { + var codePoint: Int + var i = pos + while (i < limit) { + codePoint = codePointAt(i) + if (codePoint < 0x20 || + codePoint == 0x7f || + codePoint >= 0x80 && !unicodeAllowed || + codePoint.toChar() in encodeSet || + codePoint == '%'.code && + (!alreadyEncoded || strict && !isPercentEncoded(i, limit)) || + codePoint == '+'.code && plusIsSpace) { + // Slow path: the character at i requires encoding! + val out = Buffer() + out.writeUtf8(this, pos, i) + out.writeCanonicalized( + input = this, + pos = i, + limit = limit, + encodeSet = encodeSet, + alreadyEncoded = alreadyEncoded, + strict = strict, + plusIsSpace = plusIsSpace, + unicodeAllowed = unicodeAllowed, + charset = charset + ) + return out.readUtf8() + } + i += Character.charCount(codePoint) + } + + // Fast path: no characters in [pos..limit) required encoding. + return substring(pos, limit) + } +} + +internal actual object HttpUrlCommon { + internal actual fun Buffer.writePercentDecoded( + encoded: String, + pos: Int, + limit: Int, + plusIsSpace: Boolean + ) { + var codePoint: Int + var i = pos + while (i < limit) { + codePoint = encoded.codePointAt(i) + if (codePoint == '%'.code && i + 2 < limit) { + val d1 = encoded[i + 1].parseHexDigit() + val d2 = encoded[i + 2].parseHexDigit() + if (d1 != -1 && d2 != -1) { + writeByte((d1 shl 4) + d2) + i += 2 + i += Character.charCount(codePoint) + continue + } + } else if (codePoint == '+'.code && plusIsSpace) { + writeByte(' '.code) + i++ + continue + } + writeUtf8CodePoint(codePoint) + i += Character.charCount(codePoint) + } + } + internal actual fun String.canonicalize( + pos: Int, + limit: Int, + encodeSet: String, + alreadyEncoded: Boolean, + strict: Boolean, + plusIsSpace: Boolean, + unicodeAllowed: Boolean, + ): String { + return canonicalizeWithCharset( + pos = pos, + limit = limit, + encodeSet = encodeSet, + alreadyEncoded = alreadyEncoded, + strict = strict, + plusIsSpace = plusIsSpace, + unicodeAllowed = unicodeAllowed + ) + } + +} diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/internal/-UtilJvm.kt b/okhttp/src/jvmMain/kotlin/okhttp3/internal/-UtilJvm.kt index 5362de662..873450c54 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/internal/-UtilJvm.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/internal/-UtilJvm.kt @@ -39,6 +39,7 @@ import okhttp3.OkHttpClient import okhttp3.RequestBody import okhttp3.Response import okhttp3.ResponseBody +import okhttp3.internal.CommonHttpUrl.commonDefaultPort import okhttp3.internal.http2.Header import okio.Buffer import okio.BufferedSource @@ -51,8 +52,6 @@ val EMPTY_REQUEST: RequestBody = commonEmptyRequestBody @JvmField val EMPTY_RESPONSE: ResponseBody = commonEmptyResponse -actual typealias HttpUrlRepresentation = HttpUrl - /** GMT and UTC are equivalent for our purposes. */ @JvmField internal val UTC: TimeZone = TimeZone.getTimeZone("GMT")!! @@ -72,7 +71,7 @@ internal fun HttpUrl.toHostHeader(includeDefaultPort: Boolean = false): String { } else { host } - return if (includeDefaultPort || port != HttpUrl.defaultPort(scheme)) { + return if (includeDefaultPort || port != commonDefaultPort(scheme)) { "$host:$port" } else { host diff --git a/okhttp/src/jvmTest/java/okhttp3/HttpUrlTest.kt b/okhttp/src/jvmTest/java/okhttp3/HttpUrlTest.kt index 3b678f16c..14b73699b 100644 --- a/okhttp/src/jvmTest/java/okhttp3/HttpUrlTest.kt +++ b/okhttp/src/jvmTest/java/okhttp3/HttpUrlTest.kt @@ -125,58 +125,6 @@ open class HttpUrlTest { assertThat(parse("http://h/\u3000").encodedPath).isEqualTo("/%E3%80%80") } - @Test - fun scheme() { - assertThat(parse("http://host/")).isEqualTo(parse("http://host/")) - assertThat(parse("Http://host/")).isEqualTo(parse("http://host/")) - assertThat(parse("http://host/")).isEqualTo(parse("http://host/")) - assertThat(parse("HTTP://host/")).isEqualTo(parse("http://host/")) - assertThat(parse("https://host/")).isEqualTo(parse("https://host/")) - assertThat(parse("HTTPS://host/")).isEqualTo(parse("https://host/")) - assertInvalid( - "image640://480.png", - "Expected URL scheme 'http' or 'https' but was 'image640'" - ) - assertInvalid("httpp://host/", "Expected URL scheme 'http' or 'https' but was 'httpp'") - assertInvalid( - "0ttp://host/", - "Expected URL scheme 'http' or 'https' but no scheme was found for 0ttp:/..." - ) - assertInvalid("ht+tp://host/", "Expected URL scheme 'http' or 'https' but was 'ht+tp'") - assertInvalid("ht.tp://host/", "Expected URL scheme 'http' or 'https' but was 'ht.tp'") - assertInvalid("ht-tp://host/", "Expected URL scheme 'http' or 'https' but was 'ht-tp'") - assertInvalid("ht1tp://host/", "Expected URL scheme 'http' or 'https' but was 'ht1tp'") - assertInvalid("httpss://host/", "Expected URL scheme 'http' or 'https' but was 'httpss'") - } - - @Test - fun parseNoScheme() { - assertInvalid( - "//host", - "Expected URL scheme 'http' or 'https' but no scheme was found for //host" - ) - assertInvalid( - "://host", - "Expected URL scheme 'http' or 'https' but no scheme was found for ://hos..." - ) - assertInvalid( - "/path", - "Expected URL scheme 'http' or 'https' but no scheme was found for /path" - ) - assertInvalid( - "path", - "Expected URL scheme 'http' or 'https' but no scheme was found for path" - ) - assertInvalid( - "?query", - "Expected URL scheme 'http' or 'https' but no scheme was found for ?query" - ) - assertInvalid( - "#fragment", - "Expected URL scheme 'http' or 'https' but no scheme was found for #fragm..." - ) - } - @Test fun newBuilderResolve() { // Non-exhaustive tests because implementation is the same as resolve. @@ -914,18 +862,6 @@ open class HttpUrlTest { .isEqualTo("http://[::1]/") } - @Test - fun hostIpv4CanonicalForm() { - assertThat(parse("http://255.255.255.255/").host).isEqualTo("255.255.255.255") - assertThat(parse("http://1.2.3.4/").host).isEqualTo("1.2.3.4") - assertThat(parse("http://0.0.0.0/").host).isEqualTo("0.0.0.0") - } - - @Test - fun hostWithTrailingDot() { - assertThat(parse("http://host./").host).isEqualTo("host.") - } - /** * Strip unexpected characters when converting to URI (which is more strict). * https://github.com/square/okhttp/issues/5667 @@ -945,19 +881,6 @@ open class HttpUrlTest { assertThat(httpUrl.toUri().toString()).isEqualTo("http://\$tracker/") } - @Test - fun port() { - assertThat(parse("http://host:80/")).isEqualTo(parse("http://host/")) - assertThat(parse("http://host:99/")).isEqualTo(parse("http://host:99/")) - assertThat(parse("http://host:/")).isEqualTo(parse("http://host/")) - assertThat(parse("http://host:65535/").port).isEqualTo(65535) - assertInvalid("http://host:0/", "Invalid URL port: \"0\"") - assertInvalid("http://host:65536/", "Invalid URL port: \"65536\"") - assertInvalid("http://host:-1/", "Invalid URL port: \"-1\"") - assertInvalid("http://host:a/", "Invalid URL port: \"a\"") - assertInvalid("http://host:%39%39/", "Invalid URL port: \"%39%39\"") - } - @Test fun pathCharacters() { UrlComponentEncodingTester.newInstance() @@ -1224,44 +1147,6 @@ open class HttpUrlTest { .isEqualTo("//host.com:8080/path") } - @Test - fun minimalUrlComposition() { - val url = HttpUrl.Builder().scheme("http").host("host").build() - assertThat(url.toString()).isEqualTo("http://host/") - assertThat(url.scheme).isEqualTo("http") - assertThat(url.username).isEqualTo("") - assertThat(url.password).isEqualTo("") - assertThat(url.host).isEqualTo("host") - assertThat(url.port).isEqualTo(80) - assertThat(url.encodedPath).isEqualTo("/") - assertThat(url.query).isNull() - assertThat(url.fragment).isNull() - } - - @Test - fun fullUrlComposition() { - val url = HttpUrl.Builder() - .scheme("http") - .username("username") - .password("password") - .host("host") - .port(8080) - .addPathSegment("path") - .query("query") - .fragment("fragment") - .build() - assertThat(url.toString()) - .isEqualTo("http://username:password@host:8080/path?query#fragment") - assertThat(url.scheme).isEqualTo("http") - assertThat(url.username).isEqualTo("username") - assertThat(url.password).isEqualTo("password") - assertThat(url.host).isEqualTo("host") - assertThat(url.port).isEqualTo(8080) - assertThat(url.encodedPath).isEqualTo("/path") - assertThat(url.query).isEqualTo("query") - assertThat(url.fragment).isEqualTo("fragment") - } - @Test fun changingSchemeChangesDefaultPort() { assertThat( @@ -1422,12 +1307,6 @@ open class HttpUrlTest { .isEqualTo("/a/b/c/") } - @Test - fun pathSize() { - assertThat(parse("http://host/").pathSize).isEqualTo(1) - assertThat(parse("http://host/a/b/c").pathSize).isEqualTo(3) - } - @Test fun addPathSegments() { val base = parse("http://host/a/b/c") diff --git a/okhttp/src/nonJvmMain/kotlin/okhttp3/HttpUrl.kt b/okhttp/src/nonJvmMain/kotlin/okhttp3/HttpUrl.kt new file mode 100644 index 000000000..6c8f464ac --- /dev/null +++ b/okhttp/src/nonJvmMain/kotlin/okhttp3/HttpUrl.kt @@ -0,0 +1,479 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * 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 okhttp3 + +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.internal.CommonHttpUrl +import okhttp3.internal.CommonHttpUrl.commonAddEncodedPathSegment +import okhttp3.internal.CommonHttpUrl.commonAddEncodedPathSegments +import okhttp3.internal.CommonHttpUrl.commonAddEncodedQueryParameter +import okhttp3.internal.CommonHttpUrl.commonAddPathSegment +import okhttp3.internal.CommonHttpUrl.commonAddPathSegments +import okhttp3.internal.CommonHttpUrl.commonAddQueryParameter +import okhttp3.internal.CommonHttpUrl.commonBuild +import okhttp3.internal.CommonHttpUrl.commonEncodedFragment +import okhttp3.internal.CommonHttpUrl.commonEncodedPassword +import okhttp3.internal.CommonHttpUrl.commonEncodedPath +import okhttp3.internal.CommonHttpUrl.commonEncodedPathSegments +import okhttp3.internal.CommonHttpUrl.commonEncodedQuery +import okhttp3.internal.CommonHttpUrl.commonEncodedUsername +import okhttp3.internal.CommonHttpUrl.commonEquals +import okhttp3.internal.CommonHttpUrl.commonFragment +import okhttp3.internal.CommonHttpUrl.commonHashCode +import okhttp3.internal.CommonHttpUrl.commonHost +import okhttp3.internal.CommonHttpUrl.commonIsHttps +import okhttp3.internal.CommonHttpUrl.commonNewBuilder +import okhttp3.internal.CommonHttpUrl.commonParse +import okhttp3.internal.CommonHttpUrl.commonPassword +import okhttp3.internal.CommonHttpUrl.commonPathSize +import okhttp3.internal.CommonHttpUrl.commonPort +import okhttp3.internal.CommonHttpUrl.commonQuery +import okhttp3.internal.CommonHttpUrl.commonQueryParameter +import okhttp3.internal.CommonHttpUrl.commonQueryParameterName +import okhttp3.internal.CommonHttpUrl.commonQueryParameterNames +import okhttp3.internal.CommonHttpUrl.commonQueryParameterValue +import okhttp3.internal.CommonHttpUrl.commonQueryParameterValues +import okhttp3.internal.CommonHttpUrl.commonQuerySize +import okhttp3.internal.CommonHttpUrl.commonRedact +import okhttp3.internal.CommonHttpUrl.commonRemoveAllEncodedQueryParameters +import okhttp3.internal.CommonHttpUrl.commonRemoveAllQueryParameters +import okhttp3.internal.CommonHttpUrl.commonRemovePathSegment +import okhttp3.internal.CommonHttpUrl.commonResolve +import okhttp3.internal.CommonHttpUrl.commonScheme +import okhttp3.internal.CommonHttpUrl.commonSetEncodedPathSegment +import okhttp3.internal.CommonHttpUrl.commonSetEncodedQueryParameter +import okhttp3.internal.CommonHttpUrl.commonSetPathSegment +import okhttp3.internal.CommonHttpUrl.commonSetQueryParameter +import okhttp3.internal.CommonHttpUrl.commonToHttpUrl +import okhttp3.internal.CommonHttpUrl.commonToHttpUrlOrNull +import okhttp3.internal.CommonHttpUrl.commonToString +import okhttp3.internal.CommonHttpUrl.commonUsername + +/** + * A uniform resource locator (URL) with a scheme of either `http` or `https`. Use this class to + * compose and decompose Internet addresses. For example, this code will compose and print a URL for + * Google search: + * + * ```java + * HttpUrl url = new HttpUrl.Builder() + * .scheme("https") + * .host("www.google.com") + * .addPathSegment("search") + * .addQueryParameter("q", "polar bears") + * .build(); + * System.out.println(url); + * ``` + * + * which prints: + * + * ``` + * https://www.google.com/search?q=polar%20bears + * ``` + * + * As another example, this code prints the human-readable query parameters of a Twitter search: + * + * ```java + * HttpUrl url = HttpUrl.parse("https://twitter.com/search?q=cute%20%23puppies&f=images"); + * for (int i = 0, size = url.querySize(); i < size; i++) { + * System.out.println(url.queryParameterName(i) + ": " + url.queryParameterValue(i)); + * } + * ``` + * + * which prints: + * + * ``` + * q: cute #puppies + * f: images + * ``` + * + * In addition to composing URLs from their component parts and decomposing URLs into their + * component parts, this class implements relative URL resolution: what address you'd reach by + * clicking a relative link on a specified page. For example: + * + * ```java + * HttpUrl base = HttpUrl.parse("https://www.youtube.com/user/WatchTheDaily/videos"); + * HttpUrl link = base.resolve("../../watch?v=cbP2N1BQdYc"); + * System.out.println(link); + * ``` + * + * which prints: + * + * ``` + * https://www.youtube.com/watch?v=cbP2N1BQdYc + * ``` + * + * ## What's in a URL? + * + * A URL has several components. + * + * ### Scheme + * + * Sometimes referred to as *protocol*, A URL's scheme describes what mechanism should be used to + * retrieve the resource. Although URLs have many schemes (`mailto`, `file`, `ftp`), this class only + * supports `http` and `https`. Use [java.net.URI][URI] for URLs with arbitrary schemes. + * + * ### Username and Password + * + * Username and password are either present, or the empty string `""` if absent. This class offers + * no mechanism to differentiate empty from absent. Neither of these components are popular in + * practice. Typically HTTP applications use other mechanisms for user identification and + * authentication. + * + * ### Host + * + * The host identifies the webserver that serves the URL's resource. It is either a hostname like + * `square.com` or `localhost`, an IPv4 address like `192.168.0.1`, or an IPv6 address like `::1`. + * + * Usually a webserver is reachable with multiple identifiers: its IP addresses, registered + * domain names, and even `localhost` when connecting from the server itself. Each of a web server's + * names is a distinct URL and they are not interchangeable. For example, even if + * `http://square.github.io/dagger` and `http://google.github.io/dagger` are served by the same IP + * address, the two URLs identify different resources. + * + * ### Port + * + * The port used to connect to the web server. By default this is 80 for HTTP and 443 for HTTPS. + * This class never returns -1 for the port: if no port is explicitly specified in the URL then the + * scheme's default is used. + * + * ### Path + * + * The path identifies a specific resource on the host. Paths have a hierarchical structure like + * "/square/okhttp/issues/1486" and decompose into a list of segments like `["square", "okhttp", + * "issues", "1486"]`. + * + * This class offers methods to compose and decompose paths by segment. It composes each path + * from a list of segments by alternating between "/" and the encoded segment. For example the + * segments `["a", "b"]` build "/a/b" and the segments `["a", "b", ""]` build "/a/b/". + * + * If a path's last segment is the empty string then the path ends with "/". This class always + * builds non-empty paths: if the path is omitted it defaults to "/". The default path's segment + * list is a single empty string: `[""]`. + * + * ### Query + * + * The query is optional: it can be null, empty, or non-empty. For many HTTP URLs the query string + * is subdivided into a collection of name-value parameters. This class offers methods to set the + * query as the single string, or as individual name-value parameters. With name-value parameters + * the values are optional and names may be repeated. + * + * ### Fragment + * + * The fragment is optional: it can be null, empty, or non-empty. Unlike host, port, path, and + * query the fragment is not sent to the webserver: it's private to the client. + * + * ## Encoding + * + * Each component must be encoded before it is embedded in the complete URL. As we saw above, the + * string `cute #puppies` is encoded as `cute%20%23puppies` when used as a query parameter value. + * + * ### Percent encoding + * + * Percent encoding replaces a character (like `\ud83c\udf69`) with its UTF-8 hex bytes (like + * `%F0%9F%8D%A9`). This approach works for whitespace characters, control characters, non-ASCII + * characters, and characters that already have another meaning in a particular context. + * + * Percent encoding is used in every URL component except for the hostname. But the set of + * characters that need to be encoded is different for each component. For example, the path + * component must escape all of its `?` characters, otherwise it could be interpreted as the + * start of the URL's query. But within the query and fragment components, the `?` character + * doesn't delimit anything and doesn't need to be escaped. + * + * ```java + * HttpUrl url = HttpUrl.parse("http://who-let-the-dogs.out").newBuilder() + * .addPathSegment("_Who?_") + * .query("_Who?_") + * .fragment("_Who?_") + * .build(); + * System.out.println(url); + * ``` + * + * This prints: + * + * ``` + * http://who-let-the-dogs.out/_Who%3F_?_Who?_#_Who?_ + * ``` + * + * When parsing URLs that lack percent encoding where it is required, this class will percent encode + * the offending characters. + * + * ### IDNA Mapping and Punycode encoding + * + * Hostnames have different requirements and use a different encoding scheme. It consists of IDNA + * mapping and Punycode encoding. + * + * In order to avoid confusion and discourage phishing attacks, [IDNA Mapping][idna] transforms + * names to avoid confusing characters. This includes basic case folding: transforming shouting + * `SQUARE.COM` into cool and casual `square.com`. It also handles more exotic characters. For + * example, the Unicode trademark sign (™) could be confused for the letters "TM" in + * `http://ho™ail.com`. To mitigate this, the single character (™) maps to the string (tm). There + * is similar policy for all of the 1.1 million Unicode code points. Note that some code points such + * as "\ud83c\udf69" are not mapped and cannot be used in a hostname. + * + * [Punycode](http://ietf.org/rfc/rfc3492.txt) converts a Unicode string to an ASCII string to make + * international domain names work everywhere. For example, "σ" encodes as "xn--4xa". The encoded + * string is not human readable, but can be used with classes like [InetAddress] to establish + * connections. + * + * ## Why another URL model? + * + * Java includes both [java.net.URL][URL] and [java.net.URI][URI]. We offer a new URL + * model to address problems that the others don't. + * + * ### Different URLs should be different + * + * Although they have different content, `java.net.URL` considers the following two URLs + * equal, and the [equals()][Object.equals] method between them returns true: + * + * * https://example.net/ + * + * * https://example.com/ + * + * This is because those two hosts share the same IP address. This is an old, bad design decision + * that makes `java.net.URL` unusable for many things. It shouldn't be used as a [Map] key or in a + * [Set]. Doing so is both inefficient because equality may require a DNS lookup, and incorrect + * because unequal URLs may be equal because of how they are hosted. + * + * ### Equal URLs should be equal + * + * These two URLs are semantically identical, but `java.net.URI` disagrees: + * + * * http://host:80/ + * + * * http://host + * + * Both the unnecessary port specification (`:80`) and the absent trailing slash (`/`) cause URI to + * bucket the two URLs separately. This harms URI's usefulness in collections. Any application that + * stores information-per-URL will need to either canonicalize manually, or suffer unnecessary + * redundancy for such URLs. + * + * Because they don't attempt canonical form, these classes are surprisingly difficult to use + * securely. Suppose you're building a webservice that checks that incoming paths are prefixed + * "/static/images/" before serving the corresponding assets from the filesystem. + * + * ```java + * String attack = "http://example.com/static/images/../../../../../etc/passwd"; + * System.out.println(new URL(attack).getPath()); + * System.out.println(new URI(attack).getPath()); + * System.out.println(HttpUrl.parse(attack).encodedPath()); + * ``` + * + * By canonicalizing the input paths, they are complicit in directory traversal attacks. Code that + * checks only the path prefix may suffer! + * + * ``` + * /static/images/../../../../../etc/passwd + * /static/images/../../../../../etc/passwd + * /etc/passwd + * ``` + * + * ### If it works on the web, it should work in your application + * + * The `java.net.URI` class is strict around what URLs it accepts. It rejects URLs like + * `http://example.com/abc|def` because the `|` character is unsupported. This class is more + * forgiving: it will automatically percent-encode the `|'` yielding `http://example.com/abc%7Cdef`. + * This kind behavior is consistent with web browsers. `HttpUrl` prefers consistency with major web + * browsers over consistency with obsolete specifications. + * + * ### Paths and Queries should decompose + * + * Neither of the built-in URL models offer direct access to path segments or query parameters. + * Manually using `StringBuilder` to assemble these components is cumbersome: do '+' characters get + * silently replaced with spaces? If a query parameter contains a '&', does that get escaped? + * By offering methods to read and write individual query parameters directly, application + * developers are saved from the hassles of encoding and decoding. + * + * ### Plus a modern API + * + * The URL (JDK1.0) and URI (Java 1.4) classes predate builders and instead use telescoping + * constructors. For example, there's no API to compose a URI with a custom port without also + * providing a query and fragment. + * + * Instances of [HttpUrl] are well-formed and always have a scheme, host, and path. With + * `java.net.URL` it's possible to create an awkward URL like `http:/` with scheme and path but no + * hostname. Building APIs that consume such malformed values is difficult! + * + * This class has a modern API. It avoids punitive checked exceptions: [toHttpUrl] throws + * [IllegalArgumentException] on invalid input or [toHttpUrlOrNull] returns null if the input is an + * invalid URL. You can even be explicit about whether each component has been encoded already. + * + * [idna]: http://www.unicode.org/reports/tr46/#ToASCII + */ +actual class HttpUrl internal actual constructor( + actual val scheme: String, + actual val username: String, + actual val password: String, + actual val host: String, + actual val port: Int, + actual val pathSegments: List, + internal actual val queryNamesAndValues: List?, + actual val fragment: String?, + internal actual val url: String +) { + + actual val isHttps: Boolean + get() = commonIsHttps + + actual val encodedUsername: String + get() = commonEncodedUsername + + actual val encodedPassword: String + get() = commonEncodedPassword + + actual val pathSize: Int + get() = commonPathSize + + actual val encodedPath: String + get() = commonEncodedPath + + actual val encodedPathSegments: List + get() = commonEncodedPathSegments + + actual val encodedQuery: String? + get() = commonEncodedQuery + + actual val query: String? + get() = commonQuery + + actual val querySize: Int + get() = commonQuerySize + + actual fun queryParameter(name: String): String? = commonQueryParameter(name) + + actual val queryParameterNames: Set + get() = commonQueryParameterNames + + actual fun queryParameterValues(name: String): List = commonQueryParameterValues(name) + + actual fun queryParameterName(index: Int): String = commonQueryParameterName(index) + + actual fun queryParameterValue(index: Int): String? = commonQueryParameterValue(index) + + actual val encodedFragment: String? + get() = commonEncodedFragment + + actual fun redact(): String = commonRedact() + + actual fun resolve(link: String): HttpUrl? = commonResolve(link) + + actual fun newBuilder(): HttpUrl.Builder = commonNewBuilder() + + actual fun newBuilder(link: String): Builder? = commonNewBuilder(link) + + override fun equals(other: Any?): Boolean = commonEquals(other) + + override fun hashCode(): Int = commonHashCode() + + override fun toString(): String = commonToString() + + actual companion object { + actual fun String.toHttpUrl(): HttpUrl = commonToHttpUrl() + + actual fun String.toHttpUrlOrNull(): HttpUrl? = commonToHttpUrlOrNull() + + actual fun defaultPort(scheme: String): Int = CommonHttpUrl.commonDefaultPort(scheme) + } + + actual class Builder { + internal actual var scheme: String? = null + internal actual var encodedUsername = "" + internal actual var encodedPassword = "" + internal actual var host: String? = null + internal actual var port = -1 + internal actual val encodedPathSegments = mutableListOf("") + internal actual var encodedQueryNamesAndValues: MutableList? = null + internal actual var encodedFragment: String? = null + + /** + * @param scheme either "http" or "https". + */ + actual fun scheme(scheme: String) = commonScheme(scheme) + + actual fun username(username: String) = commonUsername(username) + + actual fun encodedUsername(encodedUsername: String) = commonEncodedUsername(encodedUsername) + + actual fun password(password: String) = commonPassword(password) + + actual fun encodedPassword(encodedPassword: String) = commonEncodedPassword(encodedPassword) + + /** + * @param host either a regular hostname, International Domain Name, IPv4 address, or IPv6 + * address. + */ + actual fun host(host: String) = commonHost(host) + + actual fun port(port: Int) = commonPort(port) + + actual fun addPathSegment(pathSegment: String) = commonAddPathSegment(pathSegment) + + /** + * Adds a set of path segments separated by a slash (either `\` or `/`). If `pathSegments` + * starts with a slash, the resulting URL will have empty path segment. + */ + actual fun addPathSegments(pathSegments: String): Builder = commonAddPathSegments(pathSegments) + + actual fun addEncodedPathSegment(encodedPathSegment: String) = commonAddEncodedPathSegment(encodedPathSegment) + + /** + * Adds a set of encoded path segments separated by a slash (either `\` or `/`). If + * `encodedPathSegments` starts with a slash, the resulting URL will have empty path segment. + */ + actual fun addEncodedPathSegments(encodedPathSegments: String): Builder = + commonAddEncodedPathSegments(encodedPathSegments) + + + actual fun setPathSegment(index: Int, pathSegment: String) = commonSetPathSegment(index, pathSegment) + + actual fun setEncodedPathSegment(index: Int, encodedPathSegment: String) = + commonSetEncodedPathSegment(index, encodedPathSegment) + + actual fun removePathSegment(index: Int) = commonRemovePathSegment(index) + + actual fun encodedPath(encodedPath: String) = commonEncodedPath(encodedPath) + + actual fun query(query: String?) = commonQuery(query) + + actual fun encodedQuery(encodedQuery: String?) = commonEncodedQuery(encodedQuery) + + /** Encodes the query parameter using UTF-8 and adds it to this URL's query string. */ + actual fun addQueryParameter(name: String, value: String?) = commonAddQueryParameter(name, value) + + /** Adds the pre-encoded query parameter to this URL's query string. */ + actual fun addEncodedQueryParameter(encodedName: String, encodedValue: String?) = + commonAddEncodedQueryParameter(encodedName, encodedValue) + + actual fun setQueryParameter(name: String, value: String?) = commonSetQueryParameter(name, value) + + actual fun setEncodedQueryParameter(encodedName: String, encodedValue: String?) = + commonSetEncodedQueryParameter(encodedName, encodedValue) + + actual fun removeAllQueryParameters(name: String) = commonRemoveAllQueryParameters(name) + + actual fun removeAllEncodedQueryParameters(encodedName: String) = commonRemoveAllEncodedQueryParameters(encodedName) + + actual fun fragment(fragment: String?) = commonFragment(fragment) + + actual fun encodedFragment(encodedFragment: String?) = commonEncodedFragment(encodedFragment) + + actual fun build(): HttpUrl = commonBuild() + + override fun toString(): String = commonToString() + + internal actual fun parse(base: HttpUrl?, input: String): Builder = commonParse(base, input) + } +} diff --git a/okhttp/src/nonJvmMain/kotlin/okhttp3/Request.kt b/okhttp/src/nonJvmMain/kotlin/okhttp3/Request.kt index 2258488d3..1dfd8ae1d 100644 --- a/okhttp/src/nonJvmMain/kotlin/okhttp3/Request.kt +++ b/okhttp/src/nonJvmMain/kotlin/okhttp3/Request.kt @@ -16,6 +16,7 @@ package okhttp3 import kotlin.reflect.KClass +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.internal.canonicalUrl import okhttp3.internal.commonAddHeader import okhttp3.internal.commonCacheControl @@ -33,7 +34,7 @@ import okhttp3.internal.commonRemoveHeader import okhttp3.internal.commonTag actual class Request internal actual constructor(builder: Builder) { - actual val url: String = checkNotNull(builder.url) { "url == null" } + actual val url: HttpUrl = checkNotNull(builder.url) { "url == null" } actual val method: String = builder.method actual val headers: Headers = builder.headers.build() actual val body: RequestBody? = builder.body @@ -42,7 +43,7 @@ actual class Request internal actual constructor(builder: Builder) { internal actual var lazyCacheControl: CacheControl? = null actual val isHttps: Boolean - get() = url.startsWith("https://") + get() = url.isHttps /** * Constructs a new request. @@ -108,7 +109,7 @@ actual class Request internal actual constructor(builder: Builder) { } actual open class Builder { - internal actual var url: String? = null + internal actual var url: HttpUrl? = null internal actual var method: String internal actual var headers: Headers.Builder internal actual var body: RequestBody? = null @@ -133,12 +134,12 @@ actual class Request internal actual constructor(builder: Builder) { this.headers = request.headers.newBuilder() } - // open fun url(url: HttpUrl): Builder = apply { - // this.url = url - // } + actual open fun url(url: HttpUrl): Builder = apply { + this.url = url + } actual open fun url(url: String): Builder = apply { - this.url = canonicalUrl(url) + url(canonicalUrl(url).toHttpUrl()) } actual open fun header(name: String, value: String) = commonHeader(name, value) diff --git a/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-HostnamesNonJvm.kt b/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-HostnamesNonJvm.kt new file mode 100644 index 000000000..4dde643bd --- /dev/null +++ b/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-HostnamesNonJvm.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 Square, Inc. + * + * 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 okhttp3.internal + +actual fun String.toCanonicalHost(): String? { + val host: String = this + + // If the input contains a :, it’s an IPv6 address. + if (":" in host) { + // If the input is encased in square braces "[...]", drop 'em. + val inetAddressByteArray = (if (host.startsWith("[") && host.endsWith("]")) { + decodeIpv6(host, 1, host.length - 1) + } else { + decodeIpv6(host, 0, host.length) + }) ?: return null + // TODO implement properly + return inet6AddressToAscii(inetAddressByteArray) + } + + try { + val result = idnToAscii(host) + if (result.isEmpty()) return null + + return if (result.containsInvalidHostnameAsciiCodes()) { + // The IDN ToASCII result contains illegal characters. + null + } else if (result.containsInvalidLabelLengths()) { + // The IDN ToASCII result contains invalid labels. + null + } else { + result + } + } catch (_: IllegalArgumentException) { + return null + } +} + +internal fun inet4AddressToAscii(address: ByteArray): String { + return address.joinToString(".") +} + +fun idnToAscii(host: String): String { + // TODO implement properly + return host.lowercase() +} diff --git a/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-HttpUrlNonJvm.kt b/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-HttpUrlNonJvm.kt new file mode 100644 index 000000000..510dcf56b --- /dev/null +++ b/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-HttpUrlNonJvm.kt @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2022 Square, Inc. + * + * 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 okhttp3.internal + +import okhttp3.internal.CommonHttpUrl.isPercentEncoded +import okhttp3.internal.NonJvmHttpUrl.writeCanonicalized +import okio.Buffer + +internal object NonJvmHttpUrl { + internal fun Buffer.writeCanonicalized( + input: String, + pos: Int, + limit: Int, + encodeSet: String, + alreadyEncoded: Boolean, + strict: Boolean, + plusIsSpace: Boolean, + unicodeAllowed: Boolean, + ) { + var encodedCharBuffer: Buffer? = null // Lazily allocated. + var codePoint: Int + var i = pos + while (i < limit) { + codePoint = input[i].code + if (alreadyEncoded && (codePoint == '\t'.code || codePoint == '\n'.code || + codePoint == '\u000c'.code || codePoint == '\r'.code)) { + // Skip this character. + } else if (codePoint == ' '.code && encodeSet === CommonHttpUrl.FORM_ENCODE_SET) { + // Encode ' ' as '+'. + writeUtf8("+") + } else if (codePoint == '+'.code && plusIsSpace) { + // Encode '+' as '%2B' since we permit ' ' to be encoded as either '+' or '%20'. + writeUtf8(if (alreadyEncoded) "+" else "%2B") + } else if (codePoint < 0x20 || + codePoint == 0x7f || + codePoint >= 0x80 && !unicodeAllowed || + codePoint.toChar() in encodeSet || + codePoint == '%'.code && + (!alreadyEncoded || strict && !input.isPercentEncoded(i, limit))) { + // Percent encode this character. + if (encodedCharBuffer == null) { + encodedCharBuffer = Buffer() + } + + encodedCharBuffer.writeUtf8CodePoint(codePoint) + + while (!encodedCharBuffer.exhausted()) { + val b = encodedCharBuffer.readByte().toInt() and 0xff + writeByte('%'.code) + writeByte(CommonHttpUrl.HEX_DIGITS[b shr 4 and 0xf].code) + writeByte(CommonHttpUrl.HEX_DIGITS[b and 0xf].code) + } + } else { + // This character doesn't need encoding. Just copy it over. + writeUtf8CodePoint(codePoint) + } + i += 1 + } + } +} + +internal actual object HttpUrlCommon { + internal actual fun Buffer.writePercentDecoded( + encoded: String, + pos: Int, + limit: Int, + plusIsSpace: Boolean + ) { + var codePoint: Int + var i = pos + while (i < limit) { + codePoint = encoded.get(i).code + if (codePoint == '%'.code && i + 2 < limit) { + val d1 = encoded[i + 1].parseHexDigit() + val d2 = encoded[i + 2].parseHexDigit() + if (d1 != -1 && d2 != -1) { + writeByte((d1 shl 4) + d2) + i += 2 + i += 1 + continue + } + } else if (codePoint == '+'.code && plusIsSpace) { + writeByte(' '.code) + i++ + continue + } + writeUtf8CodePoint(codePoint) + i += 1 + } + } + + internal actual fun String.canonicalize( + pos: Int, + limit: Int, + encodeSet: String, + alreadyEncoded: Boolean, + strict: Boolean, + plusIsSpace: Boolean, + unicodeAllowed: Boolean, + ): String { + var codePoint: Int + var i = pos + while (i < limit) { + codePoint = this[i].code + if (codePoint < 0x20 || + codePoint == 0x7f || + codePoint >= 0x80 && !unicodeAllowed || + codePoint.toChar() in encodeSet || + codePoint == '%'.code && + (!alreadyEncoded || strict && !isPercentEncoded(i, limit)) || + codePoint == '+'.code && plusIsSpace + ) { + // Slow path: the character at i requires encoding! + val out = Buffer() + out.writeUtf8(this, pos, i) + out.writeCanonicalized( + input = this, + pos = i, + limit = limit, + encodeSet = encodeSet, + alreadyEncoded = alreadyEncoded, + strict = strict, + plusIsSpace = plusIsSpace, + unicodeAllowed = unicodeAllowed, + ) + return out.readUtf8() + } + i += 1 + } + + // Fast path: no characters in [pos..limit) required encoding. + return substring(pos, limit) + } +} + diff --git a/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-UtilNonJvm.kt b/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-UtilNonJvm.kt index 282d4edcf..9ebe9bb04 100644 --- a/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-UtilNonJvm.kt +++ b/okhttp/src/nonJvmMain/kotlin/okhttp3/internal/-UtilNonJvm.kt @@ -15,5 +15,3 @@ */ package okhttp3.internal - -actual typealias HttpUrlRepresentation = String