Skip to content

Commit

Permalink
chore: client certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
YusukeIwaki committed Aug 27, 2024
1 parent e3e4b02 commit b8637ae
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 3 deletions.
24 changes: 24 additions & 0 deletions lib/playwright/utils.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'base64'

module Playwright
module Utils
module PrepareBrowserContextOptions
Expand Down Expand Up @@ -65,6 +67,28 @@ module PrepareBrowserContextOptions
params[:acceptDownloads] = params[:acceptDownloads] ? 'accept' : 'deny'
end

if params[:clientCertificates].is_a?(Array)
params[:clientCertificates] = params[:clientCertificates].filter_map do |item|
out_record = {
origin: item[:origin],
passphrase: item[:passphrase],
}

{ pfxPath: 'pfx', certPath: 'cert', keyPath: 'key' }.each do |key, out_key|
if (filepath = item[key])
out_record[out_key] = Base64.encode64(File.read(filepath)) rescue ''
elsif (value = item[out_key.to_sym])
out_record[out_key] = value
end
end

out_record.compact!
next nil if out_record.empty?

out_record
end
end

params
end
end
Expand Down
182 changes: 182 additions & 0 deletions spec/integration/client_certificates_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
require 'spec_helper'

RSpec.describe 'client certificates', sinatra: true, tls: true do
before { skip unless chromium? }

it 'should validate input 1' do
options = {
ignoreHTTPSErrors: true,
clientCertificates: [{ origin: 'test' }]
}
expect {
with_context(**options) do |context|
context.new_page
end
}.to raise_error(/None of cert, key, passphrase or pfx is specified/)
end

it 'should validate input 2' do
kDummyFileName = '__filename'
options = {
ignoreHTTPSErrors: true,
clientCertificates: [{
origin: 'test',
certPath: kDummyFileName,
keyPath: kDummyFileName,
pfxPath: kDummyFileName,
passphrase: kDummyFileName,
}]
}
expect {
with_context(**options) do |context|
context.new_page
end
}.to raise_error(/pfx is specified together with cert, key or passphrase/)
end

it 'should validate input 3' do
kDummyFileName = '__filename'
options = {
ignoreHTTPSErrors: true,
proxy: { server: 'http://localhost:8080' },
clientCertificates: [{
origin: 'test',
certPath: kDummyFileName,
keyPath: kDummyFileName,
}]
}
expect {
with_context(**options) do |context|
context.new_page
end
}.to raise_error(/Cannot specify both proxy and clientCertificates/)
end

it 'should fail with no client certificates provided' do
with_context(ignoreHTTPSErrors: true) do |context|
page = context.new_page
expect {
page.goto(server_empty_page)
}.to raise_error(/net::ERR_CONNECTION_RESET|net::ERR_SOCKET_NOT_CONNECTED|The network connection was lost|Connection terminated unexpectedly/)
end
end

def asset(path)
File.join(__dir__, '../', 'assets', path)
end

it 'should throw with untrusted client certs' do
options = {
ignoreHTTPSErrors: true,
clientCertificates: [{
origin: server_prefix,
certPath: asset('client-certificates/client/self-signed/cert.pem'),
keyPath: asset('client-certificates/client/self-signed/key.pem'),
}]
}
with_context(**options) do |context|
page = context.new_page
expect {
page.goto(server_empty_page)
}.to raise_error(/net::ERR_EMPTY_RESPONSE|The network connection was lost|Connection terminated unexpectedly/)
end
end

it 'should pass with trusted client certificates' do
options = {
ignoreHTTPSErrors: true,
clientCertificates: [{
origin: server_prefix,
certPath: asset('client-certificates/client/trusted/cert.pem'),
keyPath: asset('client-certificates/client/trusted/key.pem'),
}]
}
with_context(**options) do |context|
page = context.new_page
page.goto("#{server_prefix}/one-style.html")
expect(page.content).to include('hello, world!')
end
end

