safari: TabFSServer subprocess that can live long. fixes bug where fs would die after a minute or two

This commit is contained in:
Omar Rizwan 2021-02-08 12:12:43 -08:00
parent 9b4abc40ee
commit 2f639e2a02
7 changed files with 245 additions and 120 deletions

View File

@ -20,3 +20,12 @@ sites. It should be running now! (?)
Check the `fs/mnt` folder of the TabFS repo on your computer to see if
it's mounted.
### tips
- To open Web inspector: Safari -> Develop menu -> Web Extension
Background Pages -> TabFS
- You need to rebuild if you change background.js. This is pretty
annoying.

View File

@ -16,7 +16,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
guard let message = item.userInfo?["message"] as? [AnyHashable: Any] else { return }
guard message["op"] as! String == "safari_did_connect" else { return }
// The XPC service is a subprocess that lives outside the macOS App Sandbox.
// (Safari extension native code, including this file, has to live in the sandbox.)
// It can do forbidden things like spawn tabfs filesystem and set up WebSocket server.

View File

@ -9,10 +9,11 @@
/* Begin PBXBuildFile section */
F028D2B625D0B7370095C2D5 /* TabFSService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028D2B525D0B7370095C2D5 /* TabFSService.swift */; };
F028D2B825D0B7370095C2D5 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028D2B725D0B7370095C2D5 /* main.swift */; };
F028D2BC25D0B7370095C2D5 /* TabFSService.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = F028D2B125D0B7370095C2D5 /* TabFSService.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
F028D2D725D0B8500095C2D5 /* TabFSServiceProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028D2B325D0B7370095C2D5 /* TabFSServiceProtocols.swift */; };
F028D2DE25D0B8590095C2D5 /* TabFSServiceProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028D2B325D0B7370095C2D5 /* TabFSServiceProtocols.swift */; };
F028D2ED25D106F10095C2D5 /* TabFSService.xpc in CopyFiles */ = {isa = PBXBuildFile; fileRef = F028D2B125D0B7370095C2D5 /* TabFSService.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
F028D30125D17B080095C2D5 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028D30025D17B080095C2D5 /* main.swift */; };
F028D34525D17D6A0095C2D5 /* TabFSServer in CopyFiles */ = {isa = PBXBuildFile; fileRef = F028D2FE25D17B080095C2D5 /* TabFSServer */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
F04429F625C7507200D998A5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04429F525C7507200D998A5 /* AppDelegate.swift */; };
F04429F925C7507200D998A5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F04429F725C7507200D998A5 /* Main.storyboard */; };
F04429FB25C7507200D998A5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04429FA25C7507200D998A5 /* ViewController.swift */; };
@ -27,12 +28,12 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
F028D2BA25D0B7370095C2D5 /* PBXContainerItemProxy */ = {
F028D33725D17D100095C2D5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = F04429E925C7507200D998A5 /* Project object */;
proxyType = 1;
remoteGlobalIDString = F028D2B025D0B7370095C2D5;
remoteInfo = TabFSService;
remoteGlobalIDString = F028D2FD25D17B080095C2D5;
remoteInfo = TabFSServer;
};
F0442A0525C7507400D998A5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
@ -44,17 +45,6 @@
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
F028D2BD25D0B7370095C2D5 /* Embed XPC Services */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices";
dstSubfolderSpec = 16;
files = (
F028D2BC25D0B7370095C2D5 /* TabFSService.xpc in Embed XPC Services */,
);
name = "Embed XPC Services";
runOnlyForDeploymentPostprocessing = 0;
};
F028D2E525D106BB0095C2D5 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@ -65,6 +55,25 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
F028D2FC25D17B080095C2D5 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = /usr/share/man/man1/;
dstSubfolderSpec = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 1;
};
F028D30E25D17BD20095C2D5 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 7;
files = (
F028D34525D17D6A0095C2D5 /* TabFSServer in CopyFiles */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F0442A1425C7507400D998A5 /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@ -84,6 +93,8 @@
F028D2B525D0B7370095C2D5 /* TabFSService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabFSService.swift; sourceTree = "<group>"; };
F028D2B725D0B7370095C2D5 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
F028D2B925D0B7370095C2D5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F028D2FE25D17B080095C2D5 /* TabFSServer */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = TabFSServer; sourceTree = BUILT_PRODUCTS_DIR; };
F028D30025D17B080095C2D5 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
F04429F125C7507200D998A5 /* TabFS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TabFS.app; sourceTree = BUILT_PRODUCTS_DIR; };
F04429F425C7507200D998A5 /* TabFS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TabFS.entitlements; sourceTree = "<group>"; };
F04429F525C7507200D998A5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -110,6 +121,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
F028D2FB25D17B080095C2D5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
F04429EE25C7507200D998A5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -139,12 +157,21 @@
path = TabFSService;
sourceTree = "<group>";
};
F028D2FF25D17B080095C2D5 /* TabFSServer */ = {
isa = PBXGroup;
children = (
F028D30025D17B080095C2D5 /* main.swift */,
);
path = TabFSServer;
sourceTree = "<group>";
};
F04429E825C7507200D998A5 = {
isa = PBXGroup;
children = (
F04429F325C7507200D998A5 /* TabFS */,
F0442A0A25C7507400D998A5 /* TabFS Extension */,
F028D2B225D0B7370095C2D5 /* TabFSService */,
F028D2FF25D17B080095C2D5 /* TabFSServer */,
F0442A0725C7507400D998A5 /* Frameworks */,
F04429F225C7507200D998A5 /* Products */,
);
@ -156,6 +183,7 @@
F04429F125C7507200D998A5 /* TabFS.app */,
F0442A0325C7507400D998A5 /* TabFS Extension.appex */,
F028D2B125D0B7370095C2D5 /* TabFSService.xpc */,
F028D2FE25D17B080095C2D5 /* TabFSServer */,
);
name = Products;
sourceTree = "<group>";
@ -214,16 +242,35 @@
F028D2AD25D0B7370095C2D5 /* Sources */,
F028D2AE25D0B7370095C2D5 /* Frameworks */,
F028D2AF25D0B7370095C2D5 /* Resources */,
F028D30E25D17BD20095C2D5 /* CopyFiles */,
);
buildRules = (
);
dependencies = (
F028D33825D17D100095C2D5 /* PBXTargetDependency */,
);
name = TabFSService;
productName = TabFSService;
productReference = F028D2B125D0B7370095C2D5 /* TabFSService.xpc */;
productType = "com.apple.product-type.xpc-service";
};
F028D2FD25D17B080095C2D5 /* TabFSServer */ = {
isa = PBXNativeTarget;
buildConfigurationList = F028D30425D17B090095C2D5 /* Build configuration list for PBXNativeTarget "TabFSServer" */;
buildPhases = (
F028D2FA25D17B080095C2D5 /* Sources */,
F028D2FB25D17B080095C2D5 /* Frameworks */,
F028D2FC25D17B080095C2D5 /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = TabFSServer;
productName = TabFSServer;
productReference = F028D2FE25D17B080095C2D5 /* TabFSServer */;
productType = "com.apple.product-type.tool";
};
F04429F025C7507200D998A5 /* TabFS */ = {
isa = PBXNativeTarget;
buildConfigurationList = F0442A1525C7507400D998A5 /* Build configuration list for PBXNativeTarget "TabFS" */;
@ -232,13 +279,11 @@
F04429EE25C7507200D998A5 /* Frameworks */,
F04429EF25C7507200D998A5 /* Resources */,
F0442A1425C7507400D998A5 /* Embed App Extensions */,
F028D2BD25D0B7370095C2D5 /* Embed XPC Services */,
);
buildRules = (
);
dependencies = (
F0442A0625C7507400D998A5 /* PBXTargetDependency */,
F028D2BB25D0B7370095C2D5 /* PBXTargetDependency */,
);
name = TabFS;
productName = TabFS;
@ -275,6 +320,9 @@
F028D2B025D0B7370095C2D5 = {
CreatedOnToolsVersion = 12.1;
};
F028D2FD25D17B080095C2D5 = {
CreatedOnToolsVersion = 12.1;
};
F04429F025C7507200D998A5 = {
CreatedOnToolsVersion = 12.1;
};
@ -299,6 +347,7 @@
F04429F025C7507200D998A5 /* TabFS */,
F0442A0225C7507400D998A5 /* TabFS Extension */,
F028D2B025D0B7370095C2D5 /* TabFSService */,
F028D2FD25D17B080095C2D5 /* TabFSServer */,
);
};
/* End PBXProject section */
@ -344,6 +393,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
F028D2FA25D17B080095C2D5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F028D30125D17B080095C2D5 /* main.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F04429ED25C7507200D998A5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -365,10 +422,10 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
F028D2BB25D0B7370095C2D5 /* PBXTargetDependency */ = {
F028D33825D17D100095C2D5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = F028D2B025D0B7370095C2D5 /* TabFSService */;
targetProxy = F028D2BA25D0B7370095C2D5 /* PBXContainerItemProxy */;
target = F028D2FD25D17B080095C2D5 /* TabFSServer */;
targetProxy = F028D33725D17D100095C2D5 /* PBXContainerItemProxy */;
};
F0442A0625C7507400D998A5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
@ -421,6 +478,28 @@
};
name = Release;
};
F028D30225D17B080095C2D5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 75YA78K5AM;
ENABLE_HARDENED_RUNTIME = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
F028D30325D17B080095C2D5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 75YA78K5AM;
ENABLE_HARDENED_RUNTIME = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Release;
};
F0442A0F25C7507400D998A5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -628,6 +707,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
F028D30425D17B090095C2D5 /* Build configuration list for PBXNativeTarget "TabFSServer" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F028D30225D17B080095C2D5 /* Debug */,
F028D30325D17B080095C2D5 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
F04429EC25C7507200D998A5 /* Build configuration list for PBXProject "TabFS" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@ -7,13 +7,18 @@
<key>TabFS.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>2</integer>
</dict>
<key>TabFSService.xcscheme_^#shared#^_</key>
<key>TabFSServer.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>TabFSService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,113 @@
//
// main.swift
// TabFSServer
//
// Created by Omar Rizwan on 2/8/21.
//
import Foundation
import Network
import os.log
class TabFSServer {
var fs: Process!
var fsInput: FileHandle!
var fsOutput: FileHandle!
func startFs() {
let fileURL = URL(fileURLWithPath: #filePath)
let repoURL = fileURL.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
fs = Process()
fs.executableURL = repoURL.appendingPathComponent("fs").appendingPathComponent("tabfs")
fs.currentDirectoryURL = fs.executableURL?.deletingLastPathComponent()
fs.arguments = []
let inputPipe = Pipe(), outputPipe = Pipe()
fs.standardInput = inputPipe
fs.standardOutput = outputPipe
fsInput = inputPipe.fileHandleForWriting
fsOutput = outputPipe.fileHandleForReading
try! fs.run()
}
var ws: NWListener!
func startWs() {
// TODO: randomly generate port and report back to caller?
let port = NWEndpoint.Port(rawValue: 9991)!
let parameters = NWParameters(tls: nil)
parameters.allowLocalEndpointReuse = true
parameters.includePeerToPeer = true
// for security ? so people outside your computer can't hijack TabFS at least
parameters.requiredInterfaceType = .loopback
let opts = NWProtocolWebSocket.Options()
opts.autoReplyPing = true
parameters.defaultProtocolStack.applicationProtocols.insert(opts, at: 0)
ws = try! NWListener(using: parameters, on: port)
ws.start(queue: .main)
}
init() {
startFs()
startWs()
var handleRequest: ((_ req: Data) -> Void)?
ws.newConnectionHandler = { conn in
conn.start(queue: .main)
handleRequest = { req in
let metaData = NWProtocolWebSocket.Metadata(opcode: .text)
let context = NWConnection.ContentContext(identifier: "context", metadata: [metaData])
conn.send(content: req, contentContext: context, completion: .contentProcessed({ err in
if err != nil {
os_log(.default, "req %{public}@ error: %{public}@", String(data: req, encoding: .utf8)!, err!.debugDescription as CVarArg)
// FIXME: ERROR
}
}))
}
func read() {
conn.receiveMessage { (resp, context, isComplete, err) in
guard let resp = resp else {
// FIXME err
os_log(.default, "resp error: %{public}@", err!.debugDescription as CVarArg)
return
}
// Send the response back to tabfs.c.
self.fsInput.write(withUnsafeBytes(of: UInt32(resp.count)) { Data($0) })
self.fsInput.write(resp)
read()
}
}
read()
}
DispatchQueue.global(qos: .default).async {
while true {
// Blocking read from the tabfs process.
let length = self.fsOutput.readData(ofLength: 4).withUnsafeBytes { $0.load(as: UInt32.self) }
let req = self.fsOutput.readData(ofLength: Int(length))
if let handleRequest = handleRequest {
// Send the request over the WebSocket connection to background.js in browser.
handleRequest(req)
} else {
// FIXME: ERROR
}
}
}
// FIXME: notify
}
}
let server = TabFSServer()
dispatchMain()

View File

@ -10,105 +10,15 @@ import Network
import os.log
class TabFSService: NSObject, TabFSServiceProtocol {
var fs: Process!
var fsInput: FileHandle!
var fsOutput: FileHandle!
func startFs() {
let fileURL = URL(fileURLWithPath: #filePath)
let repoURL = fileURL.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
fs = Process()
fs.executableURL = repoURL.appendingPathComponent("fs").appendingPathComponent("tabfs")
fs.currentDirectoryURL = fs.executableURL?.deletingLastPathComponent()
fs.arguments = []
let inputPipe = Pipe(), outputPipe = Pipe()
fs.standardInput = inputPipe
fs.standardOutput = outputPipe
fsInput = inputPipe.fileHandleForWriting
fsOutput = outputPipe.fileHandleForReading
try! fs.run()
}
var ws: NWListener!
func startWs() {
// TODO: randomly generate port and report back to caller?
let port = NWEndpoint.Port(rawValue: 9991)!
let parameters = NWParameters(tls: nil)
parameters.allowLocalEndpointReuse = true
parameters.includePeerToPeer = true
// for security ? so people outside your computer can't hijack TabFS at least
parameters.requiredInterfaceType = .loopback
let opts = NWProtocolWebSocket.Options()
opts.autoReplyPing = true
parameters.defaultProtocolStack.applicationProtocols.insert(opts, at: 0)
ws = try! NWListener(using: parameters, on: port)
ws.start(queue: .main)
}
override init() {
super.init()
startFs()
startWs()
var handleRequest: ((_ req: Data) -> Void)?
ws.newConnectionHandler = { conn in
conn.start(queue: .main)
handleRequest = { req in
let metaData = NWProtocolWebSocket.Metadata(opcode: .text)
let context = NWConnection.ContentContext(identifier: "context", metadata: [metaData])
conn.send(content: req, contentContext: context, completion: .contentProcessed({ err in
if err != nil {
os_log(.default, "req %{public}@ error: %{public}@", String(data: req, encoding: .utf8)!, err!.debugDescription as CVarArg)
// FIXME: ERROR
}
}))
}
func read() {
conn.receiveMessage { (resp, context, isComplete, err) in
guard let resp = resp else {
// FIXME err
os_log(.default, "resp error: %{public}@", err!.debugDescription as CVarArg)
return
}
// Send the response back to tabfs.c.
self.fsInput.write(withUnsafeBytes(of: UInt32(resp.count)) { Data($0) })
self.fsInput.write(resp)
read()
}
}
read()
}
DispatchQueue.global(qos: .default).async {
while true {
// Blocking read from the tabfs process.
let length = self.fsOutput.readData(ofLength: 4).withUnsafeBytes { $0.load(as: UInt32.self) }
let req = self.fsOutput.readData(ofLength: Int(length))
if let handleRequest = handleRequest {
// Send the request over the WebSocket connection to background.js in browser.
handleRequest(req)
} else {
// FIXME: ERROR
}
}
}
// FIXME: disable auto termination
}
func start(withReply reply: @escaping () -> Void) {
// This XPC call is enough to just force the XPC service to be started.
os_log("HELLO")
let server = Process()
os_log("HOW ARE YOU?")
server.executableURL = Bundle.main.url(forResource: "TabFSServer", withExtension: "")!
os_log("I AM GOOD")
server.launch()
os_log("GREAT")
reply()
}
}