Compare commits

...

26 commits

Author SHA1 Message Date
Omar Roth
de2c945a2e Bump version 2020-03-06 12:54:42 -06:00
Omar Roth
60c4020fd9 Correctly rescue closed socket 2020-03-06 12:54:14 -06:00
Omar Roth
53c90d4b1c
Bump version 2019-12-14 16:06:48 -05:00
Omar Roth
d8b1b5077f
Keep reference so GC doesn't collect stream_ctx 2019-12-14 16:06:12 -05:00
Omar Roth
6cbead5185
Bump version 2019-11-28 08:56:37 -05:00
Omar Roth
037cd3a47d
Fix binding for IPv6 2019-11-28 08:56:01 -05:00
Omar Roth
9049f7ec29 Bump version 2019-11-27 11:54:08 -06:00
Omar Roth
fea2b19a63 Add specs 2019-11-27 11:53:45 -06:00
Omar Roth
a88e21b222 Add support for specifying family 2019-11-27 11:53:14 -06:00
Omar Roth
fa1c9d8814
Bump version 2019-11-24 15:25:35 -05:00
Omar Roth
714461074a
Catch closed stream in readf 2019-11-24 14:24:45 -05:00
Omar Roth
15a67d48a0
Use buffered write 2019-11-24 13:57:21 -05:00
Omar Roth
8825f4741d
Bump version 2019-11-24 13:37:13 -05:00
Omar Roth
0219ab13d3
Replace IO::FileDescriptor with IO::ChanneledPipe 2019-11-24 13:22:08 -05:00
Omar Roth
2ebf1fc9ac
Catch exception in on_close 2019-11-23 18:16:28 -05:00
Omar Roth
c0ba00560f
Catch exception in on_read 2019-11-23 18:11:54 -05:00
Omar Roth
125a547a4c
Handle engine in separate fiber 2019-11-23 17:35:46 -05:00
Omar Roth
73903fa7e1
Bump version 2019-11-18 15:26:36 -05:00
Omar Roth
3a19eac5bd
Add patch for binding on musl 2019-11-18 15:21:18 -05:00
Omar Roth
0421bbed1c
Change default user-agent 2019-11-18 14:49:28 -05:00
Omar Roth
5b9a26d5ec
Undefine missing functions 2019-11-18 14:43:37 -05:00
Omar Roth
7fe65a03ea
Update static libs and bump version 2019-11-16 16:08:27 -05:00
Omar Roth
75937aee70
Fix local variable stream_ctx 2019-11-16 14:50:07 -05:00
Omar Roth
96d761aa1e
Bump version 2019-11-16 14:43:21 -05:00
Omar Roth
1dc92cd18e
Fix require 2019-11-16 14:42:28 -05:00
Omar Roth
21fb20c02b
Add licenses and update README 2019-11-16 14:15:26 -05:00
14 changed files with 874 additions and 415 deletions

251
LICENSE.boringssl Normal file
View file

@ -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.

32
LICENSE.chrome Normal file
View file

@ -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.

21
LICENSE.lsquic Normal file
View file

@ -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.

View file

