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..2829305 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ Crystal bindings to the excellent [LSQUIC](https://github.com/litespeedtech/lsquic) library. +`libssl.a`, `libcrypto.a` are both licensed under `LICENSE.boringssl`. + +`liblsquic.a` is licensed under `LICENSE.lsquic` and `LICENSE.chrome`. + +This library is available under the MIT license. + ## Installation 1. Add the dependency to your `shard.yml`: @@ -14,6 +20,21 @@ Crystal bindings to the excellent [LSQUIC](https://github.com/litespeedtech/lsqu 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 () @@ -25,3 +46,7 @@ Crystal bindings to the excellent [LSQUIC](https://github.com/litespeedtech/lsqu ## Contributors - [Omar Roth](https://github.com/omarroth) - creator and maintainer + +``` + +``` diff --git a/shard.yml b/shard.yml index dc4f372..3ae6ddc 100644 --- a/shard.yml +++ b/shard.yml @@ -1,9 +1,9 @@ name: lsquic -version: 0.1.0 +version: 0.1.9 authors: - Omar Roth -crystal: 0.31.1 +crystal: 0.33.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..b7cd4e3 100644 --- a/src/lsquic/client.cr +++ b/src/lsquic/client.cr @@ -1,98 +1,48 @@ -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 - - 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) + class Client + 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 + + 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) } + + if LibLsquic.stream_is_pushed(s) != 0 + return Box.box(stream_ctx) + end + + LibLsquic.stream_wantwrite(s, 1) + Box.box(stream_ctx) + end + + def self.on_write(s : LibLsquic::StreamT, stream_if_ctx : Void*) + stream_ctx = Box(StreamCtx).unbox(stream_if_ctx) headers = [] of LibLsquic::HttpHeader - (request.headers.to_a.sort_by { |k, v| {":authority", ":path", ":scheme", ":method"}.index(k) || -1 }).reverse.each do |tuple| + (stream_ctx.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 @@ -117,11 +67,9 @@ class QUIC::Client http_headers.count = headers.size http_headers.headers = headers.to_unsafe - # 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 + raise "Could not send headers" if LibLsquic.stream_send_headers(s, pointerof(http_headers), stream_ctx.request.body ? 0 : 1) != 0 - if request.body - body = request.body.not_nil!.gets_to_end + if body = stream_ctx.request.body.try &.gets_to_end LibLsquic.stream_write(s, body, body.bytesize) LibLsquic.stream_flush(s) end @@ -130,140 +78,254 @@ 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/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 = ->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::Lsqver046, 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) + end + + buffer = Bytes.new(0x600) + loop do + begin + bytes_read = socket.read buffer + rescue ex + break + end + 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 + @socket.try &.close + @socket = nil + end + + def socket : UDPSocket + return @socket.as(UDPSocket) 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 +387,185 @@ 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 + @socket.try &.close + @socket = nil + 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/libcrypto.a b/src/lsquic/ext/libcrypto.a new file mode 100644 index 0000000..755fa5f Binary files /dev/null and b/src/lsquic/ext/libcrypto.a differ diff --git a/src/lsquic/ext/liblsquic.a b/src/lsquic/ext/liblsquic.a new file mode 100644 index 0000000..ad85a26 Binary files /dev/null and b/src/lsquic/ext/liblsquic.a differ diff --git a/src/lsquic/ext/libssl.a b/src/lsquic/ext/libssl.a new file mode 100644 index 0000000..1b1974b Binary files /dev/null and b/src/lsquic/ext/libssl.a differ diff --git a/src/lsquic/liblsquic.cr b/src/lsquic/liblsquic.cr index 817fdf1..fbee2d0 100644 --- a/src/lsquic/liblsquic.cr +++ b/src/lsquic/liblsquic.cr @@ -67,10 +67,8 @@ lib LibLsquic id : Uint64T end - alias X__Uint8T = UInt8 - alias Uint8T = X__Uint8T - alias X__Uint64T = LibC::ULong - alias Uint64T = X__Uint64T + alias Uint8T = UInt8 + alias Uint64T = LibC::ULong alias Engine = Void alias Conn = Void alias ConnCtx = Void @@ -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 @@ -165,8 +161,7 @@ lib LibLsquic es_cc_algo : LibC::UInt end - alias X__Uint32T = LibC::UInt - alias Uint32T = X__Uint32T + alias Uint32T = LibC::UInt 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 +180,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*) @@ -271,15 +265,14 @@ lib LibLsquic 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 + alias SsizeT = LibC::Long 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_wantwrite = lsquic_stream_wantwrite(s : StreamT, is_want : LibC::Int) : LibC::Int @@ -304,7 +297,7 @@ lib LibLsquic 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 + 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 @@ -350,8 +343,8 @@ lib LibLsquic 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_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 : LibC::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* diff --git a/src/lsquic/patch.cr b/src/lsquic/patch.cr index 12e5b09..11b407e 100644 --- a/src/lsquic/patch.cr +++ b/src/lsquic/patch.cr @@ -7,6 +7,9 @@ lib LibCrypto 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")] @@ -16,6 +19,12 @@ lib LibSSL 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 + 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 + + fun ssl_library_init = rand : LibC::Int + fun ssl_load_error_strings = rand : LibC::Int end abstract class OpenSSL::SSL::Context @@ -40,6 +49,19 @@ abstract class OpenSSL::SSL::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 struct OpenSSL::BIO