blob: 979636c51b4c8b384c738c055d3af4cafaff13c8 [file] [edit]
/*
* Copyright (C) 2026 Apple Inc. 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.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. 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.
*/
#import "AutomationConnection.h"
#import <WebKit/_WKAutomationSession.h>
#import <WebKit/_WKAutomationSessionConfiguration.h>
#import <WebKit/_WKAutomationSessionPrivateForTesting.h>
#import <arpa/inet.h>
#import <string.h>
#import <unistd.h>
// Each message is a JSON object with an "event" field, preceded by its length as a
// 4-byte big-endian integer. minidriver launches a MiniBrowser for each session, so this
// connection hosts a single automation target, for a single session. MiniBrowser quits
// when the session ends, or when the connection closes.
static const uint64_t automationTargetIdentifier = 1;
static const uint64_t clientConnectionIdentifier = 1;
@implementation AutomationConnection {
__weak id<AutomationConnectionDelegate> _delegate;
dispatch_io_t _channel;
NSMutableData *_receiveBuffer;
_WKAutomationSession *_session;
BOOL _paired;
BOOL _closed;
}
- (instancetype)initWithFileDescriptor:(int)fileDescriptor delegate:(id<AutomationConnectionDelegate>)delegate
{
if (!(self = [super init]))
return nil;
_delegate = delegate;
_receiveBuffer = [NSMutableData data];
_channel = dispatch_io_create(DISPATCH_IO_STREAM, fileDescriptor, dispatch_get_main_queue(), ^(int error) {
close(fileDescriptor);
});
if (!_channel) {
close(fileDescriptor);
return nil;
}
dispatch_io_set_low_water(_channel, 1);
return self;
}
- (void)open
{
__weak AutomationConnection *weakSelf = self;
dispatch_io_read(_channel, 0, SIZE_MAX, dispatch_get_main_queue(), ^(bool done, dispatch_data_t data, int error) {
AutomationConnection *strongSelf = weakSelf;
if (!strongSelf || strongSelf->_closed)
return;
if (data)
[strongSelf _didReceiveData:data];
// The read is done at end of file or on error.
if (done) {
if (error)
NSLog(@"Reading from the automation connection failed: %s", strerror(error));
[strongSelf _close];
}
});
}
- (void)_close
{
if (_closed)
return;
_closed = YES;
dispatch_io_close(_channel, DISPATCH_IO_STOP);
// Removing the handler disconnects the session, which ends it.
[_session _setMessageToFrontendHandlerForTesting:nil];
_session = nil;
[_delegate automationConnectionDidClose:self];
}
- (void)_didReceiveData:(dispatch_data_t)data
{
dispatch_data_apply(data, ^bool(dispatch_data_t region, size_t offset, const void *buffer, size_t size) {
[_receiveBuffer appendBytes:buffer length:size];
return true;
});
NSUInteger offset = 0;
while (!_closed && _receiveBuffer.length - offset >= sizeof(uint32_t)) {
uint32_t messageSize;
[_receiveBuffer getBytes:&messageSize range:NSMakeRange(offset, sizeof(messageSize))];
messageSize = ntohl(messageSize);
if (_receiveBuffer.length - offset - sizeof(uint32_t) < messageSize)
break;
NSData *message = [_receiveBuffer subdataWithRange:NSMakeRange(offset + sizeof(uint32_t), messageSize)];
offset += sizeof(uint32_t) + messageSize;
NSDictionary *event = [NSJSONSerialization JSONObjectWithData:message options:0 error:nil];
if (![event isKindOfClass:NSDictionary.class]) {
NSLog(@"Received an automation message that isn't a JSON object.");
continue;
}
[self _didReceiveEvent:event];
}
if (!_closed)
[_receiveBuffer replaceBytesInRange:NSMakeRange(0, offset) withBytes:NULL length:0];
}
- (void)_sendEvent:(NSDictionary *)event
{
if (_closed)
return;
NSData *payload = [NSJSONSerialization dataWithJSONObject:event options:0 error:nil];
if (!payload)
return;
uint32_t messageSize = htonl((uint32_t)payload.length);
NSMutableData *message = [NSMutableData dataWithBytes:&messageSize length:sizeof(messageSize)];
[message appendData:payload];
dispatch_data_t data = dispatch_data_create(message.bytes, message.length, nil, DISPATCH_DATA_DESTRUCTOR_DEFAULT);
dispatch_io_write(_channel, 0, data, dispatch_get_main_queue(), ^(bool done, dispatch_data_t remainingData, int error) {
if (done && error)
NSLog(@"Writing to the automation connection failed: %s", strerror(error));
});
}
static NSString *stringFromJSONObject(id object)
{
NSData *data = [NSJSONSerialization dataWithJSONObject:object options:0 error:nil];
return data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : @"";
}
static id JSONObjectFromString(id string)
{
if (![string isKindOfClass:NSString.class])
return nil;
return [NSJSONSerialization JSONObjectWithData:[string dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
}
- (void)_didReceiveEvent:(NSDictionary *)event
{
NSString *name = event[@"event"];
if ([name isEqualToString:@"StartAutomationSession"])
[self _startAutomationSession:JSONObjectFromString(event[@"message"])];
else if ([name isEqualToString:@"Setup"])
[self _setupTarget:event];
else if ([name isEqualToString:@"SendMessageToBackend"]) {
if ([event[@"targetID"] unsignedLongLongValue] == automationTargetIdentifier && [event[@"message"] isKindOfClass:NSString.class])
[_session _dispatchMessageFromRemoteForTesting:event[@"message"]];
} else if ([name isEqualToString:@"FrontendDidClose"])
[self _close];
else
NSLog(@"Received an unknown automation event: %@", name);
}
- (void)_startAutomationSession:(NSDictionary *)message
{
// A second request is invalid, because the connection hosts a single session.
_WKAutomationSession *session = nil;
NSString *sessionIdentifier = [message isKindOfClass:NSDictionary.class] ? message[@"sessionID"] : nil;
if (![sessionIdentifier isKindOfClass:NSString.class] || _session)
NSLog(@"Ignoring an invalid request to start an automation session.");
else {
_WKAutomationSessionConfiguration *configuration = [[_WKAutomationSessionConfiguration alloc] init];
NSDictionary *capabilities = message[@"capabilities"];
if ([capabilities isKindOfClass:NSDictionary.class])
configuration.acceptInsecureCertificates = [capabilities[@"acceptInsecureCerts"] boolValue];
session = [_delegate automationConnection:self createSessionWithIdentifier:sessionIdentifier configuration:configuration];
_session = session;
}
// Answer every request, so that minidriver doesn't wait for a session that won't start. It gives
// up on the session if the target list doesn't have its automation target.
[self _sendEvent:@{
@"event": @"StartAutomationSession_Return",
@"message": stringFromJSONObject(@{
@"browserName": [_delegate browserNameForAutomationConnection:self],
@"browserVersion": [_delegate browserVersionForAutomationConnection:self],
}),
}];
NSArray *targetList = @[ ];
if (session) {
targetList = @[ @{
@"type": @"automation",
@"name": session.sessionIdentifier,
@"targetID": @(automationTargetIdentifier),
@"isPaired": @NO,
} ];
}
[self _sendEvent:@{
@"event": @"SetTargetList",
@"connectionID": @(clientConnectionIdentifier),
@"message": stringFromJSONObject(targetList),
}];
}
- (void)_setupTarget:(NSDictionary *)event
{
// Installing another handler would disconnect the session, and end it.
if (!_session || _paired || [event[@"targetID"] unsignedLongLongValue] != automationTargetIdentifier)
return;
_paired = YES;
__weak AutomationConnection *weakSelf = self;
[_session _setMessageToFrontendHandlerForTesting:^(NSString *message) {
[weakSelf _sendEvent:@{
@"event": @"SendMessageToFrontend",
@"connectionID": @(clientConnectionIdentifier),
@"targetID": @(automationTargetIdentifier),
@"message": message,
}];
}];
}
@end