@ -2,6 +2,12 @@
Crystal bindings to the excellent [LSQUIC](https://github.com/litespeedtech/lsquic) library. 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 ## Installation
1. Add the dependency to your `shard.yml`: 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` 2. Run `shards install`
## Usage
```crystal
require "lsquic"
client = QUIC::Client.new("www.youtube.com")
client.get("/") # => #<HTTP::Client::Response>
client.get("/", headers: HTTP::Headers{
"cookie" => "Some value",
# ...
}) # => #<HTTP::Client::Response>
```
## Contributing ## Contributing
1. Fork it (<https://github.com/omarroth/lsquic.cr/fork>) 1. Fork it (<https://github.com/omarroth/lsquic.cr/fork>)
@ -25,3 +46,7 @@ Crystal bindings to the excellent [LSQUIC](https://github.com/litespeedtech/lsqu
## Contributors ## Contributors
- [Omar Roth](https://github.com/omarroth) - creator and maintainer - [Omar Roth](https://github.com/omarroth) - creator and maintainer
```
```

View file

@ -1,9 +1,9 @@
name: lsquic name: lsquic
version: 0.1.0 version: 0.1.9
authors: authors:
- Omar Roth <omarroth@protonmail.com> - Omar Roth <omarroth@protonmail.com>
crystal: 0.31.1 crystal: 0.33.0
license: MIT license: MIT

View file

@ -1,9 +1,42 @@
require "./spec_helper" require "./spec_helper"
describe Lsquic do describe QUIC do
# TODO: Write tests
it "works" 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
end end

View file

@ -1,7 +1,5 @@
require "./lsquic/*" require "./lsquic/*"
require "socket"
module QUIC module QUIC
VERSION = "0.1.0" VERSION = "#{LibLsquic::MAJOR_VERSION}.#{LibLsquic::MINOR_VERSION}.#{LibLsquic::PATCH_VERSION}"
QUIC_VERSION = "#{LibLsquic::MAJOR_VERSION}.#{LibLsquic::MINOR_VERSION}.#{LibLsquic::PATCH_VERSION}"
end end

View file

@ -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

View file

@ -1,98 +1,48 @@
require "http/headers" require "http"
require "http/client" require "socket"
require "socket/udp_socket"
struct QUIC::PeerCtx module QUIC
property socket : UDPSocket class StreamCtx
property request : HTTP::Request
property io : IO::ChanneledPipe
def initialize(@socket) def initialize(@request, @io)
end
end end
def local_address class Client
@socket.local_address def self.stream_readf(stream_if_ctx : Void*, buf : UInt8*, buf_len : LibC::SizeT, fin : LibC::Int)
end stream_ctx = Box(StreamCtx).unbox(stream_if_ctx)
stream_ctx.io.write Slice.new(buf, buf_len)
def remote_address buf_len
@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)
end end
@stream_if.on_new_stream = ->(stream_if_ctx : Void*, s : LibLsquic::StreamT) do def self.on_new_conn(stream_if_ctx : Void*, c : LibLsquic::ConnT)
if LibLsquic.stream_is_pushed(s) != 0
return stream_if_ctx
end
LibLsquic.stream_wantwrite(s, 1)
stream_if_ctx stream_if_ctx
end end
@stream_if.on_write = ->(s : LibLsquic::StreamT, stream_if_ctx : Void*) do def self.on_conn_closed(c : LibLsquic::ConnT)
request = Box(StreamCtx).unbox(stream_if_ctx).requests.shift Box.box(nil)
raise "No request" if !request 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 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, values = tuple
name = name.downcase name = name.downcase
@ -117,11 +67,9 @@ class QUIC::Client
http_headers.count = headers.size http_headers.count = headers.size
http_headers.headers = headers.to_unsafe 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), stream_ctx.request.body ? 0 : 1) != 0
raise "Could not send headers" if LibLsquic.stream_send_headers(s, pointerof(http_headers), request.body ? 0 : 1) != 0
if request.body if body = stream_ctx.request.body.try &.gets_to_end
body = request.body.not_nil!.gets_to_end
LibLsquic.stream_write(s, body, body.bytesize) LibLsquic.stream_write(s, body, body.bytesize)
LibLsquic.stream_flush(s) LibLsquic.stream_flush(s)
end end
@ -130,140 +78,254 @@ class QUIC::Client
LibLsquic.stream_wantwrite(s, 0) LibLsquic.stream_wantwrite(s, 0)
LibLsquic.stream_wantread(s, 1) LibLsquic.stream_wantread(s, 1)
stream_if_ctx Box.box(stream_ctx)
end 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) 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 if bytes_read > 0
stream_ctx.io.write buffer[0, bytes_read] # Nothing
elsif bytes_read == 0 elsif bytes_read == 0
LibLsquic.stream_shutdown(s, 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) LibLsquic.stream_close(s)
else else
raise "Could not read stream" # raise "Could not read response"
end end
stream_if_ctx stream_if_ctx
end end
# TODO: Allow engine to break with existing connections def self.on_close(s : LibLsquic::StreamT, stream_if_ctx : Void*)
@stream_if.on_close = ->(s : LibLsquic::StreamT, stream_if_ctx : Void*) do stream_ctx = Box(StreamCtx).unbox(stream_if_ctx)
LibLsquic.conn_close(LibLsquic.stream_conn(s)) stream_ctx.io.close
GC.free stream_if_ctx
stream_if_ctx stream_if_ctx
end end
@engine_api = LibLsquic::EngineApi.new def self.ea_packets_out(peer_ctx : Void*, specs : LibLsquic::OutSpec*, count : LibC::UInt)
@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
packets_out = 0 packets_out = 0
count.times do |i| count.times do |i|
spec = specs[i] spec = specs[i]
peer_ctx = Box(PeerCtx).unbox(spec.peer_ctx) socket = Box(UDPSocket).unbox(spec.peer_ctx)
spec.iovlen.times do |j| spec.iovlen.times do |j|
iov = spec.iov[j] iov = spec.iov[j]
begin 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 packets_out += 1
rescue ex rescue ex
break
end end
end end
end end
packets_out packets_out
end end
end
private def check_host_only(string : String) ENGINE_FLAGS = LibLsquic::LSENG_HTTP
# When parsing a URI with just a host LibLsquic.global_init(ENGINE_FLAGS & LibLsquic::LSENG_SERVER ? LibLsquic::GLOBAL_SERVER : LibLsquic::GLOBAL_CLIENT)
# we end up with a URI with just a path
uri = URI.parse(string) property family : Socket::Family = Socket::Family::INET
if uri.scheme || uri.host || uri.port || uri.query || uri.user || uri.password || uri.path.includes?('/')
# 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) raise_invalid_host(string)
end end
rescue URI::Error
raise_invalid_host(string)
end
private def raise_invalid_host(string : String) 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}") 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
end end
end
def self.new(host : String, port = nil, tls = false) def self.new(uri : URI, tls = nil)
client = new(host, port, tls) tls = tls_flag(uri, tls)
begin host = validate_host(uri)
yield client new(host, uri.port, tls)
ensure
client.close
end end
end
# Configures this client to perform basic authentication in every def self.new(uri : URI, tls = nil)
# request. tls = tls_flag(uri, tls)
def basic_auth(username, password) host = validate_host(uri)
header = "Basic #{Base64.strict_encode("#{username}:#{password}")}" client = new(host, uri.port, tls)
before_request do |request| begin
request.headers["Authorization"] = header yield client
ensure
client.close
end
end end
end
def read_timeout=(read_timeout : Number) def self.new(host : String, port = nil, tls = false)
@read_timeout = read_timeout.to_f client = new(host, port, tls)
end begin
yield client
ensure
client.close
end
end
def read_timeout=(read_timeout : Time::Span) # Configures this client to perform basic authentication in every
self.read_timeout = read_timeout.total_seconds # request.
end 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) def read_timeout=(read_timeout : Number)
@connect_timeout = connect_timeout.to_f @read_timeout = read_timeout.to_f
end end
def connect_timeout=(connect_timeout : Time::Span) def read_timeout=(read_timeout : Time::Span)
self.connect_timeout = connect_timeout.total_seconds self.read_timeout = read_timeout.total_seconds
end end
def dns_timeout=(dns_timeout : Number) def connect_timeout=(connect_timeout : Number)
@dns_timeout = dns_timeout.to_f @connect_timeout = connect_timeout.to_f
end end
def dns_timeout=(dns_timeout : Time::Span) def connect_timeout=(connect_timeout : Time::Span)
self.dns_timeout = dns_timeout.total_seconds self.connect_timeout = connect_timeout.total_seconds
end end
def before_request(&callback : HTTP::Request ->) def dns_timeout=(dns_timeout : Number)
before_request = @before_request ||= [] of (HTTP::Request ->) @dns_timeout = dns_timeout.to_f
before_request << callback end
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 def {{method.id}}(path, headers : HTTP::Headers? = nil, body : BodyType = nil) : HTTP::Client::Response
exec {{method.upcase}}, path, headers, body exec {{method.upcase}}, path, headers, body
end end
@ -325,252 +387,185 @@ class QUIC::Client
end end
{% end %} {% end %}
def exec(request : HTTP::Request) : HTTP::Client::Response def exec(request : HTTP::Request) : HTTP::Client::Response
exec_internal(request) 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
end end
end
private def exec_internal(request, &block : Response -> T) : T forall T private def exec_internal(request)
exec_internal_single(request) do |response| response = exec_internal_single(request)
if response return handle_response(response) if response
return handle_response(response) { yield response }
end
# Server probably closed the connection, so retry once raise "Unexpected end of http response"
close
request.body.try &.rewind
exec_internal_single(request) do |response|
if response
return handle_response(response) do
yield response
end
end
end
end end
raise "Unexpected end of http response"
end
private def exec_internal_single(request) private def exec_internal_single(request)
send_request(request) io = send_request(request)
HTTP::Client::Response.from_io?(stream_ctx.io, ignore_body: request.ignore_body?) do |response| HTTP::Client::Response.from_io?(io, ignore_body: request.ignore_body?)
yield response
end end
end
private def handle_response(response) private def handle_response(response)
value = yield # close unless response.keep_alive?
response.body_io?.try &.close response
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
end end
end
def self.exec(method, url : String | URI, headers : HTTP::Headers? = nil, body : BodyType = nil, tls = nil) : HTTP::Client::Response def exec(request : HTTP::Request, &block)
headers = default_one_shot_headers(headers) exec_internal(request) do |response|
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 yield response
end end
end end
end
def close private def exec_internal(request, &block : HTTP::Client::Response -> T) : T forall T
# @conn.try { |c| LibLsquic.conn_close(c) } exec_internal_single(request) do |response|
@conn = nil if response
end return handle_response(response) { yield response }
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
end end
raise "Unexpected end of http response"
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)
end end
end
private def peer_ctx private def exec_internal_single(request)
peer_ctx = @peer_ctx io = send_request(request)
return peer_ctx if peer_ctx HTTP::Client::Response.from_io?(io, ignore_body: request.ignore_body?) do |response|
yield response
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
end end
end end
socket.sync = false
peer_ctx = PeerCtx.new(socket) private def handle_response(response)
@peer_ctx = peer_ctx value = yield
end response.body_io?.try &.close
# close unless response.keep_alive?
def conn value
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}")
end end
exec(uri, tls) do |client, path| private def send_request(request)
yield client, path 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
end
protected def self.tls_flag(uri, context : OpenSSL::SSL::Context::Client?) private def set_defaults(request)
scheme = uri.scheme request.headers[":method"] ||= request.method
case {scheme, context} request.headers[":scheme"] ||= "https"
when {nil, _} request.headers[":path"] ||= request.resource
raise ArgumentError.new("Missing scheme: #{uri}") request.headers[":authority"] ||= host_header
when {"http", nil} 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"
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
end
protected def self.validate_host(uri) private def self.default_one_shot_headers(headers)
host = uri.host headers ||= HTTP::Headers.new
return host if host && !host.empty? headers["connection"] ||= "close"
headers
end
raise ArgumentError.new %(Request URI must have host (URI is: #{uri})) private def run_before_request_callbacks(request)
end @before_request.try &.each &.call(request)
end
private def self.exec(uri : URI, tls = nil) def exec(method : String, path, headers : HTTP::Headers? = nil, body : BodyType = nil) : HTTP::Client::Response
tls = tls_flag(uri, tls) exec new_request method, path, headers, body
host = validate_host(uri) end
port = uri.port def exec(method : String, path, headers : HTTP::Headers? = nil, body : BodyType = nil)
path = uri.full_path exec(new_request(method, path, headers, body)) do |response|
user = uri.user yield response
password = uri.password end
end
HTTP::Client.new(host, port, tls) do |client|
if user && password def self.exec(method, url : String | URI, headers : HTTP::Headers? = nil, body : BodyType = nil, tls = nil) : HTTP::Client::Response
client.basic_auth(user, password) 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 end
yield client, path
end end
end end
end end

BIN
src/lsquic/ext/libcrypto.a Normal file

Binary file not shown.

BIN
src/lsquic/ext/liblsquic.a Normal file

Binary file not shown.

BIN
src/lsquic/ext/libssl.a Normal file

Binary file not shown.

View file

@ -67,10 +67,8 @@ lib LibLsquic
id : Uint64T id : Uint64T
end end
alias X__Uint8T = UInt8 alias Uint8T = UInt8
alias Uint8T = X__Uint8T alias Uint64T = LibC::ULong
alias X__Uint64T = LibC::ULong
alias Uint64T = X__Uint64T
alias Engine = Void alias Engine = Void
alias Conn = Void alias Conn = Void
alias ConnCtx = Void alias ConnCtx = Void
@ -108,9 +106,7 @@ lib LibLsquic
end end
type ConnT = Void* type ConnT = Void*
type ConnCtxT = Void*
type StreamT = Void* type StreamT = Void*
type StreamCtxT = Void*
enum HskStatus enum HskStatus
LsqHskFail = 0 LsqHskFail = 0
LsqHskOk = 1 LsqHskOk = 1
@ -165,8 +161,7 @@ lib LibLsquic
es_cc_algo : LibC::UInt es_cc_algo : LibC::UInt
end end
alias X__Uint32T = LibC::UInt alias Uint32T = LibC::UInt
alias Uint32T = X__Uint32T
fun engine_init_settings = lsquic_engine_init_settings(x0 : EngineSettings*, engine_flags : 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 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) shi_lookup : (Void*, Void*, LibC::UInt, Void**, LibC::UInt* -> LibC::Int)
end end
alias X__TimeT = LibC::Long alias TimeT = LibC::Long
alias TimeT = X__TimeT
struct PackoutMemIf struct PackoutMemIf
pmi_allocate : (Void*, Void*, LibC::UShort, LibC::Char -> Void*) 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_send_unsent_packets = lsquic_engine_send_unsent_packets(engine : EngineT)
fun engine_destroy = lsquic_engine_destroy(x0 : 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_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_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_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_going_away = lsquic_conn_going_away(x0 : ConnT)
fun conn_close = lsquic_conn_close(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_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 fun stream_read = lsquic_stream_read(s : StreamT, buf : Void*, len : LibC::SizeT) : SsizeT
alias X__SsizeT = LibC::Long alias SsizeT = LibC::Long
alias SsizeT = X__SsizeT
fun stream_readv = lsquic_stream_readv(s : StreamT, x1 : Iovec*, iovcnt : LibC::Int) : 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_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 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 conn_get_server_cert_chain = lsquic_conn_get_server_cert_chain(x0 : ConnT) : StackStX509*
fun stream_id = lsquic_stream_id(s : StreamT) : StreamIdT fun stream_id = lsquic_stream_id(s : StreamT) : StreamIdT
alias StreamIdT = Uint64T 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_pushed = lsquic_stream_is_pushed(s : StreamT) : LibC::Int
fun stream_is_rejected = lsquic_stream_is_rejected(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 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 engine_cooldown = lsquic_engine_cooldown(x0 : EngineT)
fun hsk_getssl = lsquic_hsk_getssl(conn : ConnT) : SslSt* fun hsk_getssl = lsquic_hsk_getssl(conn : ConnT) : SslSt*
alias SslSt = Void alias SslSt = Void
fun conn_get_ctx = lsquic_conn_get_ctx(x0 : ConnT) : ConnCtxT fun conn_get_ctx = lsquic_conn_get_ctx(x0 : ConnT) : Void*
fun conn_set_ctx = lsquic_conn_set_ctx(x0 : ConnT, x1 : ConnCtxT) 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_get_peer_ctx = lsquic_conn_get_peer_ctx(x0 : ConnT, local_sa : LibC::Sockaddr*) : Void*
fun conn_abort = lsquic_conn_abort(x0 : ConnT) 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_alt_svc_versions = lsquic_get_alt_svc_versions(versions : LibC::UInt) : LibC::Char*

View file

@ -7,6 +7,9 @@ lib LibCrypto
fun sk_num = sk_num(x0 : Void*) : Int fun sk_num = sk_num(x0 : Void*) : Int
fun sk_pop_free = sk_pop_free(st : Void*, callback : (Void*) ->) fun sk_pop_free = sk_pop_free(st : Void*, callback : (Void*) ->)
fun sk_value = sk_value(x0 : Void*, x1 : Int) : 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 end
@[Link(ldflags: "#{__DIR__}/ext/libssl.a")] @[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_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_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_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 end
abstract class OpenSSL::SSL::Context abstract class OpenSSL::SSL::Context
@ -40,6 +49,19 @@ abstract class OpenSSL::SSL::Context
def remove_modes(mode : OpenSSL::SSL::Modes) def remove_modes(mode : OpenSSL::SSL::Modes)
OpenSSL::SSL::Modes.new LibSSL.ssl_ctx_clear_mode(@handle, mode) OpenSSL::SSL::Modes.new LibSSL.ssl_ctx_clear_mode(@handle, mode)
end 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 end
struct OpenSSL::BIO struct OpenSSL::BIO