Fetching https
This commit is contained in:
parent
1c19a807d9
commit
25fc919913
18
util/https-fetch/Cargo.toml
Normal file
18
util/https-fetch/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
description = "HTTPS fetching library"
|
||||
homepage = "http://ethcore.io"
|
||||
license = "GPL-3.0"
|
||||
name = "https-fetch"
|
||||
version = "0.1.0"
|
||||
authors = ["Ethcore <admin@ethcore.io>"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.3"
|
||||
mio = { git = "https://github.com/ethcore/mio", branch = "v0.5.x" }
|
||||
rustls = { git = "https://github.com/ctz/rustls" }
|
||||
clippy = { version = "0.0.85", optional = true}
|
||||
|
||||
[features]
|
||||
default = []
|
||||
ca-github-only = []
|
||||
dev = ["clippy"]
|
14
util/https-fetch/examples/fetch.rs
Normal file
14
util/https-fetch/examples/fetch.rs
Normal file
@ -0,0 +1,14 @@
|
||||
extern crate https_fetch;
|
||||
|
||||
use std::io;
|
||||
use https_fetch::*;
|
||||
|
||||
fn main() {
|
||||
let client = Client::new().unwrap();
|
||||
|
||||
let rx = client.fetch(Url::new("github.com", 443, "/").unwrap(), Box::new(io::stdout())).unwrap();
|
||||
|
||||
let result = rx.recv().unwrap();
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
4496
util/https-fetch/src/ca-certificates.crt
Normal file
4496
util/https-fetch/src/ca-certificates.crt
Normal file
File diff suppressed because it is too large
Load Diff
93
util/https-fetch/src/ca-github.crt
Normal file
93
util/https-fetch/src/ca-github.crt
Normal file
@ -0,0 +1,93 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
|
||||
ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL
|
||||
MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
|
||||
LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
|
||||
RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm
|
||||
+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW
|
||||
PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM
|
||||
xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB
|
||||
Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3
|
||||
hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg
|
||||
EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF
|
||||
MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA
|
||||
FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec
|
||||
nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z
|
||||
eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF
|
||||
hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2
|
||||
Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
|
||||
vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
|
||||
+OkuE6N36B9K
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUgkmFf4msdgzANBgkqhkiG9w0BAQsFADBs
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
|
||||
ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowdTEL
|
||||
MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
|
||||
LmRpZ2ljZXJ0LmNvbTE0MDIGA1UEAxMrRGlnaUNlcnQgU0hBMiBFeHRlbmRlZCBW
|
||||
YWxpZGF0aW9uIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
||||
ggEBANdTpARR+JmmFkhLZyeqk0nQOe0MsLAAh/FnKIaFjI5j2ryxQDji0/XspQUY
|
||||
uD0+xZkXMuwYjPrxDKZkIYXLBxA0sFKIKx9om9KxjxKws9LniB8f7zh3VFNfgHk/
|
||||
LhqqqB5LKw2rt2O5Nbd9FLxZS99RStKh4gzikIKHaq7q12TWmFXo/a8aUGxUvBHy
|
||||
/Urynbt/DvTVvo4WiRJV2MBxNO723C3sxIclho3YIeSwTQyJ3DkmF93215SF2AQh
|
||||
cJ1vb/9cuhnhRctWVyh+HA1BV6q3uCe7seT6Ku8hI3UarS2bhjWMnHe1c63YlC3k
|
||||
8wyd7sFOYn4XwHGeLN7x+RAoGTMCAwEAAaOCAUkwggFFMBIGA1UdEwEB/wQIMAYB
|
||||
Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
|
||||
BQcDAjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
|
||||
Z2ljZXJ0LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsNC5kaWdpY2Vy
|
||||
dC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2
|
||||
MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j
|
||||
b20vQ1BTMB0GA1UdDgQWBBQ901Cl1qCt7vNKYApl0yHU+PjWDzAfBgNVHSMEGDAW
|
||||
gBSxPsNpA/i/RwHUmCYaCALvY2QrwzANBgkqhkiG9w0BAQsFAAOCAQEAnbbQkIbh
|
||||
hgLtxaDwNBx0wY12zIYKqPBKikLWP8ipTa18CK3mtlC4ohpNiAexKSHc59rGPCHg
|
||||
4xFJcKx6HQGkyhE6V6t9VypAdP3THYUYUN9XR3WhfVUgLkc3UHKMf4Ib0mKPLQNa
|
||||
2sPIoc4sUqIAY+tzunHISScjl2SFnjgOrWNoPLpSgVh5oywM395t6zHyuqB8bPEs
|
||||
1OG9d4Q3A84ytciagRpKkk47RpqF/oOi+Z6Mo8wNXrM9zwR4jxQUezKcxwCmXMS1
|
||||
oVWNWlZopCJwqjyBcdmdqEU79OX2olHdx3ti6G8MdOu42vi/hw15UJGQmxg7kVkn
|
||||
8TUoE6smftX3eg==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIHeTCCBmGgAwIBAgIQC/20CQrXteZAwwsWyVKaJzANBgkqhkiG9w0BAQsFADB1
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk
|
||||
IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE2MDMxMDAwMDAwMFoXDTE4MDUxNzEy
|
||||
MDAwMFowgf0xHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYB
|
||||
BAGCNzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMRAwDgYDVQQF
|
||||
Ewc1MTU3NTUwMSQwIgYDVQQJExs4OCBDb2xpbiBQIEtlbGx5LCBKciBTdHJlZXQx
|
||||
DjAMBgNVBBETBTk0MTA3MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHViLCBJbmMu
|
||||
MRMwEQYDVQQDEwpnaXRodWIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEA54hc8pZclxgcupjiA/F/OZGRwm/ZlucoQGTNTKmBEgNsrn/mxhngWmPw
|
||||
bAvUaLP//T79Jc+1WXMpxMiz9PK6yZRRFuIo0d2bx423NA6hOL2RTtbnfs+y0PFS
|
||||
/YTpQSelTuq+Fuwts5v6aAweNyMcYD0HBybkkdosFoDccBNzJ92Ac8I5EVDUc3Or
|
||||
/4jSyZwzxu9kdmBlBzeHMvsqdH8SX9mNahXtXxRpwZnBiUjw36PgN+s9GLWGrafd
|
||||
02T0ux9Yzd5ezkMxukqEAQ7AKIIijvaWPAJbK/52XLhIy2vpGNylyni/DQD18bBP
|
||||
T+ZG1uv0QQP9LuY/joO+FKDOTler4wIDAQABo4IDejCCA3YwHwYDVR0jBBgwFoAU
|
||||
PdNQpdagre7zSmAKZdMh1Pj41g8wHQYDVR0OBBYEFIhcSGcZzKB2WS0RecO+oqyH
|
||||
IidbMCUGA1UdEQQeMByCCmdpdGh1Yi5jb22CDnd3dy5naXRodWIuY29tMA4GA1Ud
|
||||
DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdQYDVR0f
|
||||
BG4wbDA0oDKgMIYuaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItZXYtc2Vy
|
||||
dmVyLWcxLmNybDA0oDKgMIYuaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTIt
|
||||
ZXYtc2VydmVyLWcxLmNybDBLBgNVHSAERDBCMDcGCWCGSAGG/WwCATAqMCgGCCsG
|
||||
AQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAcGBWeBDAEBMIGI
|
||||
BggrBgEFBQcBAQR8MHowJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0
|
||||
LmNvbTBSBggrBgEFBQcwAoZGaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0Rp
|
||||
Z2lDZXJ0U0hBMkV4dGVuZGVkVmFsaWRhdGlvblNlcnZlckNBLmNydDAMBgNVHRMB
|
||||
Af8EAjAAMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkAdgCkuQmQtBhYFIe7E6LM
|
||||
Z3AKPDWYBPkb37jjd80OyA3cEAAAAVNhieoeAAAEAwBHMEUCIQCHHSEY/ROK2/sO
|
||||
ljbKaNEcKWz6BxHJNPOtjSyuVnSn4QIgJ6RqvYbSX1vKLeX7vpnOfCAfS2Y8lB5R
|
||||
NMwk6us2QiAAdgBo9pj4H2SCvjqM7rkoHUz8cVFdZ5PURNEKZ6y7T0/7xAAAAVNh
|
||||
iennAAAEAwBHMEUCIQDZpd5S+3to8k7lcDeWBhiJASiYTk2rNAT26lVaM3xhWwIg
|
||||
NUqrkIODZpRg+khhp8ag65B8mu0p4JUAmkRDbiYnRvYAdwBWFAaaL9fC7NP14b1E
|
||||
sj7HRna5vJkRXMDvlJhV1onQ3QAAAVNhieqZAAAEAwBIMEYCIQDnm3WStlvE99GC
|
||||
izSx+UGtGmQk2WTokoPgo1hfiv8zIAIhAPrYeXrBgseA9jUWWoB4IvmcZtshjXso
|
||||
nT8MIG1u1zF8MA0GCSqGSIb3DQEBCwUAA4IBAQCLbNtkxuspqycq8h1EpbmAX0wM
|
||||
5DoW7hM/FVdz4LJ3Kmftyk1yd8j/PSxRrAQN2Mr/frKeK8NE1cMji32mJbBqpWtK
|
||||
/+wC+avPplBUbNpzP53cuTMF/QssxItPGNP5/OT9Aj1BxA/NofWZKh4ufV7cz3pY
|
||||
RDS4BF+EEFQ4l5GY+yp4WJA/xSvYsTHWeWxRD1/nl62/Rd9FN2NkacRVozCxRVle
|
||||
FrBHTFxqIP6kDnxiLElBrZngtY07ietaYZVLQN/ETyqLQftsf8TecwTklbjvm8NT
|
||||
JqbaIVifYwqwNN+4lRxS3F5lNlA/il12IOgbRioLI62o8G0DaEUQgHNf8vSG
|
||||
-----END CERTIFICATE-----
|
185
util/https-fetch/src/client.rs
Normal file
185
util/https-fetch/src/client.rs
Normal file
@ -0,0 +1,185 @@
|
||||
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::str;
|
||||
use std::thread;
|
||||
use std::sync::mpsc;
|
||||
use std::io::{self, Write};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use mio;
|
||||
use tlsclient::{TlsClient, TlsClientError};
|
||||
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FetchError {
|
||||
InvalidAddress,
|
||||
ReadingCaCertificates,
|
||||
CaCertificates(io::Error),
|
||||
Io(io::Error),
|
||||
Notify(mio::NotifyError<ClientMessage>),
|
||||
Client(TlsClientError),
|
||||
}
|
||||
|
||||
impl From<io::Error> for FetchError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
FetchError::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mio::NotifyError<ClientMessage>> for FetchError {
|
||||
fn from(e: mio::NotifyError<ClientMessage>) -> Self {
|
||||
FetchError::Notify(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TlsClientError> for FetchError {
|
||||
fn from(e: TlsClientError) -> Self {
|
||||
FetchError::Client(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub type FetchResult = Result<(), FetchError>;
|
||||
|
||||
pub enum ClientMessage {
|
||||
Fetch(Url, Box<io::Write + Send>, mpsc::Sender<FetchResult>),
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
channel: mio::Sender<ClientMessage>,
|
||||
thread: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Drop for Client {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = self.channel.send(ClientMessage::Shutdown) {
|
||||
warn!("Error while closing client: {:?}. Already stopped?", e);
|
||||
}
|
||||
if let Some(thread) = self.thread.take() {
|
||||
thread.join().expect("Clean shutdown.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new() -> Result<Self, FetchError> {
|
||||
let mut event_loop = try!(mio::EventLoop::new());
|
||||
let channel = event_loop.channel();
|
||||
|
||||
let thread = thread::spawn(move || {
|
||||
let mut client = ClientLoop {
|
||||
next_token: 0,
|
||||
sessions: HashMap::new(),
|
||||
};
|
||||
event_loop.run(&mut client).unwrap();
|
||||
});
|
||||
|
||||
Ok(Client {
|
||||
channel: channel,
|
||||
thread: Some(thread),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fetch(&self, url: Url, writer: Box<io::Write + Send>) -> Result<mpsc::Receiver<FetchResult>, FetchError> {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
try!(self.channel.send(ClientMessage::Fetch(url, writer, tx)));
|
||||
Ok(rx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientLoop {
|
||||
next_token: usize,
|
||||
sessions: HashMap<usize, TlsClient>,
|
||||
}
|
||||
|
||||
impl mio::Handler for ClientLoop {
|
||||
type Timeout = ();
|
||||
type Message = ClientMessage;
|
||||
|
||||
fn ready(&mut self, event_loop: &mut mio::EventLoop<ClientLoop>, token: mio::Token, events: mio::EventSet) {
|
||||
let utoken = token.as_usize();
|
||||
let remove = if let Some(mut tlsclient) = self.sessions.get_mut(&utoken) {
|
||||
tlsclient.ready(event_loop, token, events)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if remove {
|
||||
self.sessions.remove(&utoken);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify(&mut self, event_loop: &mut mio::EventLoop<Self>, msg: Self::Message) {
|
||||
match msg {
|
||||
ClientMessage::Shutdown => event_loop.shutdown(),
|
||||
ClientMessage::Fetch(url, writer, sender) => {
|
||||
let token = self.next_token;
|
||||
self.next_token += 1;
|
||||
|
||||
if let Ok(mut tlsclient) = TlsClient::new(mio::Token(token), &url, writer, sender) {
|
||||
let httpreq = format!(
|
||||
"GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\nAccept-Encoding: identity\r\n\r\n",
|
||||
url.path(),
|
||||
url.hostname()
|
||||
);
|
||||
let _ = tlsclient.write(httpreq.as_bytes());
|
||||
tlsclient.register(event_loop);
|
||||
|
||||
self.sessions.insert(token, tlsclient);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_successfuly_fetch_a_page() {
|
||||
use std::io::{self, Cursor};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
struct Writer {
|
||||
wrote: Arc<AtomicUsize>,
|
||||
data: Cursor<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl io::Write for Writer {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let res = self.data.write(buf);
|
||||
if let Ok(count) = res {
|
||||
self.wrote.fetch_add(count, Ordering::Relaxed);
|
||||
}
|
||||
res
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> { Ok(()) }
|
||||
}
|
||||
|
||||
let client = Client::new().unwrap();
|
||||
|
||||
let wrote = Arc::new(AtomicUsize::new(0));
|
||||
let writer = Writer {
|
||||
wrote: wrote.clone(),
|
||||
data: Cursor::new(Vec::new()),
|
||||
};
|
||||
let rx = client.fetch(Url::new("github.com", 443, "/").unwrap(), Box::new(writer)).unwrap();
|
||||
|
||||
let result = rx.recv().unwrap();
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert!(wrote.load(Ordering::Relaxed) > 0);
|
||||
}
|
27
util/https-fetch/src/lib.rs
Normal file
27
util/https-fetch/src/lib.rs
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
extern crate rustls;
|
||||
extern crate mio;
|
||||
#[macro_use] extern crate log;
|
||||
|
||||
mod tlsclient;
|
||||
mod client;
|
||||
mod url;
|
||||
|
||||
pub use self::client::{Client, FetchError, FetchResult};
|
||||
pub use self::url::{Url, UrlError};
|
||||
|
231
util/https-fetch/src/tlsclient.rs
Normal file
231
util/https-fetch/src/tlsclient.rs
Normal file
@ -0,0 +1,231 @@
|
||||
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::str;
|
||||
use std::sync::{mpsc, Arc};
|
||||
use std::io::{self, Read, Cursor, BufReader};
|
||||
|
||||
use mio;
|
||||
use mio::tcp::TcpStream;
|
||||
use rustls::{self, Session};
|
||||
|
||||
use client::{FetchError, ClientLoop, FetchResult};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TlsClientError {
|
||||
Initialization,
|
||||
UnexpectedEof,
|
||||
Connection(io::Error),
|
||||
Writer(io::Error),
|
||||
Tls(rustls::TLSError),
|
||||
}
|
||||
|
||||
/// This encapsulates the TCP-level connection, some connection
|
||||
/// state, and the underlying TLS-level session.
|
||||
pub struct TlsClient {
|
||||
token: mio::Token,
|
||||
socket: TcpStream,
|
||||
tls_session: rustls::ClientSession,
|
||||
writer: Box<io::Write>,
|
||||
error: Option<TlsClientError>,
|
||||
closing: bool,
|
||||
listener: mpsc::Sender<FetchResult>,
|
||||
}
|
||||
|
||||
impl io::Write for TlsClient {
|
||||
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
|
||||
self.tls_session.write(bytes)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.tls_session.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Read for TlsClient {
|
||||
fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
|
||||
self.tls_session.read(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl TlsClient {
|
||||
pub fn make_config() -> Result<Arc<rustls::ClientConfig>, FetchError> {
|
||||
let mut config = rustls::ClientConfig::new();
|
||||
// TODO [ToDr] Windows / MacOs support!
|
||||
let mut cursor = Cursor::new(if cfg!(feature = "ca-github-only") {
|
||||
include_bytes!("./ca-github.crt").to_vec()
|
||||
} else {
|
||||
include_bytes!("./ca-certificates.crt").to_vec()
|
||||
});
|
||||
let mut reader = BufReader::new(&mut cursor);
|
||||
try!(config.root_store.add_pem_file(&mut reader).map_err(|_| FetchError::ReadingCaCertificates));
|
||||
// TODO [ToDr] client certificate?
|
||||
Ok(Arc::new(config))
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
token: mio::Token,
|
||||
url: &Url,
|
||||
writer: Box<io::Write + Send>,
|
||||
sender: mpsc::Sender<FetchResult>,
|
||||
) -> Result<Self, FetchError> {
|
||||
let res = TlsClient::make_config().and_then(|cfg| {
|
||||
TcpStream::connect(url.address()).map(|sock| {
|
||||
(cfg, sock)
|
||||
}).map_err(Into::into)
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok((cfg, sock)) => Ok(TlsClient {
|
||||
token: token,
|
||||
writer: writer,
|
||||
socket: sock,
|
||||
closing: false,
|
||||
error: None,
|
||||
tls_session: rustls::ClientSession::new(&cfg, url.hostname()),
|
||||
listener: sender,
|
||||
}),
|
||||
Err(e) => {
|
||||
sender.send(Err(e)).unwrap_or_else(|e| warn!("Client initialization error: {:?}", e));
|
||||
Err(FetchError::Client(TlsClientError::Initialization))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by mio each time events we register() for happen.
|
||||
/// Return false if reregistering again.
|
||||
pub fn ready(&mut self, event_loop: &mut mio::EventLoop<ClientLoop>, token: mio::Token, events: mio::EventSet) -> bool {
|
||||
assert_eq!(token, self.token);
|
||||
|
||||
if events.is_readable() {
|
||||
self.do_read();
|
||||
}
|
||||
|
||||
if events.is_writable() {
|
||||
self.do_write();
|
||||
}
|
||||
|
||||
if self.is_closed() {
|
||||
trace!("Connection closed");
|
||||
let res = self.listener.send(match self.error.take() {
|
||||
Some(err) => Err(err.into()),
|
||||
None => Ok(()),
|
||||
});
|
||||
|
||||
if let Err(e) = res {
|
||||
warn!("Finished fetching but listener is not available: {:?}", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
self.reregister(event_loop);
|
||||
false
|
||||
}
|
||||
|
||||
pub fn register(&mut self, event_loop: &mut mio::EventLoop<ClientLoop>) {
|
||||
event_loop.register(
|
||||
&self.socket,
|
||||
self.token,
|
||||
self.event_set(),
|
||||
mio::PollOpt::level() | mio::PollOpt::oneshot()
|
||||
).unwrap_or_else(|e| self.error = Some(TlsClientError::Connection(e)));
|
||||
}
|
||||
|
||||
fn reregister(&mut self, event_loop: &mut mio::EventLoop<ClientLoop>) {
|
||||
event_loop.reregister(
|
||||
&self.socket,
|
||||
self.token,
|
||||
self.event_set(),
|
||||
mio::PollOpt::level() | mio::PollOpt::oneshot()
|
||||
).unwrap_or_else(|e| self.error = Some(TlsClientError::Connection(e)));
|
||||
}
|
||||
|
||||
/// We're ready to do a read.
|
||||
fn do_read(&mut self) {
|
||||
// Read TLS data. This fails if the underlying TCP connection is broken.
|
||||
let rc = self.tls_session.read_tls(&mut self.socket);
|
||||
if let Err(e) = rc {
|
||||
trace!("TLS read error: {:?}", e);
|
||||
self.closing = true;
|
||||
self.error = Some(TlsClientError::Connection(e));
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're ready but there's no data: EOF.
|
||||
if rc.unwrap() == 0 {
|
||||
trace!("Unexpected EOF");
|
||||
self.error = Some(TlsClientError::UnexpectedEof);
|
||||
self.closing = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Reading some TLS data might have yielded new TLS messages to process.
|
||||
// Errors from this indicate TLS protocol problems and are fatal.
|
||||
let processed = self.tls_session.process_new_packets();
|
||||
if let Err(e) = processed {
|
||||
trace!("TLS error: {:?}", e);
|
||||
self.error = Some(TlsClientError::Tls(e));
|
||||
self.closing = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Having read some TLS data, and processed any new messages, we might have new plaintext as a result.
|
||||
// Read it and then write it to stdout.
|
||||
let mut plaintext = Vec::new();
|
||||
let rc = self.tls_session.read_to_end(&mut plaintext);
|
||||
if !plaintext.is_empty() {
|
||||
self.writer.write(&plaintext).unwrap_or_else(|e| {
|
||||
trace!("Write error: {:?}", e);
|
||||
self.error = Some(TlsClientError::Writer(e));
|
||||
0
|
||||
});
|
||||
}
|
||||
|
||||
// If that fails, the peer might have started a clean TLS-level session closure.
|
||||
if let Err(err) = rc {
|
||||
if err.kind() != io::ErrorKind::ConnectionAborted {
|
||||
self.error = Some(TlsClientError::Connection(err));
|
||||
}
|
||||
self.closing = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn do_write(&mut self) {
|
||||
self.tls_session.write_tls(&mut self.socket).unwrap_or_else(|e| {
|
||||
warn!("TLS write error: {:?}", e);
|
||||
});
|
||||
}
|
||||
|
||||
// Use wants_read/wants_write to register for different mio-level IO readiness events.
|
||||
fn event_set(&self) -> mio::EventSet {
|
||||
let rd = self.tls_session.wants_read();
|
||||
let wr = self.tls_session.wants_write();
|
||||
|
||||
if rd && wr {
|
||||
mio::EventSet::readable() | mio::EventSet::writable()
|
||||
} else if wr {
|
||||
mio::EventSet::writable()
|
||||
} else {
|
||||
mio::EventSet::readable()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_closed(&self) -> bool {
|
||||
self.closing
|
||||
}
|
||||
}
|
||||
|
81
util/https-fetch/src/url.rs
Normal file
81
util/https-fetch/src/url.rs
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UrlError {
|
||||
InvalidAddress
|
||||
}
|
||||
|
||||
/// Build a ClientConfig from our arguments
|
||||
pub struct Url {
|
||||
address: SocketAddr,
|
||||
hostname: String,
|
||||
port: u16,
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl Url {
|
||||
pub fn new(hostname: &str, port: u16, path: &str) -> Result<Self, UrlError> {
|
||||
let addr = try!(Self::lookup_ipv4(hostname, port));
|
||||
Ok(Url {
|
||||
address: addr,
|
||||
hostname: hostname.into(),
|
||||
port: port,
|
||||
path: path.into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn lookup_ipv4(host: &str, port: u16) -> Result<SocketAddr, UrlError> {
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
let addrs = try!((host, port).to_socket_addrs().map_err(|_| UrlError::InvalidAddress));
|
||||
for addr in addrs {
|
||||
if let SocketAddr::V4(_) = addr {
|
||||
return Ok(addr.clone());
|
||||
}
|
||||
}
|
||||
Err(UrlError::InvalidAddress)
|
||||
}
|
||||
|
||||
pub fn address(&self) -> &SocketAddr {
|
||||
&self.address
|
||||
}
|
||||
|
||||
pub fn hostname(&self) -> &str {
|
||||
&self.hostname
|
||||
}
|
||||
|
||||
pub fn port(&self) -> u16 {
|
||||
self.port
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn should_parse_url() {
|
||||
// given
|
||||
let url = Url::new("github.com", 443, "/").unwrap();
|
||||
|
||||
assert_eq!(url.hostname(), "github.com");
|
||||
assert_eq!(url.port(), 443);
|
||||
assert_eq!(url.path(), "/");
|
||||
}
|
Loading…
Reference in New Issue
Block a user