it 'should pass with trusted client certificates in base64 format' do
options = {
ignoreHTTPSErrors: true,
clientCertificates: [{
origin: server_prefix,
cert: Base64.encode64(File.read(asset('client-certificates/client/trusted/cert.pem'))),
key: Base64.encode64(File.read(asset('client-certificates/client/trusted/key.pem'))),
}]
}
with_context(**options) do |context|
page = context.new_page
page.goto("#{server_prefix}/one-style.html")
expect(page.content).to include('hello, world!')
end
end

it 'should pass with trusted client certificates in pfx format' do
options = {
ignoreHTTPSErrors: true,
clientCertificates: [{
origin: server_prefix,
pfxPath: asset('client-certificates/client/trusted/cert.pfx'),
passphrase: 'secure'
}]
}
with_context(**options) do |context|
page = context.new_page
page.goto("#{server_prefix}/one-style.html")
expect(page.content).to include('hello, world!')
end
end

it 'should pass with trusted client certificates in pfx format with base64 encoded' do
options = {
ignoreHTTPSErrors: true,
clientCertificates: [{
origin: server_prefix,
pfx: Base64.encode64(File.read(asset('client-certificates/client/trusted/cert.pfx'))),
passphrase: 'secure'
}]
}
with_context(**options) do |context|
page = context.new_page
page.goto("#{server_prefix}/one-style.html")
expect(page.content).to include('hello, world!')
end
end

it 'should throw a http error if the pfx passphrase is incorect' do
options = {
ignoreHTTPSErrors: true,
clientCertificates: [{
origin: server_prefix,
pfxPath: asset('client-certificates/client/trusted/cert.pfx'),
passphrase: 'this-password-is-incorrect'
}]
}
with_context(**options) do |context|
page = context.new_page
page.goto(server_empty_page)
expect(page.content).to include('mac verify failure')
end
end

it 'should fail with matching certificates in legacy pfx format' do
skip unless Gem::Version.new(OpenSSL::VERSION) >= Gem::Version.new('3.0.0')

options = {
ignoreHTTPSErrors: true,
clientCertificates: [{
origin: server_prefix,
pfxPath: asset('client-certificates/client/trusted/cert-legacy.pfx'),
passphrase: 'secure'
}]
}
with_context(**options) do |context|
page = context.new_page
page.goto(server_empty_page)
expect(page.content).to include('Unsupported TLS certificate')
end
end
end
35 changes: 32 additions & 3 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def call(env)
if ENV['CI']
module PumaEventsLogSuppressing
ACCEPT= [
/^\* Listening on http/,
/^\* Listening on (http|ssl)/,
]

def log(str)
Expand Down Expand Up @@ -239,13 +239,42 @@ def route_missing

sinatra_app.get('/_ping') { '_pong' }

if example.metadata[:tls]
@server_prefix = "https://localhost:#{@server_port}"
@server_empty_page = "#{@server_prefix}/empty.html"

base_path = File.join(__dir__, 'assets/client-certificates/server')
key_path = File.join(base_path, 'server_key.pem')
cert_path = File.join(base_path, 'server_cert.pem')
ca_path = File.join(base_path, 'server_cert.pem')
uri = URI('ssl://localhost')
uri.query = URI.encode_www_form(
key: key_path,
cert: cert_path,
ca: ca_path,
verify_mode: 'force_peer',
)
bind = uri.to_s
else
bind = '127.0.0.1'
end

# Start server and wait for server ready.
# FIXME should change port when Errno::EADDRINUSE
Thread.new(@server_port) { |port| sinatra_app.run!(port: port) }
Thread.new(@server_port) { |port| sinatra_app.run!(port: port, bind: bind) }
Timeout.timeout(3) do
loop do
begin
Net::HTTP.get(URI("#{server_prefix}/_ping"))
if example.metadata[:tls]
Net::HTTP.start('localhost', @server_port, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
http.get('/_ping')
end
else
Net::HTTP.get(URI("#{server_prefix}/_ping"))
end
break
rescue Errno::ECONNRESET, OpenSSL::SSL::SSLError, EOFError
# In this case socket is connected but just SSL client cert is not provided.
break
rescue Errno::EADDRNOTAVAIL
sleep 1
Expand Down

0 comments on commit b8637ae

Please sign in to comment.