diff --git a/src/client.toit b/src/client.toit index 4b1c1c0..7295eed 100644 --- a/src/client.toit +++ b/src/client.toit @@ -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. @@ -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_ @@ -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 @@ -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 @@ -620,9 +623,9 @@ 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 @@ -630,24 +633,24 @@ class Client: 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: @@ -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" diff --git a/tests/google_test.toit b/tests/google_test.toit index 7740eec..8db2c97 100644 --- a/tests/google_test.toit +++ b/tests/google_test.toit @@ -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" "/" @@ -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: