diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3e088ef --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,66 @@ +name: lsquic.cr CI + +on: + schedule: + - cron: "0 0 * * *" # Every day at 00:00 + push: + branches: + - "master" + - "api-only" + pull_request: + branches: "*" + +jobs: + build: + + runs-on: ubuntu-latest + + name: "build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }}" + + continue-on-error: ${{ !matrix.stable }} + + strategy: + fail-fast: false + matrix: + stable: [true] + crystal: + - 1.0.0 + - 1.1.1 + - 1.2.0 + include: + - crystal: nightly + stable: false + + steps: + - uses: actions/checkout@v2 + + - name: Install Crystal + uses: crystal-lang/install-crystal@v1.5.3 + with: + crystal: ${{ matrix.crystal }} + + - name: Cache Shards + uses: actions/cache@v2 + with: + path: ./lib + key: shards-${{ hashFiles('shard.lock') }} + + - name: Install Shards + run: | + if ! shards check; then + shards install + fi + + - name: Run tests + run: crystal spec --warnings all --error-on-warnings --error-trace + + - name: Run lint + run: | + if ! crystal tool format --check; then + crystal tool format + git diff + exit 1 + fi + + - name: Build + run: crystal build --warnings all --error-on-warnings --error-trace src/lsquic.cr diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml new file mode 100644 index 0000000..63f5936 --- /dev/null +++ b/.github/workflows/container-release.yml @@ -0,0 +1,39 @@ +name: Build and release container + +on: + push: + branches: + - "master" + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + with: + platforms: arm64 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to registry + uses: docker/login-action@v1 + with: + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} + + - name: Build and push Docker image for Push Event + if: github.ref == 'refs/heads/master' + uses: docker/build-push-action@v2 + with: + context: . + file: docker/Dockerfile + platforms: linux/amd64,linux/arm64/v8 + push: true + tags: quay.io/invidious/lsquic-compiled:${{ github.sha }},quay.io/invidious/lsquic-compiled:latest \ No newline at end of file diff --git a/LICENSE.boringssl b/LICENSE.boringssl new file mode 100644 index 0000000..49c41fa --- /dev/null +++ b/LICENSE.boringssl @@ -0,0 +1,251 @@ +BoringSSL is a fork of OpenSSL. As such, large parts of it fall under OpenSSL +licensing. Files that are completely new have a Google copyright and an ISC +license. This license is reproduced at the bottom of this file. + +Contributors to BoringSSL are required to follow the CLA rules for Chromium: +https://cla.developers.google.com/clas + +Files in third_party/ have their own licenses, as described therein. The MIT +license, for third_party/fiat, which, unlike other third_party directories, is +compiled into non-test libraries, is included below. + +The OpenSSL toolkit stays under a dual license, i.e. both the conditions of the +OpenSSL License and the original SSLeay license apply to the toolkit. See below +for the actual license texts. Actually both licenses are BSD-style Open Source +licenses. In case of any license issues related to OpenSSL please contact +openssl-core@openssl.org. + +The following are Google-internal bug numbers where explicit permission from +some authors is recorded for use of their work. (This is purely for our own +record keeping.) + 27287199 + 27287880 + 27287883 + + OpenSSL License + --------------- + +/* ==================================================================== + * Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + + Original SSLeay License + ----------------------- + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + + +ISC license used for completely new code in BoringSSL: + +/* Copyright (c) 2015, Google Inc. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ + + +The code in third_party/fiat carries the MIT license: + +Copyright (c) 2015-2016 the fiat-crypto authors (see +https://github.com/mit-plv/fiat-crypto/blob/master/AUTHORS). + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +Licenses for support code +------------------------- + +Parts of the TLS test suite are under the Go license. This code is not included +in BoringSSL (i.e. libcrypto and libssl) when compiled, however, so +distributing code linked against BoringSSL does not trigger this license: + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +BoringSSL uses the Chromium test infrastructure to run a continuous build, +trybots etc. The scripts which manage this, and the script for generating build +metadata, are under the Chromium license. Distributing code linked against +BoringSSL does not trigger this license. + +Copyright 2015 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE.chrome b/LICENSE.chrome new file mode 100644 index 0000000..9efe9fa --- /dev/null +++ b/LICENSE.chrome @@ -0,0 +1,32 @@ +A few parts of LiteSpeed QUIC library are based on proto-quic. That +code is covered by this additional license: + +------------------------------ + +Copyright 2015 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. +* Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE.lsquic b/LICENSE.lsquic new file mode 100644 index 0000000..93bdf56 --- /dev/null +++ b/LICENSE.lsquic @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - 2019 LiteSpeed Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index cbe2446..c4f01d2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,14 @@ Crystal bindings to the excellent [LSQUIC](https://github.com/litespeedtech/lsquic) library. +Releases track lsquic's versioning starting with `v2.18.1`. + +`liblsquic.a` is licensed under `LICENSE.lsquic` and `LICENSE.chrome`. + +Lsquic uses [boringssl](https://github.com/google/boringssl), which is licensed under `LICENSE.boringssl`. + +This library is available under the MIT license. + ## Installation 1. Add the dependency to your `shard.yml`: @@ -9,14 +17,29 @@ Crystal bindings to the excellent [LSQUIC](https://github.com/litespeedtech/lsqu ```yaml dependencies: lsquic: - github: omarroth/lsquic.cr + github: iv-org/lsquic.cr ``` 2. Run `shards install` +## Usage + +```crystal +require "lsquic" + +client = QUIC::Client.new("www.youtube.com") +client.get("/") # => # + +client.get("/", headers: HTTP::Headers{ + "cookie" => "Some value", + # ... +}) # => # + +``` + ## Contributing -1. Fork it () +1. Fork it () 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) diff --git a/docker/APKBUILD-boringssl b/docker/APKBUILD-boringssl new file mode 100644 index 0000000..a162119 --- /dev/null +++ b/docker/APKBUILD-boringssl @@ -0,0 +1,46 @@ +# Based on https://aur.archlinux.org/packages/boringssl-git/ +# Maintainer: Omar Roth +pkgname=boringssl +pkgver=1.1.0 +pkgrel=0 +pkgdesc="BoringSSL is a fork of OpenSSL that is designed to meet Google's needs" +url="https://boringssl.googlesource.com/boringssl" +arch="all" +license="MIT" +replaces="openssl libressl" +depends="!openssl-libs-static" +makedepends_host="linux-headers" +makedepends="cmake git go perl" +subpackages="$pkgname-static $pkgname-dev $pkgname-doc" +source="251b516.tar.gz::https://github.com/google/boringssl/tarball/251b516" +builddir="$srcdir/google-boringssl-251b516" + +prepare() { + : +} + +build() { + cmake -DCMAKE_BUILD_TYPE=Debug . + make ssl crypto +} + +check() { + make all_tests +} + +package() { + for i in *.md ; do + install -Dm644 $i "$pkgdir/usr/share/doc/$pkgname/$i" + done + install -d "$pkgdir/usr/lib" + install -d "$pkgdir/usr/include" + cp -R include/openssl "$pkgdir/usr/include" + + install -Dm755 crypto/libcrypto.a "$pkgdir/usr/lib/libcrypto.a" + install -Dm755 ssl/libssl.a "$pkgdir/usr/lib/libssl.a" +# install -Dm755 decrepit/libdecrepit.a "$pkgdir/usr/lib/libdecrepit.a" +# install -Dm755 libboringssl_gtest.a "$pkgdir/usr/lib/libboringssl_gtest.a" +} +sha512sums=" +b1d42ed188cf0cce89d40061fa05de85b387ee4244f1236ea488a431536a2c6b657b4f03daed0ac9328c7f5c4c9330499283b8a67f1444dcf9ba5e97e1199c4e 251b516.tar.gz +" diff --git a/docker/APKBUILD-lsquic b/docker/APKBUILD-lsquic new file mode 100644 index 0000000..2e2f982 --- /dev/null +++ b/docker/APKBUILD-lsquic @@ -0,0 +1,43 @@ +# Maintainer: Omar Roth +pkgname=lsquic +pkgver=2.18.1 +pkgrel=0 +pkgdesc="LiteSpeed QUIC and HTTP/3 Library" +url="https://github.com/litespeedtech/lsquic" +arch="all" +license="MIT" +depends="boringssl-dev boringssl-static zlib-static libevent-static" +makedepends="cmake git go perl bsd-compat-headers linux-headers" +subpackages="$pkgname-static" +source="v$pkgver.tar.gz::https://github.com/litespeedtech/lsquic/tarball/v2.18.1 +ls-qpack-$pkgver.tar.gz::https://github.com/litespeedtech/ls-qpack/tarball/a8ae6ef +ls-hpack-$pkgver.tar.gz::https://github.com/litespeedtech/ls-hpack/tarball/bd5d589" +builddir="$srcdir/litespeedtech-$pkgname-692a910" + +prepare() { + cp -r -T "$srcdir/litespeedtech-ls-qpack-a8ae6ef" "$builddir/src/liblsquic/ls-qpack" + cp -r -T "$srcdir/litespeedtech-ls-hpack-bd5d589" "$builddir/src/lshpack" +} + +build() { + cmake \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DBORINGSSL_INCLUDE=/usr/include/openssl \ + -DBORINGSSL_LIB_crypto=/usr/lib \ + -DBORINGSSL_LIB_ssl=/usr/lib . + make lsquic +} + +check() { + make tests +} + +package() { + install -d "$pkgdir/usr/lib" + install -Dm755 src/liblsquic/liblsquic.a "$pkgdir/usr/lib/liblsquic.a" +} +sha512sums=" +d015a72f1e88750ecb364768a40f532678f11ded09c6447a2e698b20f43fa499ef143a53f4c92a5938dfece0e39e687dc9df4aea97c618faee0c63da771561c3 v2.18.1.tar.gz +c5629085a3881815fb0b72a321eeba8de093eff9417b8ac7bde1ee1264971be0dca6d61d74799b02ae03a4c629b2a9cf21387deeb814935339a8a2503ea33fee ls-qpack-2.18.1.tar.gz +1b9f7ce4c82dadfca8154229a415b0335a61761eba698f814d4b94195c708003deb5cb89318a1ab78ac8fa88b141bc9df283fb1c6e40b3ba399660feaae353a0 ls-hpack-2.18.1.tar.gz +" diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..02f269a --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,28 @@ +FROM alpine:edge AS liblsquic-builder +WORKDIR /src + +RUN apk add --no-cache build-base git apk-tools abuild cmake go perl linux-headers + +RUN abuild-keygen -a -n && \ + cp /root/.abuild/-*.rsa.pub /etc/apk/keys/ + +COPY docker/APKBUILD-boringssl boringssl/APKBUILD +RUN cd boringssl && abuild -F -r && cd .. + +RUN apk add --repository /root/packages/src boringssl boringssl-dev boringssl-static + +RUN apk add --no-cache zlib-dev zlib-static libevent-dev libevent-static + +COPY docker/APKBUILD-lsquic lsquic/APKBUILD +RUN cd lsquic && abuild -F -r && cd .. + +RUN apk add --repository /root/packages/src lsquic-static + +RUN mkdir tmp && cd tmp && \ + ar -x /usr/lib/libssl.a && \ + ar -x /usr/lib/libcrypto.a && \ + ar -x /usr/lib/liblsquic.a && \ + ar rc liblsquic.a *.o && \ + ranlib liblsquic.a && \ + cp liblsquic.a /root/liblsquic.a && \ + cd .. && rm -rf tmp \ No newline at end of file diff --git a/shard.yml b/shard.yml index dc4f372..231c9d3 100644 --- a/shard.yml +++ b/shard.yml @@ -1,9 +1,9 @@ name: lsquic -version: 0.1.0 +version: 2.18.1-2 authors: - Omar Roth -crystal: 0.31.1 +crystal: ">= 0.35.0, < 2.0.0" license: MIT diff --git a/spec/lsquic_spec.cr b/spec/lsquic_spec.cr index 04ba804..83e2ea4 100644 --- a/spec/lsquic_spec.cr +++ b/spec/lsquic_spec.cr @@ -1,9 +1,42 @@ require "./spec_helper" -describe Lsquic do - # TODO: Write tests - +describe QUIC do it "works" do - false.should eq(true) + client = QUIC::Client.new("www.youtube.com") + + 5.times do + client.get("/").status_code.should eq(200) + end + + client.close + end + + it "works with fibers" do + ch = Channel(Int32).new + + 5.times do + spawn do + client = QUIC::Client.new("www.youtube.com") + + 5.times do + ch.send client.get("/").status_code + end + + client.close + end + end + + (5 * 5).times do + ch.receive.should eq(200) + end + end + + it "restarts engine after closing" do + client = QUIC::Client.new("www.youtube.com") + + client.get("/").status_code.should eq(200) + client.close + Fiber.yield + client.get("/").status_code.should eq(200) end end diff --git a/src/lsquic.cr b/src/lsquic.cr index 50cec43..8de68de 100644 --- a/src/lsquic.cr +++ b/src/lsquic.cr @@ -1,7 +1,5 @@ require "./lsquic/*" -require "socket" module QUIC - VERSION = "0.1.0" - QUIC_VERSION = "#{LibLsquic::MAJOR_VERSION}.#{LibLsquic::MINOR_VERSION}.#{LibLsquic::PATCH_VERSION}" + VERSION = "#{LibLsquic::MAJOR_VERSION}.#{LibLsquic::MINOR_VERSION}.#{LibLsquic::PATCH_VERSION}" end diff --git a/src/lsquic/channeled_pipe.cr b/src/lsquic/channeled_pipe.cr new file mode 100644 index 0000000..e0be9c2 --- /dev/null +++ b/src/lsquic/channeled_pipe.cr @@ -0,0 +1,89 @@ +# Based on https://github.com/anykeyh/channeled_pipe/blob/master/src/channeled_pipe/channeled_pipe.cr +class IO::ChanneledPipe < IO + BUFFER_SIZE = 8192 + + include IO::Buffered + + @channel : Channel(Bytes?) + @direction : Symbol + @buffer : Bytes? + + getter? closed = false + + protected def initialize(@channel, @direction) + end + + def unbuffered_read(slice : Bytes) + raise "Cannot read from write side" if @direction == :w + return 0 if @channel.closed? && !@buffer + + buffer = @buffer + + if buffer + bytes_read = {slice.size, buffer.size}.min + slice.copy_from(buffer.to_unsafe, bytes_read) + + if buffer.size == bytes_read + @buffer = nil + else + @buffer = buffer[bytes_read, buffer.size - bytes_read] + end + + return bytes_read + else + buffer = @channel.receive + + if buffer + bytes_read = {slice.size, buffer.size}.min + slice.copy_from(buffer.to_unsafe, bytes_read) + + if buffer.size > bytes_read + @buffer = buffer[bytes_read, buffer.size - bytes_read] + end + + return bytes_read + else + @channel.close + return 0 + end + end + end + + def unbuffered_write(slice : Bytes) + raise "Write not allowed on read side" if @direction == :r + raise "Closed stream" if @closed + @channel.send slice.clone + end + + def close_channel + @channel.close + end + + def unbuffered_flush + # Nothing + end + + def unbuffered_rewind + raise IO::Error.new("Can't rewind") + end + + def unbuffered_close + return if @closed + @closed = true + @channel.send nil + end + + def self.new(mem = BUFFER_SIZE) + mem = BUFFER_SIZE if mem <= 0 + + capacity = (mem / BUFFER_SIZE) + + ((mem % BUFFER_SIZE != 0) ? 1 : 0) + + channel = Channel(Bytes?).new(capacity: mem) + + { + ChanneledPipe.new(channel, :r), + ChanneledPipe.new(channel, :w), + } + end +end diff --git a/src/lsquic/client.cr b/src/lsquic/client.cr index 0bd4274..b99f3db 100644 --- a/src/lsquic/client.cr +++ b/src/lsquic/client.cr @@ -1,127 +1,76 @@ -require "http/headers" -require "http/client" -require "socket/udp_socket" +require "http" +require "socket" -struct QUIC::PeerCtx - property socket : UDPSocket +module QUIC + class StreamCtx + property request : HTTP::Request + property io : IO::ChanneledPipe - def initialize(@socket) + def initialize(@request, @io) + end end - def local_address - @socket.local_address - end + class Client + REQUIRED_HEADERS = {":method", ":scheme", ":path", ":authority"} - def remote_address - @socket.remote_address - end -end - -struct QUIC::StreamCtx - property requests : Array(HTTP::Request) - property io : IO - - def initialize - @requests = [] of HTTP::Request - @io = IO::Memory.new - end -end - -class QUIC::Client - ENGINE_FLAGS = LibLsquic::LSENG_HTTP - LibLsquic.global_init(ENGINE_FLAGS & LibLsquic::LSENG_SERVER ? LibLsquic::GLOBAL_SERVER : LibLsquic::GLOBAL_CLIENT) - - # The set of possible valid body types. - alias BodyType = String | Bytes | IO | Nil - - getter host : String - getter port : Int32 - getter! tls : OpenSSL::SSL::Context::Client - - @peer_ctx : PeerCtx | Nil - @engine : LibLsquic::EngineT | Nil - @conn : LibLsquic::ConnT | Nil - @engine : LibLsquic::EngineT | Nil - @engine_settings : LibLsquic::EngineSettings - @stream_if : LibLsquic::StreamIf - @engine_api : LibLsquic::EngineApi - - @dns_timeout : Float64? - @connect_timeout : Float64? - @read_timeout : Float64? - - def initialize(@host : String, port = nil, tls : Bool | OpenSSL::SSL::Context::Client = false) - check_host_only(@host) - - @tls = case tls - when true - OpenSSL::SSL::Context::Client.new - when OpenSSL::SSL::Context::Client - tls - when false - nil - end - - @port = (port || 443).to_i - - LibLsquic.engine_init_settings(out @engine_settings, ENGINE_FLAGS) - @engine_settings.es_ua = "Chrome/78.0.3904.97 Linux x86_64" - @engine_settings.es_ecn = 0 - - err_buf = Bytes.new(0x100) - err_code = LibLsquic.engine_check_settings(pointerof(@engine_settings), ENGINE_FLAGS, err_buf, err_buf.size) - raise String.new(err_buf) if err_code != 0 - - @stream_if = LibLsquic::StreamIf.new - @stream_if.on_new_conn = ->(stream_if_ctx : Void*, c : LibLsquic::ConnT) { stream_if_ctx } - @stream_if.on_conn_closed = ->(c : LibLsquic::ConnT) do - Box.box(nil) + def self.stream_readf(stream_if_ctx : Void*, buf : UInt8*, buf_len : LibC::SizeT, fin : LibC::Int) + stream_ctx = Box(StreamCtx).unbox(stream_if_ctx) + stream_ctx.io.write Slice.new(buf, buf_len) + buf_len end - @stream_if.on_new_stream = ->(stream_if_ctx : Void*, s : LibLsquic::StreamT) do - if LibLsquic.stream_is_pushed(s) != 0 - return stream_if_ctx - end - - LibLsquic.stream_wantwrite(s, 1) + def self.on_new_conn(stream_if_ctx : Void*, c : LibLsquic::ConnT) stream_if_ctx end - @stream_if.on_write = ->(s : LibLsquic::StreamT, stream_if_ctx : Void*) do - request = Box(StreamCtx).unbox(stream_if_ctx).requests.shift - raise "No request" if !request + def self.on_conn_closed(c : LibLsquic::ConnT) + Box.box(nil) + end - headers = [] of LibLsquic::HttpHeader - (request.headers.to_a.sort_by { |k, v| {":authority", ":path", ":scheme", ":method"}.index(k) || -1 }).reverse.each do |tuple| - name, values = tuple - name = name.downcase + def self.on_new_stream(stream_if_ctx : Void*, s : LibLsquic::StreamT) + stream_ctx = LibLsquic.stream_conn(s) + .try { |c| LibLsquic.conn_get_ctx(c) } + .try { |c| Box(StreamCtx).unbox(c) } - values.each do |value| - name_vec = LibLsquic::Iovec.new - name_vec.iov_base = name.to_slice - name_vec.iov_len = name.bytesize + return Box.box(stream_ctx) if LibLsquic.stream_is_pushed(s) != 0 - value_vec = LibLsquic::Iovec.new - value_vec.iov_base = value.to_slice - value_vec.iov_len = value.bytesize + LibLsquic.stream_wantwrite(s, 1) + Box.box(stream_ctx) + end - header = LibLsquic::HttpHeader.new - header.name = name_vec - header.value = value_vec + def self.on_write(s : LibLsquic::StreamT, stream_if_ctx : Void*) + stream_ctx = Box(StreamCtx).unbox(stream_if_ctx) + request_headers = stream_ctx.request.headers - headers << header - end + headers = [] of LibLsquic::LsxpackHeader + REQUIRED_HEADERS.each do |name| + value = stream_ctx.request.headers[name] + headers << LibLsquic::LsxpackHeader.new( + buf: "#{name}#{value}", + name_len: name.bytesize, + name_offset: 0, + val_len: value.bytesize, + val_offset: name.bytesize + ) end - http_headers = LibLsquic::HttpHeaders.new - http_headers.count = headers.size - http_headers.headers = headers.to_unsafe + request_headers.each do |name, values| + name = name.downcase + next if REQUIRED_HEADERS.includes? name + headers << LibLsquic::LsxpackHeader.new( + buf: "#{name}#{values[0]}", + name_len: name.bytesize, + name_offset: 0, + val_len: values[0].bytesize, + val_offset: name.bytesize + ) + end - # For payload, last argument is 0 - raise "Could not send headers" if LibLsquic.stream_send_headers(s, pointerof(http_headers), request.body ? 0 : 1) != 0 + http_headers = LibLsquic::HttpHeaders.new(count: headers.size, headers: headers.to_unsafe) - if request.body - body = request.body.not_nil!.gets_to_end + raise "Could not send headers" if LibLsquic.stream_send_headers(s, pointerof(http_headers), stream_ctx.request.body ? 0 : 1) != 0 + + if body = stream_ctx.request.body.try &.gets_to_end LibLsquic.stream_write(s, body, body.bytesize) LibLsquic.stream_flush(s) end @@ -130,140 +79,267 @@ class QUIC::Client LibLsquic.stream_wantwrite(s, 0) LibLsquic.stream_wantread(s, 1) - stream_if_ctx + Box.box(stream_ctx) end - @stream_if.on_read = ->(s : LibLsquic::StreamT, stream_if_ctx : Void*) do + def self.on_read(s : LibLsquic::StreamT, stream_if_ctx : Void*) stream_ctx = Box(StreamCtx).unbox(stream_if_ctx) + bytes_read = LibLsquic.stream_readf(s, ->stream_readf, Box.box(stream_ctx)) - buffer = Bytes.new(0x200) - bytes_read = LibLsquic.stream_read(s, buffer, buffer.size) if bytes_read > 0 - stream_ctx.io.write buffer[0, bytes_read] + # Nothing elsif bytes_read == 0 LibLsquic.stream_shutdown(s, 0) - elsif LibLsquic.stream_is_rejected(s) + LibLsquic.stream_wantread(s, 0) + elsif LibLsquic.stream_is_rejected(s) == 1 LibLsquic.stream_close(s) else - raise "Could not read stream" + # raise "Could not read response" end stream_if_ctx end - # TODO: Allow engine to break with existing connections - @stream_if.on_close = ->(s : LibLsquic::StreamT, stream_if_ctx : Void*) do - LibLsquic.conn_close(LibLsquic.stream_conn(s)) + def self.on_close(s : LibLsquic::StreamT, stream_if_ctx : Void*) + stream_ctx = Box(StreamCtx).unbox(stream_if_ctx) + stream_ctx.io.close + GC.free stream_if_ctx stream_if_ctx end - @engine_api = LibLsquic::EngineApi.new - @engine_api.ea_settings = pointerof(@engine_settings) - @engine_api.ea_stream_if = pointerof(@stream_if) - - @stream_ctx = StreamCtx.new - @engine_api.ea_stream_if_ctx = Box.box(@stream_ctx) # TODO - - @engine_api.ea_packets_out = ->(peer_ctx : Void*, specs : LibLsquic::OutSpec*, count : LibC::UInt) do + def self.ea_packets_out(peer_ctx : Void*, specs : LibLsquic::OutSpec*, count : LibC::UInt) packets_out = 0 count.times do |i| spec = specs[i] - peer_ctx = Box(PeerCtx).unbox(spec.peer_ctx) + socket = Box(UDPSocket).unbox(spec.peer_ctx) spec.iovlen.times do |j| iov = spec.iov[j] begin - peer_ctx.socket.send(iov.iov_base.to_slice(iov.iov_len), to: peer_ctx.remote_address) + socket.send(iov.iov_base.to_slice(iov.iov_len), to: socket.remote_address) packets_out += 1 rescue ex + break end end end packets_out end - end - private def check_host_only(string : String) - # When parsing a URI with just a host - # we end up with a URI with just a path - uri = URI.parse(string) - if uri.scheme || uri.host || uri.port || uri.query || uri.user || uri.password || uri.path.includes?('/') + ENGINE_FLAGS = LibLsquic::LSENG_HTTP + LibLsquic.global_init(ENGINE_FLAGS & LibLsquic::LSENG_SERVER ? LibLsquic::GLOBAL_SERVER : LibLsquic::GLOBAL_CLIENT) + + property family : Socket::Family = Socket::Family::INET + + # The set of possible valid body types. + alias BodyType = String | Bytes | IO | Nil + + getter host : String + getter port : Int32 + getter! tls : OpenSSL::SSL::Context::Client + + @stream_channel : Channel(StreamCtx?) + @dns_timeout : Float64? + @connect_timeout : Float64? + @read_timeout : Float64? + @socket : UDPSocket? + @stream_ctx : StreamCtx? + + def initialize(@host : String, port = nil, tls : Bool | OpenSSL::SSL::Context::Client = false) + check_host_only(@host) + + @tls = case tls + when true + OpenSSL::SSL::Context::Client.new + when OpenSSL::SSL::Context::Client + tls + when false + nil + end + + @port = (port || 443).to_i + @stream_channel = Channel(StreamCtx?).new(20) + @stream_ctx = nil + @engine_open = false + end + + def run_engine + LibLsquic.engine_init_settings(out engine_settings, ENGINE_FLAGS) + engine_settings.es_ua = "Chrome/83.0.4103.61 Linux x86_64" + engine_settings.es_ecn = 0 + + err_buf = Bytes.new(0x100) + err_code = LibLsquic.engine_check_settings(pointerof(engine_settings), ENGINE_FLAGS, err_buf, err_buf.size) + raise String.new(err_buf) if err_code != 0 + + stream_if = LibLsquic::StreamIf.new + stream_if.on_new_conn = ->QUIC::Client.on_new_conn(Void*, LibLsquic::ConnT) + stream_if.on_conn_closed = ->QUIC::Client.on_conn_closed(LibLsquic::ConnT) + stream_if.on_new_stream = ->QUIC::Client.on_new_stream(Void*, LibLsquic::StreamT) + stream_if.on_write = ->QUIC::Client.on_write(LibLsquic::StreamT, Void*) + stream_if.on_read = ->QUIC::Client.on_read(LibLsquic::StreamT, Void*) + stream_if.on_close = ->QUIC::Client.on_close(LibLsquic::StreamT, Void*) + + engine_api = LibLsquic::EngineApi.new + engine_api.ea_settings = pointerof(engine_settings) + engine_api.ea_stream_if = pointerof(stream_if) + engine_api.ea_packets_out = ->QUIC::Client.ea_packets_out(Void*, LibLsquic::OutSpec*, LibC::UInt) + + # logger_if = LibLsquic::LoggerIf.new + # logger_if.log_buf = ->(logger_ctx : Void*, msg_buf : LibC::Char*, msg_size : LibC::SizeT) { puts String.new(msg_buf); 0 } + # LibLsquic.logger_init(pointerof(logger_if), nil, LibLsquic::LoggerTimestampStyle::LltsHhmmssms) + # LibLsquic.set_log_level("debug") + + engine = LibLsquic.engine_new(ENGINE_FLAGS, pointerof(engine_api)) + hostname = host.starts_with?('[') && host.ends_with?(']') ? host[1..-2] : host + @engine_open = true + + conn = LibLsquic.engine_connect( + engine, + LibLsquic::Version::Lsqver050, + socket.local_address, + socket.remote_address, + Box.box(socket), nil, + hostname, 0, + nil, 0, + nil, 0 + ) + spawn do + while stream_ctx = @stream_channel.receive + LibLsquic.conn_set_ctx(conn, Box.box(stream_ctx)) + LibLsquic.conn_make_stream(conn) + LibLsquic.engine_process_conns(engine) + end + @engine_open = false + LibLsquic.engine_destroy(engine) + @socket.try &.close + @socket = nil + end + + begin + buffer = Bytes.new(0x600) + loop do + bytes_read = socket.read buffer + break if !@engine_open + LibLsquic.engine_packet_in(engine, buffer[0, bytes_read], bytes_read, socket.local_address, socket.remote_address, Box.box(socket), 0) if bytes_read != 0 + LibLsquic.engine_process_conns(engine) + end + rescue IO::Error + # may have already been closed + ensure + @socket.try &.close + @socket = nil + end + end + + def socket : UDPSocket + socket = @socket + return socket.not_nil! if @socket + + socket = UDPSocket.new @family + case @family + when Socket::Family::INET + socket.bind Socket::IPAddress.new("0.0.0.0", 0) + when Socket::Family::INET6 + socket.bind Socket::IPAddress.new("::", 0) + else + socket.bind Socket::IPAddress.new("0.0.0.0", 0) + end + + Socket::Addrinfo.udp(@host, @port, timeout: @dns_timeout, family: @family) do |addrinfo| + socket.connect(addrinfo, timeout: @connect_timeout) do |error| + close + error + end + end + + socket.read_timeout = @read_timeout if @read_timeout + socket.sync = false + + @socket = socket + end + + private def check_host_only(string : String) + # When parsing a URI with just a host + # we end up with a URI with just a path + uri = URI.parse(string) + if uri.scheme || uri.host || uri.port || uri.query || uri.user || uri.password || uri.path.includes?('/') + raise_invalid_host(string) + end + rescue URI::Error raise_invalid_host(string) end - rescue URI::Error - raise_invalid_host(string) - end - private def raise_invalid_host(string : String) - raise ArgumentError.new("The string passed to create an HTTP::Client must be just a host, not #{string.inspect}") - end - - def self.new(uri : URI, tls = nil) - tls = tls_flag(uri, tls) - host = validate_host(uri) - new(host, uri.port, tls) - end - - def self.new(uri : URI, tls = nil) - tls = tls_flag(uri, tls) - host = validate_host(uri) - client = new(host, uri.port, tls) - begin - yield client - ensure - client.close + private def raise_invalid_host(string : String) + raise ArgumentError.new("The string passed to create an HTTP::Client must be just a host, not #{string.inspect}") end - end - def self.new(host : String, port = nil, tls = false) - client = new(host, port, tls) - begin - yield client - ensure - client.close + def self.new(uri : URI, tls = nil) + tls = tls_flag(uri, tls) + host = validate_host(uri) + new(host, uri.port, tls) end - end - # Configures this client to perform basic authentication in every - # request. - def basic_auth(username, password) - header = "Basic #{Base64.strict_encode("#{username}:#{password}")}" - before_request do |request| - request.headers["Authorization"] = header + def self.new(uri : URI, tls = nil) + tls = tls_flag(uri, tls) + host = validate_host(uri) + client = new(host, uri.port, tls) + begin + yield client + ensure + client.close + end end - end - def read_timeout=(read_timeout : Number) - @read_timeout = read_timeout.to_f - end + def self.new(host : String, port = nil, tls = false) + client = new(host, port, tls) + begin + yield client + ensure + client.close + end + end - def read_timeout=(read_timeout : Time::Span) - self.read_timeout = read_timeout.total_seconds - end + # Configures this client to perform basic authentication in every + # request. + def basic_auth(username, password) + header = "Basic #{Base64.strict_encode("#{username}:#{password}")}" + before_request do |request| + request.headers["Authorization"] = header + end + end - def connect_timeout=(connect_timeout : Number) - @connect_timeout = connect_timeout.to_f - end + def read_timeout=(read_timeout : Number) + @read_timeout = read_timeout.to_f + end - def connect_timeout=(connect_timeout : Time::Span) - self.connect_timeout = connect_timeout.total_seconds - end + def read_timeout=(read_timeout : Time::Span) + self.read_timeout = read_timeout.total_seconds + end - def dns_timeout=(dns_timeout : Number) - @dns_timeout = dns_timeout.to_f - end + def connect_timeout=(connect_timeout : Number) + @connect_timeout = connect_timeout.to_f + end - def dns_timeout=(dns_timeout : Time::Span) - self.dns_timeout = dns_timeout.total_seconds - end + def connect_timeout=(connect_timeout : Time::Span) + self.connect_timeout = connect_timeout.total_seconds + end - def before_request(&callback : HTTP::Request ->) - before_request = @before_request ||= [] of (HTTP::Request ->) - before_request << callback - end + def dns_timeout=(dns_timeout : Number) + @dns_timeout = dns_timeout.to_f + end - {% for method in %w(get post put head delete patch options) %} + def dns_timeout=(dns_timeout : Time::Span) + self.dns_timeout = dns_timeout.total_seconds + end + + def before_request(&callback : HTTP::Request ->) + before_request = @before_request ||= [] of (HTTP::Request ->) + before_request << callback + end + + {% for method in %w(get post put head delete patch options) %} def {{method.id}}(path, headers : HTTP::Headers? = nil, body : BodyType = nil) : HTTP::Client::Response exec {{method.upcase}}, path, headers, body end @@ -325,252 +401,183 @@ class QUIC::Client end {% end %} - def exec(request : HTTP::Request) : HTTP::Client::Response - exec_internal(request) - end - - private def exec_internal(request) - response = exec_internal_single(request) - return handle_response(response) if response - - # Server probably closed the connection, so retry one - close - request.body.try &.rewind - response = exec_internal_single(request) - return handle_response(response) if response - - raise "Unexpected end of http response" - end - - private def exec_internal_single(request) - send_request(request) - @stream_ctx.io.rewind - - HTTP::Client::Response.from_io?(@stream_ctx.io, ignore_body: request.ignore_body?) - end - - private def handle_response(response) - close # unless response.keep_alive? - response - end - - def exec(request : HTTP::Request, &block) - exec_internal(request) do |response| - yield response + def exec(request : HTTP::Request) : HTTP::Client::Response + exec_internal(request) end - end - private def exec_internal(request, &block : Response -> T) : T forall T - exec_internal_single(request) do |response| - if response - return handle_response(response) { yield response } - end + private def exec_internal(request) + response = exec_internal_single(request) + return handle_response(response) if response - # Server probably closed the connection, so retry once - close - request.body.try &.rewind - exec_internal_single(request) do |response| - if response - return handle_response(response) do - yield response - end - end - end + raise "Unexpected end of http response" end - raise "Unexpected end of http response" - end - private def exec_internal_single(request) - send_request(request) - HTTP::Client::Response.from_io?(stream_ctx.io, ignore_body: request.ignore_body?) do |response| - yield response + private def exec_internal_single(request) + io = send_request(request) + HTTP::Client::Response.from_io?(io, ignore_body: request.ignore_body?) end - end - private def handle_response(response) - value = yield - response.body_io?.try &.close - close # unless response.keep_alive? - value - end - - private def send_request(request) - set_defaults request - run_before_request_callbacks(request) - - @stream_ctx.requests << request - LibLsquic.conn_make_stream(conn) - - run_engine - end - - private def set_defaults(request) - request.headers[":method"] ||= request.method - request.headers[":scheme"] ||= "https" - request.headers[":path"] ||= request.resource - request.headers[":authority"] ||= host_header - request.headers["user-agent"] ||= "Chrome/78.0.3904.97 Linux x86_64" - end - - private def self.default_one_shot_headers(headers) - headers ||= HTTP::Headers.new - headers["Connection"] ||= "close" - headers - end - - private def run_before_request_callbacks(request) - @before_request.try &.each &.call(request) - end - - def exec(method : String, path, headers : HTTP::Headers? = nil, body : BodyType = nil) : HTTP::Client::Response - exec new_request method, path, headers, body - end - - def exec(method : String, path, headers : HTTP::Headers? = nil, body : BodyType = nil) - exec(new_request(method, path, headers, body)) do |response| - yield response + private def handle_response(response) + # close unless response.keep_alive? + response end - end - def self.exec(method, url : String | URI, headers : HTTP::Headers? = nil, body : BodyType = nil, tls = nil) : HTTP::Client::Response - headers = default_one_shot_headers(headers) - exec(url, tls) do |client, path| - client.exec method, path, headers, body - end - end - - def self.exec(method, url : String | URI, headers : HTTP::Headers? = nil, body : BodyType = nil, tls = nil) - headers = default_one_shot_headers(headers) - exec(url, tls) do |client, path| - client.exec(method, path, headers, body) do |response| + def exec(request : HTTP::Request, &block) + exec_internal(request) do |response| yield response end end - end - def close - # @conn.try { |c| LibLsquic.conn_close(c) } - @conn = nil - end - - private def new_request(method, path, headers, body : BodyType) - HTTP::Request.new(method, path, headers, body) - end - - private def engine - engine = @engine - return engine if engine - - engine = LibLsquic.engine_new(ENGINE_FLAGS, pointerof(@engine_api)) - @engine = engine - end - - def run_engine - buffer = Bytes.new(0x600) - - loop do - LibLsquic.engine_process_conns(engine) - - if LibLsquic.engine_earliest_adv_tick(engine, out diff) == 0 - break - # else - # sleep (diff / 1000000).seconds - # sleep (diff % 1000000).microseconds + private def exec_internal(request, &block : HTTP::Client::Response -> T) : T forall T + exec_internal_single(request) do |response| + if response + return handle_response(response) { yield response } + end end - - bytes_read = peer_ctx.socket.read(buffer) - LibLsquic.engine_packet_in(engine, buffer[0, bytes_read], bytes_read, peer_ctx.local_address, peer_ctx.remote_address, Box.box(peer_ctx), 0) + raise "Unexpected end of http response" end - end - private def peer_ctx - peer_ctx = @peer_ctx - return peer_ctx if peer_ctx - - hostname = @host.starts_with?('[') && @host.ends_with?(']') ? @host[1..-2] : @host - socket = UDPSocket.new - socket.bind Socket::IPAddress.new("0.0.0.0", 0) - socket.read_timeout = @read_timeout if @read_timeout - Socket::Addrinfo.udp(host, port, timeout: @dns_timeout) do |addrinfo| - socket.connect(addrinfo, timeout: @connect_timeout) do |error| - error + private def exec_internal_single(request) + io = send_request(request) + HTTP::Client::Response.from_io?(io, ignore_body: request.ignore_body?) do |response| + yield response end end - socket.sync = false - peer_ctx = PeerCtx.new(socket) - @peer_ctx = peer_ctx - end - - def conn - conn = @conn - return conn if conn - - hostname = @host.starts_with?('[') && @host.ends_with?(']') ? @host[1..-2] : @host - conn = LibLsquic.engine_connect(engine, LibLsquic::Version::Lsqver046, peer_ctx.local_address, peer_ctx.remote_address, Box.box(peer_ctx), nil, hostname, 0, nil, 0, nil, 0) - @conn = conn - end - - private def host_header - if (@tls && @port != 443) || (!@tls && @port != 80) - "#{@host}:#{@port}" - else - @host - end - end - - private def self.exec(string : String, tls = nil) - uri = URI.parse(string) - - unless uri.scheme && uri.host - # Assume http if no scheme and host are specified - uri = URI.parse("http://#{string}") + private def handle_response(response) + value = yield + response.body_io?.try &.close + # close unless response.keep_alive? + value end - exec(uri, tls) do |client, path| - yield client, path + private def send_request(request) + set_defaults request + run_before_request_callbacks(request) + + spawn run_engine if !@engine_open + + reader, writer = IO::ChanneledPipe.new + # See https://github.com/crystal-lang/crystal/blob/0.32.0/src/openssl/ssl/context.cr#L126 + @stream_ctx = StreamCtx.new(request, writer) + @stream_channel.send @stream_ctx + reader end - end - protected def self.tls_flag(uri, context : OpenSSL::SSL::Context::Client?) - scheme = uri.scheme - case {scheme, context} - when {nil, _} - raise ArgumentError.new("Missing scheme: #{uri}") - when {"http", nil} - false - when {"http", OpenSSL::SSL::Context::Client} - raise ArgumentError.new("TLS context given for HTTP URI") - when {"https", nil} - true - when {"https", OpenSSL::SSL::Context::Client} - context - else - raise ArgumentError.new "Unsupported scheme: #{scheme}" + private def set_defaults(request) + request.headers[":method"] ||= request.method + request.headers[":scheme"] ||= "https" + request.headers[":path"] ||= request.resource + request.headers[":authority"] ||= host_header + request.headers["user-agent"] ||= "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36" end - end - protected def self.validate_host(uri) - host = uri.host - return host if host && !host.empty? + private def self.default_one_shot_headers(headers) + headers ||= HTTP::Headers.new + headers["connection"] ||= "close" + headers + end - raise ArgumentError.new %(Request URI must have host (URI is: #{uri})) - end + private def run_before_request_callbacks(request) + @before_request.try &.each &.call(request) + end - private def self.exec(uri : URI, tls = nil) - tls = tls_flag(uri, tls) - host = validate_host(uri) + def exec(method : String, path, headers : HTTP::Headers? = nil, body : BodyType = nil) : HTTP::Client::Response + exec new_request method, path, headers, body + end - port = uri.port - path = uri.full_path - user = uri.user - password = uri.password - - HTTP::Client.new(host, port, tls) do |client| - if user && password - client.basic_auth(user, password) + def exec(method : String, path, headers : HTTP::Headers? = nil, body : BodyType = nil) + exec(new_request(method, path, headers, body)) do |response| + yield response + end + end + + def self.exec(method, url : String | URI, headers : HTTP::Headers? = nil, body : BodyType = nil, tls = nil) : HTTP::Client::Response + headers = default_one_shot_headers(headers) + exec(url, tls) do |client, path| + client.exec method, path, headers, body + end + end + + def self.exec(method, url : String | URI, headers : HTTP::Headers? = nil, body : BodyType = nil, tls = nil) + headers = default_one_shot_headers(headers) + exec(url, tls) do |client, path| + client.exec(method, path, headers, body) do |response| + yield response + end + end + end + + def close + @stream_channel.send nil + Fiber.yield + end + + private def new_request(method, path, headers, body : BodyType) + HTTP::Request.new(method, path, headers, body) + end + + private def host_header + if (@tls && @port != 443) || (!@tls && @port != 80) + "#{@host}:#{@port}" + else + @host + end + end + + private def self.exec(string : String, tls = nil) + uri = URI.parse(string) + + unless uri.scheme && uri.host + # Assume http if no scheme and host are specified + uri = URI.parse("http://#{string}") + end + + exec(uri, tls) do |client, path| + yield client, path + end + end + + protected def self.tls_flag(uri, context : OpenSSL::SSL::Context::Client?) + scheme = uri.scheme + case {scheme, context} + when {nil, _} + raise ArgumentError.new("Missing scheme: #{uri}") + when {"http", nil} + false + when {"http", OpenSSL::SSL::Context::Client} + raise ArgumentError.new("TLS context given for HTTP URI") + when {"https", nil} + true + when {"https", OpenSSL::SSL::Context::Client} + context + else + raise ArgumentError.new "Unsupported scheme: #{scheme}" + end + end + + protected def self.validate_host(uri) + host = uri.host + return host if host && !host.empty? + + raise ArgumentError.new %(Request URI must have host (URI is: #{uri})) + end + + private def self.exec(uri : URI, tls = nil) + tls = tls_flag(uri, tls) + host = validate_host(uri) + + port = uri.port + path = uri.full_path + user = uri.user + password = uri.password + + HTTP::Client.new(host, port, tls) do |client| + if user && password + client.basic_auth(user, password) + end + yield client, path end - yield client, path end end end diff --git a/src/lsquic/ext/liblsquic.a b/src/lsquic/ext/liblsquic.a new file mode 100644 index 0000000..a9c7ade Binary files /dev/null and b/src/lsquic/ext/liblsquic.a differ diff --git a/src/lsquic/liblsquic.cr b/src/lsquic/liblsquic.cr index 817fdf1..9d193d4 100644 --- a/src/lsquic/liblsquic.cr +++ b/src/lsquic/liblsquic.cr @@ -1,76 +1,64 @@ -@[Link(ldflags: "#{__DIR__}/ext/liblsquic.a -lz")] +@[Link(ldflags: "#{__DIR__}/ext/liblsquic.a")] lib LibLsquic - MAX_CID_LEN = 20 - QQUIC_CID_LEN = 8 - LSENG_SERVER = 1 - LSENG_HTTP = 2 + LSENG_SERVER = 1 + LSENG_HTTP = 2 LSENG_HTTP_SERVER = LSENG_SERVER | LSENG_HTTP - GLOBAL_CLIENT = 1 - GLOBAL_SERVER = 2 - MAJOR_VERSION = 2 - MINOR_VERSION = 6 - PATCH_VERSION = 1 - EXPERIMENTAL_Q098 = 0 - DEPRECATED_VERSIONS = 0 - MIN_CFW = 16 * 1024 - DF_CFCW_SERVER = 3 * 1024 * 1024 / 2 - DF_CFCW_CLIENT = 15 * 1024 * 1024 - DF_SFCW_SERVER = 1 * 1024 * 1024 - DF_SFCW_CLIENT = 6 * 1024 * 1024 + GLOBAL_CLIENT = 1 + GLOBAL_SERVER = 2 + MAJOR_VERSION = 2 + MINOR_VERSION = 18 + PATCH_VERSION = 1 + EXPERIMENTAL_Q098 = 0 + DEPRECATED_VERSIONS = 0 DF_MAX_STREAMS_IN = 100 DF_INIT_MAX_STREAM_DATA_BIDI_LOCAL_SERVER = 0 DF_INIT_MAX_STREAM_DATA_BIDI_REMOTE_CLIENT = 0 DF_INIT_MAX_STREAMS_UNI_CLIENT = 100 DF_INIT_MAX_STREAMS_UNI_SERVER = 3 - DF_INIT_MAX_STREAM_DATA_UNI_CLIENT = 32 * 1024 - DF_INIT_MAX_STREAM_DATA_UNI_SERVER = 12 * 1024 - DF_IDLE_TIMEOUT = 30 - DF_PING_PERIOD = 15 - DF_HANDSHAKE_TO = 10 * 1000 * 1000 - DF_IDLE_CONN_TO = DF_IDLE_TIMEOUT * 1000 * 1000 - DF_SILENT_CLOSE = 1 - DF_MAX_HEADER_LIST_SIZE = 0 + DF_IDLE_TIMEOUT = 30 + DF_PING_PERIOD = 15 + DF_SILENT_CLOSE = 1 + DF_MAX_HEADER_LIST_SIZE = 0 DF_UA = "LSQUIC" DF_STTL = 86400 - DF_MAX_INCHOATE = 1 * 1000 * 1000 - DF_SUPPORT_SREJ_SERVER = 1 - DF_SUPPORT_SREJ_CLIENT = 0 - DF_SUPPORT_NSTP = 0 - DF_SUPPORT_PUSH = 1 - DF_SUPPORT_TCID0 = 1 - DF_HONOR_PRST = 0 - DF_SEND_PRST = 0 - DF_PROGRESS_CHECK = 1000 - DF_RW_ONCE = 0 - DF_PROC_TIME_THRESH = 0 - DF_PACE_PACKETS = 1 - DF_CLOCK_GRANULARITY = 1000 - DF_SCID_LEN = 8 - DF_SCID_ISS_RATE = 60 - DF_QPACK_DEC_MAX_BLOCKED = 100 - DF_QPACK_DEC_MAX_SIZE = 4096 - DF_QPACK_ENC_MAX_BLOCKED = 100 - DF_QPACK_ENC_MAX_SIZE = 4096 - DF_ECN = 0 - DF_ALLOW_MIGRATION = 1 - DF_CC_ALGO = 2 + DF_SUPPORT_NSTP = 0 + DF_SUPPORT_PUSH = 1 + DF_SUPPORT_TCID0 = 1 + DF_HONOR_PRST = 0 + DF_SEND_PRST = 0 + DF_PROGRESS_CHECK = 1000 + DF_RW_ONCE = 0 + DF_PROC_TIME_THRESH = 0 + DF_PACE_PACKETS = 1 + DF_CLOCK_GRANULARITY = 1000 + DF_SCID_LEN = 8 + DF_SCID_ISS_RATE = 60 + DF_QPACK_DEC_MAX_BLOCKED = 100 + DF_QPACK_DEC_MAX_SIZE = 4096 + DF_QPACK_ENC_MAX_BLOCKED = 100 + DF_QPACK_ENC_MAX_SIZE = 4096 + DF_ECN = 0 + DF_ALLOW_MIGRATION = 1 + DF_QL_BITS = 2 + DF_SPIN = 1 + DF_DELAYED_ACKS = 0 + DF_TIMESTAMPS = 1 + DF_CC_ALGO = 1 + DF_MAX_UDP_PAYLOAD_SIZE_RX = 0 + DF_GREASE_QUIC_BIT = 1 + DF_NOPROGRESS_TIMEOUT_SERVER = 60 + DF_NOPROGRESS_TIMEOUT_CLIENT = 0 struct Cid - len : UintFast8T + len : UInt8 u_cid : CidUCid end - alias UintFast8T = UInt8 - union CidUCid - buf : Uint8T[20] - id : Uint64T + buf : LibC::UInt8T[20] + id : LibC::UInt64T end - alias X__Uint8T = UInt8 - alias Uint8T = X__Uint8T - alias X__Uint64T = LibC::ULong - alias Uint64T = X__Uint64T alias Engine = Void alias Conn = Void alias ConnCtx = Void @@ -79,16 +67,26 @@ lib LibLsquic struct HttpHeaders count : LibC::Int - headers : HttpHeader* + headers : LsxpackHeader* end - struct HttpHeader - name : Iovec - value : Iovec + struct LsxpackHeader + buf : LibC::Char* + name_hash : LibC::UInt32T + nameval_hash : LibC::UInt32T + name_offset : LibC::UInt16T + name_len : LibC::UInt16T + val_offset : LibC::UInt16T + val_len : LibC::UInt16T + chain_next_idx : LibC::UInt16T + hpack_index : LibC::UInt8T + qpack_index : LibC::UInt8T + app_index : LibC::UInt8T + flags : LibC::UInt8T + indexed_type : LibC::UInt8T + dec_overhead : LibC::UInt8T end - type HttpHeaderT = HttpHeader - struct Iovec iov_base : UInt8* iov_len : LibC::SizeT @@ -108,9 +106,7 @@ lib LibLsquic end type ConnT = Void* - type ConnCtxT = Void* type StreamT = Void* - type StreamCtxT = Void* enum HskStatus LsqHskFail = 0 LsqHskOk = 1 @@ -130,12 +126,11 @@ lib LibLsquic es_silent_close : LibC::Int es_max_header_list_size : LibC::UInt es_ua : LibC::Char* - es_sttl : Uint64T - es_pdmd : Uint32T - es_aead : Uint32T - es_kexs : Uint32T + es_sttl : LibC::UInt64T + es_pdmd : LibC::UInt32T + es_aead : LibC::UInt32T + es_kexs : LibC::UInt32T es_max_inchoate : LibC::UInt - es_support_srej : LibC::Int es_support_push : LibC::Int es_support_tcid0 : LibC::Int es_support_nstp : LibC::Int @@ -163,10 +158,13 @@ lib LibLsquic es_ecn : LibC::Int es_allow_migration : LibC::Int es_cc_algo : LibC::UInt + es_ql_bits : LibC::Int + es_spin : LibC::Int + es_delayed_acks : LibC::Int + es_timestamps : LibC::Int + es_max_packet_size_rx : LibC::UInt16T end - alias X__Uint32T = LibC::UInt - alias Uint32T = X__Uint32T fun engine_init_settings = lsquic_engine_init_settings(x0 : EngineSettings*, engine_flags : LibC::UInt) fun engine_check_settings = lsquic_engine_check_settings(settings : EngineSettings*, engine_flags : LibC::UInt, err_buf : LibC::Char*, err_buf_sz : LibC::SizeT) : LibC::Int @@ -185,8 +183,7 @@ lib LibLsquic shi_lookup : (Void*, Void*, LibC::UInt, Void**, LibC::UInt* -> LibC::Int) end - alias X__TimeT = LibC::Long - alias TimeT = X__TimeT + alias TimeT = LibC::Long struct PackoutMemIf pmi_allocate : (Void*, Void*, LibC::UShort, LibC::Char -> Void*) @@ -195,25 +192,17 @@ lib LibLsquic end struct HsetIf - hsi_create_header_set : (Void*, LibC::Int -> Void*) - hsi_process_header : (Void*, LibC::UInt, LibC::Char*, LibC::UInt, LibC::Char*, LibC::UInt -> HeaderStatus) + hsi_create_header_set : (Void*, StreamT, LibC::Int -> Void*) + hsi_prepare_decode : (Void*, LsxpackHeader*, LibC::SizeT -> LsxpackHeader*) + hsi_process_header : (Void*, LsxpackHeader* -> LibC::Int) hsi_discard_header_set : (Void* -> Void) + hsi_flags : HsiFlag end - enum HeaderStatus - HdrOk = 0 - HdrErrDuplicatePsdoHdr = 1 - HdrErrIncomplReqPsdoHdr = 2 - HdrErrUnnecReqPsdoHdr = 3 - HdrErrBadReqHeader = 4 - HdrErrIncomplRespPsdoHdr = 5 - HdrErrUnnecRespPsdoHdr = 6 - HdrErrUnknownPsdoHdr = 7 - HdrErrUppercaseHeader = 8 - HdrErrMisplacedPsdoHdr = 9 - HdrErrMissingPsdoHdr = 10 - HdrErrHeadersTooLarge = 11 - HdrErrNomem = 12 + enum HsiFlag + HsiHttp1X = 2 + HsiHashName = 4 + HsiHashNameval = 8 end struct KeylogIf @@ -245,46 +234,49 @@ lib LibLsquic ea_hsi_ctx : Void* ea_keylog_if : KeylogIf* ea_keylog_ctx : Void* + ea_alpn : LibC::Char* end alias PacketsOutF = (Void*, OutSpec*, LibC::UInt -> LibC::Int) alias SslCtxSt = Void - alias LookupCertF = (Void*, LibC::Sockaddr*, LibC::Char* -> SslCtxSt*) + alias Sockaddr = LibC::Sockaddr + alias LookupCertF = (Void*, Sockaddr*, LibC::Char* -> SslCtxSt*) type CidT = Cid alias CidsUpdateF = (Void*, Void**, CidT*, LibC::UInt -> Void) alias StackStX509 = Void - fun engine_new = lsquic_engine_new(engine_flags : LibC::UInt, x1 : EngineApi*) : EngineT + fun engine_new = lsquic_engine_new(engine_flags : LibC::UInt, api : EngineApi*) : EngineT type EngineT = Void* - fun engine_connect = lsquic_engine_connect(x0 : EngineT, x1 : Version, local_sa : LibC::Sockaddr*, peer_sa : LibC::Sockaddr*, peer_ctx : Void*, conn_ctx : Void*, hostname : LibC::Char*, max_packet_size : LibC::UShort, zero_rtt : UInt8*, zero_rtt_len : LibC::SizeT, token : UInt8*, token_sz : LibC::SizeT) : ConnT + fun engine_connect = lsquic_engine_connect(x0 : EngineT, x1 : Version, local_sa : Sockaddr*, peer_sa : Sockaddr*, peer_ctx : Void*, conn_ctx : Void*, hostname : LibC::Char*, max_packet_size : LibC::UShort, zero_rtt : UInt8*, zero_rtt_len : LibC::SizeT, token : UInt8*, token_sz : LibC::SizeT) : ConnT + enum Version - Lsqver039 = 0 - Lsqver043 = 1 - Lsqver046 = 2 - LsqverId23 = 3 - LsqverId24 = 4 - LsqverVerneg = 5 - NLsqver = 6 + Lsqver043 = 0 + Lsqver046 = 1 + Lsqver050 = 2 + LsqverId27 = 3 + LsqverId28 = 4 + LsqverId29 = 5 + LsqverVerneg = 6 + NLsqver = 7 end - fun engine_packet_in = lsquic_engine_packet_in(x0 : EngineT, packet_in_data : UInt8*, packet_in_size : LibC::SizeT, sa_local : LibC::Sockaddr*, sa_peer : LibC::Sockaddr*, peer_ctx : Void*, ecn : LibC::Int) : LibC::Int + + fun engine_packet_in = lsquic_engine_packet_in(x0 : EngineT, packet_in_data : UInt8*, packet_in_size : LibC::SizeT, sa_local : Sockaddr*, sa_peer : Sockaddr*, peer_ctx : Void*, ecn : LibC::Int) : LibC::Int fun engine_process_conns = lsquic_engine_process_conns(engine : EngineT) fun engine_has_unsent_packets = lsquic_engine_has_unsent_packets(engine : EngineT) : LibC::Int fun engine_send_unsent_packets = lsquic_engine_send_unsent_packets(engine : EngineT) fun engine_destroy = lsquic_engine_destroy(x0 : EngineT) fun conn_n_avail_streams = lsquic_conn_n_avail_streams(x0 : ConnT) : LibC::UInt - fun conn_make_stream = lsquic_conn_make_stream(x0 : ConnT) : ConnCtxT + fun conn_make_stream = lsquic_conn_make_stream(x0 : ConnT) : Void* fun conn_n_pending_streams = lsquic_conn_n_pending_streams(x0 : ConnT) : LibC::UInt fun conn_cancel_pending_streams = lsquic_conn_cancel_pending_streams(x0 : ConnT, n : LibC::UInt) : LibC::UInt fun conn_going_away = lsquic_conn_going_away(x0 : ConnT) fun conn_close = lsquic_conn_close(x0 : ConnT) fun stream_wantread = lsquic_stream_wantread(s : StreamT, is_want : LibC::Int) : LibC::Int - fun stream_read = lsquic_stream_read(s : StreamT, buf : Void*, len : LibC::SizeT) : SsizeT - alias X__SsizeT = LibC::Long - alias SsizeT = X__SsizeT - fun stream_readv = lsquic_stream_readv(s : StreamT, x1 : Iovec*, iovcnt : LibC::Int) : SsizeT - fun stream_readf = lsquic_stream_readf(s : StreamT, readf : (Void*, UInt8*, LibC::SizeT, LibC::Int -> LibC::SizeT), ctx : Void*) : SsizeT + fun stream_read = lsquic_stream_read(s : StreamT, buf : Void*, len : LibC::SizeT) : LibC::SizeT + fun stream_readv = lsquic_stream_readv(s : StreamT, vec : Iovec*, iovcnt : LibC::Int) : LibC::SizeT + fun stream_readf = lsquic_stream_readf(s : StreamT, readf : (Void*, UInt8*, LibC::SizeT, LibC::Int -> LibC::SizeT), ctx : Void*) : LibC::SizeT fun stream_wantwrite = lsquic_stream_wantwrite(s : StreamT, is_want : LibC::Int) : LibC::Int - fun stream_write = lsquic_stream_write(s : StreamT, buf : Void*, len : LibC::SizeT) : SsizeT - fun stream_writev = lsquic_stream_writev(s : StreamT, vec : Iovec*, count : LibC::Int) : SsizeT + fun stream_write = lsquic_stream_write(s : StreamT, buf : Void*, len : LibC::SizeT) : LibC::SizeT + fun stream_writev = lsquic_stream_writev(s : StreamT, vec : Iovec*, count : LibC::Int) : LibC::SizeT struct Reader lsqr_read : (Void*, Void*, LibC::SizeT -> LibC::SizeT) @@ -292,19 +284,18 @@ lib LibLsquic lsqr_ctx : Void* end - fun stream_writef = lsquic_stream_writef(x0 : StreamT, x1 : Reader*) : SsizeT + fun stream_writef = lsquic_stream_writef(x0 : StreamT, x1 : Reader*) : LibC::SizeT fun stream_flush = lsquic_stream_flush(s : StreamT) : LibC::Int - fun stream_send_headers = lsquic_stream_send_headers(s : StreamT, h : HttpHeaders*, eos : LibC::Int) : LibC::Int - type HttpHeadersT = HttpHeaders + fun stream_send_headers = lsquic_stream_send_headers(s : StreamT, headers : HttpHeaders*, eos : LibC::Int) : LibC::Int fun stream_get_hset = lsquic_stream_get_hset(x0 : StreamT) : Void* - fun conn_push_stream = lsquic_conn_push_stream(c : ConnT, hdr_set : Void*, s : StreamT, url : Iovec*, authority : Iovec*, headers : HttpHeaders*) : LibC::Int + fun conn_push_stream = lsquic_conn_push_stream(c : ConnT, hdr_set : Void*, s : StreamT, headers : HttpHeaders*) : LibC::Int fun conn_is_push_enabled = lsquic_conn_is_push_enabled(x0 : ConnT) : LibC::Int fun stream_shutdown = lsquic_stream_shutdown(s : StreamT, how : LibC::Int) : LibC::Int fun stream_close = lsquic_stream_close(s : StreamT) : LibC::Int fun conn_get_server_cert_chain = lsquic_conn_get_server_cert_chain(x0 : ConnT) : StackStX509* fun stream_id = lsquic_stream_id(s : StreamT) : StreamIdT - alias StreamIdT = Uint64T - fun stream_get_ctx = lsquic_stream_get_ctx(s : StreamT) : StreamCtxT + alias StreamIdT = LibC::UInt64T + fun stream_get_ctx = lsquic_stream_get_ctx(s : StreamT) : Void* fun stream_is_pushed = lsquic_stream_is_pushed(s : StreamT) : LibC::Int fun stream_is_rejected = lsquic_stream_is_rejected(s : StreamT) : LibC::Int fun stream_refuse_push = lsquic_stream_refuse_push(s : StreamT) : LibC::Int @@ -312,16 +303,16 @@ lib LibLsquic fun stream_priority = lsquic_stream_priority(s : StreamT) : LibC::UInt fun stream_set_priority = lsquic_stream_set_priority(s : StreamT, priority : LibC::UInt) : LibC::Int fun stream_conn = lsquic_stream_conn(s : StreamT) : ConnT - fun conn_get_stream_by_id = lsquic_conn_get_stream_by_id(c : ConnT, stream_id : StreamIdT) : StreamT fun conn_id = lsquic_conn_id(c : ConnT) : CidT* fun conn_get_engine = lsquic_conn_get_engine(c : ConnT) : EngineT - fun conn_get_sockaddr = lsquic_conn_get_sockaddr(c : ConnT, local : LibC::Sockaddr**, peer : LibC::Sockaddr**) : LibC::Int + fun conn_get_sockaddr = lsquic_conn_get_sockaddr(c : ConnT, local : Sockaddr**, peer : Sockaddr**) : LibC::Int struct LoggerIf log_buf : (Void*, LibC::Char*, LibC::SizeT -> LibC::Int) end fun logger_init = lsquic_logger_init(x0 : LoggerIf*, logger_ctx : Void*, x2 : LoggerTimestampStyle) + enum LoggerTimestampStyle LltsNone = 0 LltsHhmmssms = 1 @@ -331,6 +322,7 @@ lib LibLsquic LltsYyyymmddHhmmssus = 5 NLlts = 6 end + fun set_log_level = lsquic_set_log_level(log_level : LibC::Char*) : LibC::Int fun logger_lopt = lsquic_logger_lopt(optarg : LibC::Char*) : LibC::Int fun engine_quic_versions = lsquic_engine_quic_versions(x0 : EngineT) : LibC::UInt @@ -344,15 +336,14 @@ lib LibLsquic LsqCryQuic = 0 LsqCryTlSv13 = 1 end + fun conn_crypto_cipher = lsquic_conn_crypto_cipher(c : ConnT) : LibC::Char* fun str2ver = lsquic_str2ver(str : LibC::Char*, len : LibC::SizeT) : Version fun alpn2ver = lsquic_alpn2ver(alpn : LibC::Char*, len : LibC::SizeT) : Version fun engine_cooldown = lsquic_engine_cooldown(x0 : EngineT) - fun hsk_getssl = lsquic_hsk_getssl(conn : ConnT) : SslSt* - alias SslSt = Void - fun conn_get_ctx = lsquic_conn_get_ctx(x0 : ConnT) : ConnCtxT - fun conn_set_ctx = lsquic_conn_set_ctx(x0 : ConnT, x1 : ConnCtxT) - fun conn_get_peer_ctx = lsquic_conn_get_peer_ctx(x0 : ConnT, local_sa : LibC::Sockaddr*) : Void* + fun conn_get_ctx = lsquic_conn_get_ctx(x0 : ConnT) : Void* + fun conn_set_ctx = lsquic_conn_set_ctx(x0 : ConnT, x1 : Void*) + fun conn_get_peer_ctx = lsquic_conn_get_peer_ctx(x0 : ConnT, local_sa : Sockaddr*) : Void* fun conn_abort = lsquic_conn_abort(x0 : ConnT) fun get_alt_svc_versions = lsquic_get_alt_svc_versions(versions : LibC::UInt) : LibC::Char* fun get_h3_alpns = lsquic_get_h3_alpns(versions : LibC::UInt) : LibC::Char** @@ -361,6 +352,7 @@ lib LibLsquic fun engine_earliest_adv_tick = lsquic_engine_earliest_adv_tick(engine : EngineT, diff : LibC::Int*) : LibC::Int fun engine_count_attq = lsquic_engine_count_attq(engine : EngineT, from_now : LibC::Int) : LibC::UInt fun conn_status = lsquic_conn_status(x0 : ConnT, errbuf : LibC::Char*, bufsz : LibC::SizeT) : ConnStatus + enum ConnStatus LsconnStHskInProgress = 0 LsconnStConnected = 1 @@ -373,5 +365,6 @@ lib LibLsquic LsconnStClosed = 8 LsconnStPeerGoingAway = 9 end - $ver2str : LibC::Char*[6] + + $lsquic_ver2str : LibC::Char*[7] end diff --git a/src/lsquic/patch.cr b/src/lsquic/patch.cr index 12e5b09..a3889ec 100644 --- a/src/lsquic/patch.cr +++ b/src/lsquic/patch.cr @@ -1,45 +1,30 @@ require "openssl" -@[Link(ldflags: "#{__DIR__}/ext/liblsquic.a #{__DIR__}/ext/libcrypto.a")] lib LibCrypto fun evp_ripemd160 = EVP_sha1 : EVP_MD fun sk_free = sk_free(st : Void*) fun sk_num = sk_num(x0 : Void*) : Int fun sk_pop_free = sk_pop_free(st : Void*, callback : (Void*) ->) fun sk_value = sk_value(x0 : Void*, x1 : Int) : Void* + + fun openssl_add_all_algorithms = rand : LibC::Int + fun err_load_crypto_strings = rand : LibC::Int end -@[Link(ldflags: "#{__DIR__}/ext/libssl.a")] lib LibSSL + {% ssl_version = "1.1.0" %} + fun ssl_set_tlsext_host_name = SSL_set_tlsext_host_name(handle : SSL, name : Char*) : Long fun ssl_ctx_set_tmp_ecdh = SSL_CTX_set_tmp_ecdh(ctx : SSLContext, parg : Void*) : ULong fun ssl_ctx_get_mode = SSL_CTX_get_mode(ctx : SSLContext) : ULong fun ssl_ctx_set_mode = SSL_CTX_set_mode(ctx : SSLContext, mode : ULong) : ULong fun ssl_ctx_clear_mode = SSL_CTX_clear_mode(ctx : SSLContext, mode : ULong) : ULong -end + fun ssl_ctx_get_options = SSL_CTX_get_options(ctx : SSLContext) : ULong + fun ssl_ctx_set_options = SSL_CTX_set_options(ctx : SSLContext, larg : ULong) : ULong + fun ssl_ctx_clear_options = SSL_CTX_clear_options(ctx : SSLContext, larg : ULong) : ULong -abstract class OpenSSL::SSL::Context - def set_tmp_ecdh_key(curve = LibCrypto::NID_X9_62_prime256v1) - key = LibCrypto.ec_key_new_by_curve_name(curve) - raise OpenSSL::Error.new("ec_key_new_by_curve_name") if key.null? - LibSSL.ssl_ctx_set_tmp_ecdh(@handle, key) - LibCrypto.ec_key_free(key) - end - - # Returns the current modes set on the TLS context. - def modes - OpenSSL::SSL::Modes.new LibSSL.ssl_ctx_get_mode(@handle) - end - - # Adds modes to the TLS context. - def add_modes(mode : OpenSSL::SSL::Modes) - OpenSSL::SSL::Modes.new LibSSL.ssl_ctx_set_mode(@handle, mode) - end - - # Removes modes from the TLS context. - def remove_modes(mode : OpenSSL::SSL::Modes) - OpenSSL::SSL::Modes.new LibSSL.ssl_ctx_clear_mode(@handle, mode) - end + fun ssl_library_init = rand : LibC::Int + fun ssl_load_error_strings = rand : LibC::Int end struct OpenSSL::BIO @@ -141,6 +126,43 @@ struct OpenSSL::BIO end end +abstract class OpenSSL::SSL::Context + def set_tmp_ecdh_key(curve = LibCrypto::NID_X9_62_prime256v1) + key = LibCrypto.ec_key_new_by_curve_name(curve) + raise OpenSSL::Error.new("ec_key_new_by_curve_name") if key.null? + LibSSL.ssl_ctx_set_tmp_ecdh(@handle, key) + LibCrypto.ec_key_free(key) + end + + # Returns the current modes set on the TLS context. + def modes + OpenSSL::SSL::Modes.new LibSSL.ssl_ctx_get_mode(@handle) + end + + # Adds modes to the TLS context. + def add_modes(mode : OpenSSL::SSL::Modes) + OpenSSL::SSL::Modes.new LibSSL.ssl_ctx_set_mode(@handle, mode) + end + + # Removes modes from the TLS context. + def remove_modes(mode : OpenSSL::SSL::Modes) + OpenSSL::SSL::Modes.new LibSSL.ssl_ctx_clear_mode(@handle, mode) + end + + # Returns the current options set on the TLS context. + def options + OpenSSL::SSL::Options.new LibSSL.ssl_ctx_get_options(@handle) + end + + def add_options(options : OpenSSL::SSL::Options) + OpenSSL::SSL::Options.new LibSSL.ssl_ctx_set_options(@handle, options) + end + + def remove_options(options : OpenSSL::SSL::Options) + OpenSSL::SSL::Options.new LibSSL.ssl_ctx_clear_options(@handle, options) + end +end + abstract class OpenSSL::SSL::Socket < IO class Client < Socket def initialize(io, context : Context::Client = Context::Client.new, sync_close : Bool = false, hostname : String? = nil)