Skip to content

Commit

Permalink
Allow client users to provide a security store (#98)
Browse files Browse the repository at this point in the history
* Allow client users to provide a security store

* feedback
  • Loading branch information
Erik Corry authored Jun 1, 2023
1 parent c2acc29 commit 05ce206
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 14 deletions.
62 changes: 49 additions & 13 deletions src/client.toit
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class Client:
server_name_/string? ::= null
root_certificates_/List ::= []
connection_/Connection? := null
session_data_/Map ::= {:} // From host:port to session data.
security_store_/SecurityStore

/**
Constructs a new client instance over the given interface.
Expand All @@ -115,7 +115,9 @@ class Client:
Use `net.open` to obtain an interface.
*/
constructor .interface_
--root_certificates/List=[]:
--root_certificates/List=[]
--security_store/SecurityStore=SecurityStoreInMemory:
security_store_ = security_store
root_certificates_ = root_certificates
add_finalizer this:: this.finalize_

Expand All @@ -135,7 +137,9 @@ class Client:
constructor.tls .interface_
--root_certificates/List=[]
--server_name/string?=null
--certificate/tls.Certificate?=null:
--certificate/tls.Certificate?=null
--security_store/SecurityStore=SecurityStoreInMemory:
security_store_ = security_store
use_tls_by_default_ = true
root_certificates_ = root_certificates
server_name_ = server_name
Expand Down Expand Up @@ -607,7 +611,6 @@ class Client:
// We try to reuse an existing connection to a server, but a web server can
// lose interest in a long-running connection at any time and close it, so
// if it fails we need to reconnect.
host_with_port := location.host_with_port
success := false
try:
// Three attempts. One with a reused connection, one with reused session
Expand All @@ -620,34 +623,34 @@ class Client:
sock := connection_.socket_
if sock is tls.Socket and not reused:
tls_socket := sock as tls.Socket
use_stored_session_state_ tls_socket host_with_port
use_stored_session_state_ tls_socket location
tls_socket.handshake
update_stored_session_state_ tls_socket host_with_port
update_stored_session_state_ tls_socket location
block.call connection_
success = true
return
// We tried to reuse an already-open connection, but the server closed it.
connection_.close
connection_ = null
// Don't try again with session data if the connection attempt failed.
if not reused: session_data_.remove host_with_port
if not reused: security_store_.delete_session_data location.host location.port
finally:
if not success:
session_data_.remove host_with_port
security_store_.delete_session_data location.host location.port
if connection_:
connection_.close
connection_ = null

use_stored_session_state_ tls_socket/tls.Socket host_with_port/string:
if data := session_data_.get host_with_port:
use_stored_session_state_ tls_socket/tls.Socket location/ParsedUri_:
if data := security_store_.retrieve_session_data location.host location.port:
tls_socket.session_state = data

update_stored_session_state_ tls_socket/tls.Socket host_with_port/string:
update_stored_session_state_ tls_socket/tls.Socket location/ParsedUri_:
state := tls_socket.session_state
if state:
session_data_[host_with_port] = state
security_store_.store_session_data location.host location.port state
else:
session_data_.remove host_with_port
security_store_.delete_session_data location.host location.port

/// Returns true if the connection was reused.
ensure_connection_ location/ParsedUri_ -> bool:
Expand Down Expand Up @@ -889,3 +892,36 @@ class ParsedUri_:
static is_alpha_ str/string -> bool:
str.do: if not 'a' <= it <= 'z' and not 'A' <= it <= 'Z': return false
return true

/**
The interface of an object you can provide to the $Client to store and
retrieve security data. Currently only supports session data, which is
data that can be used to speed up reconnections to TLS servers.
*/
abstract class SecurityStore:
/// Store session data (eg a TLS ticket) for a given host and port.
abstract store_session_data host/string port/int data/ByteArray -> none
/// After a failed attempt to use session data we should not try to use it
/// again. This method should delete it from the store.
abstract delete_session_data host/string port/int -> none
/// If we have session data stored for a given host and port, this method
/// should return it.
abstract retrieve_session_data host/string port/int -> ByteArray?

/**
Default implementation of $SecurityStore that stores the data in an in-memory
hash map. This is not very useful, since data is not persisted over deep
sleep or between Clients, but it's an example of how to implement the
interface.
*/
class SecurityStoreInMemory extends SecurityStore:
session_data_ ::= {:}

store_session_data host/string port/int data/ByteArray -> none:
session_data_["$host:$port"] = data

delete_session_data host/string port/int -> none:
session_data_.remove "$host:$port"

retrieve_session_data host/string port/int -> ByteArray?:
return session_data_.get "$host:$port"
4 changes: 3 additions & 1 deletion tests/google_test.toit
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import certificate_roots

main:
network := net.open
security_store := http.SecurityStoreInMemory
client := http.Client.tls network
--security_store=security_store
--root_certificates=[certificate_roots.GLOBALSIGN_ROOT_CA,
certificate_roots.GTS_ROOT_R1]
response := client.get "script.google.com" "/"
Expand All @@ -21,6 +23,6 @@ main:
// Deliberately break the session state so that the server rejects our
// attempt to use an abbreviated handshake. We harmlessly retry without the
// session data.
client.session_data_["www.google.com"][15] ^= 42
security_store.session_data_["www.google.com:443"][15] ^= 42
response = client.get "www.google.com" "/"
while data := response.body.read:

0 comments on commit 05ce206

Please sign in to comment.