90 Commits

Author SHA1 Message Date
relikd
e54d69ef4b Version 1.0.0 (34) 2020-09-19 14:35:26 +02:00
relikd
be8269ad56 Include iOS version in json 2020-09-19 14:25:02 +02:00
relikd
7118ec3b02 Update to Xcode 12 2020-09-17 16:41:18 +02:00
relikd
71045bf0dd Ignore forceDisconnect on background recording 2020-09-17 13:46:18 +02:00
relikd
27abdd66f5 Display long domain names – two lines everywhere 2020-09-14 22:46:58 +02:00
relikd
162e18c912 Cleanup NEKit 2020-09-14 21:10:03 +02:00
relikd
d68e4ec869 Version 1.0.0 (33) 2020-09-14 12:56:30 +02:00
relikd
762263bfbd Default disconnect swdc + pre-check connect message 2020-09-14 12:56:06 +02:00
relikd
b1cddc796e Version 1.0.0 (32) 2020-09-14 11:52:21 +02:00
relikd
77e20f31f5 Persist recording logs in background 2020-09-14 11:51:44 +02:00
relikd
0175f5390e Fix crash trying to access userInfo 2020-09-13 12:13:14 +02:00
relikd
effc305b86 Version 1.0.0 (31) 2020-09-12 22:28:32 +02:00
relikd
c1fe258b0d Force disconnect to prevent domain spamming (optional in advanced settings) 2020-09-12 22:28:11 +02:00
relikd
36a8f0b97b Version 1.0.0 (30) 2020-09-12 11:35:11 +02:00
relikd
33b9cab8a8 Indicate background recording needs more time 2020-09-12 11:32:06 +02:00
relikd
b88874b38b Version 1.0.0 (29) 2020-09-12 10:57:20 +02:00
relikd
f55f3ea32d Disable copy menu on meta cells in 5 min context 2020-09-12 10:42:37 +02:00
relikd
c843bd76a2 Share notes opt-out, assuming notes are created for upload anyway 2020-09-12 10:31:13 +02:00
relikd
4dd2339ed8 Set recording segment color to indicate tap action 2020-09-12 10:27:04 +02:00
relikd
280526bef4 Hide filter button if new recording 2020-09-12 10:04:24 +02:00
relikd
34caffd4a7 Change tutorial text about app recording length 2020-09-12 09:57:53 +02:00
relikd
9e19b457e2 AppStore search: sort local apps case independent 2020-09-11 15:39:23 +02:00
relikd
e6846953b7 Copy upload key to clipboard 2020-09-08 18:35:30 +02:00
relikd
6d78aeac7b Fix header banner display issues 2020-09-08 18:16:21 +02:00
relikd
5d94fe3a0d Version 1.0.0 (28) 2020-09-08 11:56:02 +02:00
relikd
fb680d669b Fix crash on loading App Store search results 2020-09-08 11:52:07 +02:00
relikd
6409e5eaf3 Allow to contribute empty recordings 2020-09-08 04:28:07 +02:00
relikd
39ca9dbdb1 Persist recording logs before save operation (crash-safe) 2020-09-08 03:16:38 +02:00
relikd
27ab2a621a Set recording time as filter 2020-09-08 02:51:25 +02:00
relikd
3f572eeb15 Version 1.0.0 (27) 2020-09-06 10:13:01 +02:00
relikd
e83540d5de Fix empty json log 2020-09-05 23:32:57 +02:00
relikd
847556bec1 Add important notice to app recording 2020-09-05 22:26:04 +02:00
relikd
42b045fb85 Open co-occurrence from recording 2020-09-05 22:07:22 +02:00
relikd
35a211f87f Fix action target self-reference timing issues 2020-09-05 22:05:56 +02:00
relikd
d2fa67e0e3 Reduce redundant code, cell copy menu 2020-09-05 21:05:12 +02:00
relikd
b8660c9a35 Jump from Recordings to Requests tab 2020-09-05 20:08:37 +02:00
relikd
8cd3f7fb3a Fix iOS 10 layout issues 2020-09-04 09:14:35 +02:00
relikd
2ee0272a05 Improve recording contribution view. Replace TextView with interactive TableView. 2020-09-04 09:14:23 +02:00
relikd
4ae82fc763 Show recording how-to at least once after app install 2020-08-31 23:02:46 +02:00
relikd
aac42d7eff Fix duration 2020-08-31 22:42:55 +02:00
relikd
8bb77ef741 Tiny markdown parser, makes tutorial screens editing much simpler 2020-08-31 17:10:11 +02:00
relikd
ff4218981f Discard recording if time criteria not met 2020-08-31 12:18:36 +02:00
relikd
7b7c5f3d9a UI app recording vs. background recording 2020-08-30 00:03:15 +02:00
relikd
1c203e39c3 Fix iOS 10 Tutorial sheet top padding missing 2020-08-29 23:46:13 +02:00
relikd
7dbf21d564 Disable block & ignore filter during recording 2020-08-29 18:36:41 +02:00
relikd
8fcb5ad874 No VPN, no recording 2020-08-29 17:49:30 +02:00
relikd
b4bf705b7f Rename column uploadkey 2020-08-29 14:48:53 +02:00
relikd
69d8321180 check status 'ok' 2020-08-29 14:44:40 +02:00
relikd
b03daeca66 Store sharing key instead of just a bool 2020-08-28 23:41:08 +02:00
relikd
c502484bcf Indicate shared on recordings overview + move isShared check to sharing sheet 2020-08-28 23:02:10 +02:00
relikd
448d69c6d8 Show "no results" in recordings + mark recording as shared 2020-08-28 22:05:49 +02:00
relikd
42aa7cf926 Contribute recording 2020-08-28 18:36:52 +02:00
relikd
52fa2e460e Fix: Wait for busy lock instead of instantly dropping the operation 2020-08-24 00:58:50 +02:00
relikd
8855ae754a Fix: Auto-delete logs did not clear heap 2020-08-24 00:56:14 +02:00
relikd
908a909c87 Share results screen 2020-08-12 18:15:32 +02:00
relikd
41aee797a9 Split storyboard tabs 2020-08-11 20:10:13 +02:00
relikd
685f636d5b Recordings: Choose app instead of custom title 2020-08-11 19:21:07 +02:00
relikd
4af56b0cb1 Reverting to single step persist 2020-08-01 09:42:57 +02:00
relikd
a3973c7e9a Embed Recordings in navigation controller, not the other way around 2020-08-01 09:33:48 +02:00
relikd
b270f30f3c Version 1.0.0 (26) 2020-07-28 15:20:30 +02:00
relikd
03177cee0b Invalidate restart reminder when VPN is running 2020-07-28 15:19:47 +02:00
relikd
9ee094dc20 Version 1.0.0 (25) 2020-07-28 14:50:59 +02:00
relikd
b1d49c6765 Persist logs by renaming table (hopefully reduces lock time) 2020-07-27 21:16:14 +02:00
relikd
b774e2152c Don't perform notification open action if modal window is open 2020-07-27 19:23:16 +02:00
relikd
e398ac8bcd Let notification open domain 2020-07-27 19:06:44 +02:00
relikd
01523b250f Proper VPN simulator with notifications, etc. 2020-07-27 17:50:15 +02:00
relikd
a2b0f311d5 First version with app notifications 2020-07-26 22:32:11 +02:00
relikd
88a52fb92c Version 1.0.0 (24) 2020-07-02 14:12:26 +02:00
relikd
723f1665a7 fittingSize() 2020-07-02 12:26:34 +02:00
relikd
4f92d3d58d Co-Occurrence on domain level 2020-07-02 12:26:07 +02:00
relikd
05d06a4f31 Update readme 2020-07-01 13:31:29 +02:00
relikd
f9ab545e0f Version 1.0.0 (23) 2020-07-01 12:30:22 +02:00
relikd
b10d4c8b36 Show database file size in settings 2020-07-01 11:47:15 +02:00
relikd
5a3ca024f8 Vacuum before export 2020-07-01 10:57:50 +02:00
relikd
92216c0c03 CustomAlert refactoring. Using proper UIPresentationController with adaptive margins 2020-07-01 00:53:25 +02:00
relikd
9ece3474c6 Limit CustomAlert to screen size & cut padding in half if necessary 2020-06-29 00:34:15 +02:00
relikd
6dcc2086e6 Auto-delete logs finished + custom App-to-VPN messages 2020-06-28 23:55:08 +02:00
relikd
08483711e2 Remove two unimportant and verbose error logs 2020-06-28 21:17:37 +02:00
relikd
0e100006d3 Moving extensions around 2020-06-28 17:04:48 +02:00
relikd
710c617862 Move VPN manager logic into its own controller 2020-06-28 16:31:11 +02:00
relikd
3ed25c92cd Render assets as template image 2020-06-28 14:34:16 +02:00
relikd
f7644e6048 Rename Pref -> Prefs 2020-06-28 14:33:36 +02:00
relikd
80afa6aff1 Privacy: Auto-delete logs (no functionality yet) 2020-06-28 14:20:31 +02:00
relikd
43de81929f Alerts with custom views 2020-06-28 01:06:06 +02:00
relikd
e315e71d07 Storyboard constantly trying to replace floats with rounding error 2020-06-27 16:27:48 +02:00
relikd
416eb34799 Reverse context analysis sort order (oldest first) 2020-06-27 16:20:20 +02:00
relikd
b7b13f51b2 Recordings: Toggle between raw logs and summary 2020-06-27 16:13:58 +02:00
relikd
2312187670 DB readText -> col_text 2020-06-27 00:54:50 +02:00
relikd
c7d0dc7c5f UIColor.sysFg -> UIColor.sysLabel 2020-06-27 00:50:47 +02:00
relikd
895cabee80 Context analysis: +/-5min raw logs 2020-06-27 00:40:29 +02:00
157 changed files with 7066 additions and 3768 deletions

View File

@@ -7,31 +7,77 @@
objects = {
/* Begin PBXBuildFile section */
5404AEEB24A90717003B2F54 /* PrefsShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4524A8B0FE0025D261 /* PrefsShared.swift */; };
5404AEED24A95F3F003B2F54 /* SlideInAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5404AEEC24A95F3F003B2F54 /* SlideInAnimation.swift */; };
5404AEEF24ACC089003B2F54 /* VCAnalysisBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5404AEEE24ACC089003B2F54 /* VCAnalysisBar.swift */; };
540E6780242D2CF100871BBE /* VCRecordings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E677F242D2CF100871BBE /* VCRecordings.swift */; };
540E67822433483D00871BBE /* VCEditRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E67812433483D00871BBE /* VCEditRecording.swift */; };
540E67842433FAFE00871BBE /* TVCPreviousRecords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */; };
541075CE24C9D43A00D6F1BF /* UNNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075CD24C9D43A00D6F1BF /* UNNotification.swift */; };
541075CF24C9D43A00D6F1BF /* UNNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075CD24C9D43A00D6F1BF /* UNNotification.swift */; };
541075D124CDBA0000D6F1BF /* ThrottledBatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */; };
541075D224CDBA0000D6F1BF /* ThrottledBatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */; };
541075D524CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D424CE286200D6F1BF /* CachedConnectionAlert.swift */; };
541075D624CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D424CE286200D6F1BF /* CachedConnectionAlert.swift */; };
541075D924CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D824CE2C7200D6F1BF /* GlassVPNHook.swift */; };
541075DA24CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D824CE2C7200D6F1BF /* GlassVPNHook.swift */; };
5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412F8ED24571B8100A63D7A /* VCDateFilter.swift */; };
5412FCC224C628FA000DE429 /* TVCReminderAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412FCBF24C628F9000DE429 /* TVCReminderAlerts.swift */; };
5412FCC324C628FA000DE429 /* TVCChooseAlertTone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412FCC024C628F9000DE429 /* TVCChooseAlertTone.swift */; };
5412FCC424C628FA000DE429 /* TVCConnectionAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412FCC124C628FA000DE429 /* TVCConnectionAlerts.swift */; };
541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 541A957523E602DF00C09C19 /* LaunchIcon.png */; };
541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541AC5D72399498A00A769D7 /* AppDelegate.swift */; };
541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5DB2399498A00A769D7 /* Main.storyboard */; };
541AC5DF2399498B00A769D7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5DE2399498B00A769D7 /* Assets.xcassets */; };
541AC5E22399498B00A769D7 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */; };
541DCA6124A6B0F6005F1A4B /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541DCA6024A6B0F6005F1A4B /* Color.swift */; };
541FC47624A12D01009154D8 /* IBViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541FC47524A12D01009154D8 /* IBViews.swift */; };
541FC47824A1453F009154D8 /* VCCoOccurrence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541FC47724A1453F009154D8 /* VCCoOccurrence.swift */; };
541FC9872497D81C00962623 /* TheGreatDestroyer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541FC9862497D81C00962623 /* TheGreatDestroyer.swift */; };
542E2A982404973F001462DC /* TBCMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542E2A972404973F001462DC /* TBCMain.swift */; };
542E2A9A24051556001462DC /* TVCSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542E2A9924051556001462DC /* TVCSettings.swift */; };
543078AA24B5E12500278F2D /* snap2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 5430789F24B5E12200278F2D /* snap2.caf */; };
543078AB24B5E12500278F2D /* snap2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 5430789F24B5E12200278F2D /* snap2.caf */; };
543078AC24B5E12500278F2D /* typewriter2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A024B5E12200278F2D /* typewriter2.caf */; };
543078AD24B5E12500278F2D /* typewriter2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A024B5E12200278F2D /* typewriter2.caf */; };
543078AE24B5E12500278F2D /* wood1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A124B5E12300278F2D /* wood1.caf */; };
543078AF24B5E12500278F2D /* wood1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A124B5E12300278F2D /* wood1.caf */; };
543078B024B5E12500278F2D /* plop2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A224B5E12300278F2D /* plop2.caf */; };
543078B124B5E12500278F2D /* plop2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A224B5E12300278F2D /* plop2.caf */; };
543078B224B5E12500278F2D /* plop1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A324B5E12300278F2D /* plop1.caf */; };
543078B324B5E12500278F2D /* plop1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A324B5E12300278F2D /* plop1.caf */; };
543078B424B5E12500278F2D /* snap1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A424B5E12300278F2D /* snap1.caf */; };
543078B524B5E12500278F2D /* snap1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A424B5E12300278F2D /* snap1.caf */; };
543078B624B5E12500278F2D /* drum1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A524B5E12300278F2D /* drum1.caf */; };
543078B724B5E12500278F2D /* drum1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A524B5E12300278F2D /* drum1.caf */; };
543078B824B5E12500278F2D /* wood2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A624B5E12400278F2D /* wood2.caf */; };
543078B924B5E12500278F2D /* wood2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A624B5E12400278F2D /* wood2.caf */; };
543078BA24B5E12500278F2D /* typewriter1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A724B5E12400278F2D /* typewriter1.caf */; };
543078BB24B5E12500278F2D /* typewriter1.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A724B5E12400278F2D /* typewriter1.caf */; };
543078BC24B5E12500278F2D /* clock.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A824B5E12400278F2D /* clock.caf */; };
543078BD24B5E12500278F2D /* clock.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A824B5E12400278F2D /* clock.caf */; };
543078BE24B5E12500278F2D /* drum2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A924B5E12500278F2D /* drum2.caf */; };
543078BF24B5E12500278F2D /* drum2.caf in Resources */ = {isa = PBXBuildFile; fileRef = 543078A924B5E12500278F2D /* drum2.caf */; };
543078C324B60F3B00278F2D /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 543078C124B60F3B00278F2D /* Settings.storyboard */; };
543078C924B75CEA00278F2D /* PushNotificationAppOnly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543078C824B75CD100278F2D /* PushNotificationAppOnly.swift */; };
543CDB2023EEE61900B7F323 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543CDB1F23EEE61900B7F323 /* PacketTunnelProvider.swift */; };
543CDB2523EEE61900B7F323 /* GlassVPN.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 543CDB1D23EEE61900B7F323 /* GlassVPN.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
54448A2E2486464F00771C96 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A2D2486464F00771C96 /* Array.swift */; };
54448A30248647D900771C96 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A2F248647D900771C96 /* Time.swift */; };
54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A3124899A4000771C96 /* SearchBarManager.swift */; };
544C95262407B1C700AB89D0 /* SharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544C95252407B1C700AB89D0 /* SharedState.swift */; };
544F912024A67EC5001D4B00 /* TVCOccurrenceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544F911F24A67EC5001D4B00 /* TVCOccurrenceContext.swift */; };
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */; };
545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDCE243E6267003B6544 /* TutorialSheet.swift */; };
545DDDD124436983003B6544 /* QuickUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD024436983003B6544 /* QuickUI.swift */; };
545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD324466D37003B6544 /* AutoLayout.swift */; };
546063E523FEFAFE008F505A /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; };
54686A7624F8062C0084934D /* NotificationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54686A7524F8062C0084934D /* NotificationBanner.swift */; };
54686A8524FD0A3F0084934D /* tut-recording-howto.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8424FD0A3F0084934D /* tut-recording-howto.md */; };
54686A8724FD27AA0084934D /* TinyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54686A8624FD26410084934D /* TinyMarkdown.swift */; };
54686A8D24FD428C0084934D /* tut-welcome-1.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8824FD31580084934D /* tut-welcome-1.md */; };
54686A8E24FD42950084934D /* tut-welcome-2.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8B24FD3F180084934D /* tut-welcome-2.md */; };
54686A8F24FD42950084934D /* tut-recording-1.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8A24FD3F100084934D /* tut-recording-1.md */; };
54686A9024FD42950084934D /* tut-recording-2.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8924FD31630084934D /* tut-recording-2.md */; };
54686A9124FD42950084934D /* tut-cooccurrence.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8C24FD3F630084934D /* tut-cooccurrence.md */; };
54751E512423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; };
54751E522423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; };
54953E3323DC752E0054345C /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; };
@@ -39,23 +85,27 @@
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6023E0D69A0054345C /* TVCHosts.swift */; };
54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6E23E44CD00054345C /* TVCHostDetails.swift */; };
54953E7123E473F10054345C /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 54953E7023E473F10054345C /* Settings.bundle */; };
549A96D62501198400C565FA /* VCEditText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549A96D52501198400C565FA /* VCEditText.swift */; };
549A96DA250419B200C565FA /* CoOccurrence.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 549A96D8250419B200C565FA /* CoOccurrence.storyboard */; };
549D6ED524D5BFDB0032E498 /* TVCAppSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */; };
549ECD9D24A7AD550097571C /* CustomAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549ECD9C24A7AD550097571C /* CustomAlert.swift */; };
54A0CC0924E30C56009B5EC1 /* Recordings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A0CC0724E30C56009B5EC1 /* Recordings.storyboard */; };
54A0CC0C24E30D6F009B5EC1 /* Requests.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A0CC0A24E30D6F009B5EC1 /* Requests.storyboard */; };
54B34594240E6343004C53CC /* TVCFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34593240E6343004C53CC /* TVCFilter.swift */; };
54B34596240F0513004C53CC /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34595240F0513004C53CC /* TableView.swift */; };
54B345A6241BB982004C53CC /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345A5241BB982004C53CC /* Notifications.swift */; };
54B345A9241BBA0B004C53CC /* Generic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345A8241BBA0B004C53CC /* Generic.swift */; };
54B345A9241BBA0B004C53CC /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345A8241BBA0B004C53CC /* Logging.swift */; };
54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AA241BBA5B004C53CC /* AlertSheet.swift */; };
54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AC241BBB00004C53CC /* DBExtensions.swift */; };
54B345B0242264F8004C53CC /* third-level.txt in Resources */ = {isa = PBXBuildFile; fileRef = 54B345AF242264F8004C53CC /* third-level.txt */; };
54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DA23E9E36E00214A3F /* AppInfoType.swift */; };
54C056DB23E9E36E00214A3F /* AppStoreSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */; };
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DC23E9EEF700214A3F /* BundleIcon.swift */; };
54CA01D32426B23D003A5E04 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D22426B23D003A5E04 /* Resolver.swift */; };
54CA01D52426B252003A5E04 /* SafeDict.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D42426B251003A5E04 /* SafeDict.swift */; };
54CA025C2426B2FD003A5E04 /* ConnectSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E22426B2FC003A5E04 /* ConnectSession.swift */; };
54CA025D2426B2FD003A5E04 /* HTTPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E32426B2FC003A5E04 /* HTTPHeader.swift */; };
54CA025E2426B2FD003A5E04 /* ResponseGeneratorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E42426B2FC003A5E04 /* ResponseGeneratorFactory.swift */; };
54CA025F2426B2FD003A5E04 /* ProxyServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E62426B2FC003A5E04 /* ProxyServer.swift */; };
54CA02602426B2FD003A5E04 /* GCDProxyServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E72426B2FC003A5E04 /* GCDProxyServer.swift */; };
54CA02612426B2FD003A5E04 /* GCDSOCKS5ProxyServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E82426B2FC003A5E04 /* GCDSOCKS5ProxyServer.swift */; };
54CA02622426B2FD003A5E04 /* GCDHTTPProxyServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E92426B2FC003A5E04 /* GCDHTTPProxyServer.swift */; };
54CA02662426B2FD003A5E04 /* NWUDPSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01EF2426B2FC003A5E04 /* NWUDPSocket.swift */; };
54CA02672426B2FD003A5E04 /* RawTCPSocketProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01F02426B2FC003A5E04 /* RawTCPSocketProtocol.swift */; };
@@ -77,15 +127,8 @@
54CA027B2426B2FD003A5E04 /* HTTPAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02062426B2FC003A5E04 /* HTTPAuthentication.swift */; };
54CA027C2426B2FD003A5E04 /* StreamScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02072426B2FC003A5E04 /* StreamScanner.swift */; };
54CA027D2426B2FD003A5E04 /* GlobalIntializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02082426B2FC003A5E04 /* GlobalIntializer.swift */; };
54CA027E2426B2FD003A5E04 /* DomainListRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA020A2426B2FC003A5E04 /* DomainListRule.swift */; };
54CA02802426B2FD003A5E04 /* DNSSessionMatchType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA020C2426B2FC003A5E04 /* DNSSessionMatchType.swift */; };
54CA02812426B2FD003A5E04 /* DNSFailRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA020D2426B2FC003A5E04 /* DNSFailRule.swift */; };
54CA02822426B2FD003A5E04 /* AllRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA020E2426B2FC003A5E04 /* AllRule.swift */; };
54CA02832426B2FD003A5E04 /* DNSSessionMatchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA020F2426B2FC003A5E04 /* DNSSessionMatchResult.swift */; };
54CA02842426B2FD003A5E04 /* Rule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02102426B2FC003A5E04 /* Rule.swift */; };
54CA02852426B2FD003A5E04 /* DirectRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02112426B2FC003A5E04 /* DirectRule.swift */; };
54CA02862426B2FD003A5E04 /* RuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02122426B2FC003A5E04 /* RuleManager.swift */; };
54CA02872426B2FD003A5E04 /* IPRangeListRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02132426B2FC003A5E04 /* IPRangeListRule.swift */; };
54CA02882426B2FD003A5E04 /* QueueFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02152426B2FC003A5E04 /* QueueFactory.swift */; };
54CA02892426B2FD003A5E04 /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02162426B2FC003A5E04 /* Tunnel.swift */; };
54CA028A2426B2FD003A5E04 /* ResponseGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02172426B2FC003A5E04 /* ResponseGenerator.swift */; };
@@ -104,30 +147,19 @@
54CA029E2426B2FD003A5E04 /* EventType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02302426B2FC003A5E04 /* EventType.swift */; };
54CA029F2426B2FD003A5E04 /* ProxySocketEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02312426B2FC003A5E04 /* ProxySocketEvent.swift */; };
54CA02A02426B2FD003A5E04 /* TunnelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02322426B2FC003A5E04 /* TunnelEvent.swift */; };
54CA02A12426B2FD003A5E04 /* RuleMatchEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02332426B2FC003A5E04 /* RuleMatchEvent.swift */; };
54CA02A22426B2FD003A5E04 /* ObserverFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02342426B2FC003A5E04 /* ObserverFactory.swift */; };
54CA02A32426B2FD003A5E04 /* HTTPAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02372426B2FC003A5E04 /* HTTPAdapter.swift */; };
54CA02A42426B2FD003A5E04 /* SecureHTTPAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02382426B2FC003A5E04 /* SecureHTTPAdapter.swift */; };
54CA02A62426B2FD003A5E04 /* AdapterSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023A2426B2FC003A5E04 /* AdapterSocket.swift */; };
54CA02A72426B2FD003A5E04 /* DirectAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023B2426B2FC003A5E04 /* DirectAdapter.swift */; };
54CA02A82426B2FD003A5E04 /* SOCKS5Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023C2426B2FC003A5E04 /* SOCKS5Adapter.swift */; };
54CA02A92426B2FD003A5E04 /* RejectAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023D2426B2FC003A5E04 /* RejectAdapter.swift */; };
54CA02AC2426B2FD003A5E04 /* AuthenticationServerAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02412426B2FC003A5E04 /* AuthenticationServerAdapterFactory.swift */; };
54CA02AD2426B2FD003A5E04 /* RejectAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02422426B2FC003A5E04 /* RejectAdapterFactory.swift */; };
54CA02AE2426B2FD003A5E04 /* AdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02432426B2FD003A5E04 /* AdapterFactory.swift */; };
54CA02AF2426B2FD003A5E04 /* SOCKS5AdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02442426B2FD003A5E04 /* SOCKS5AdapterFactory.swift */; };
54CA02B02426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02452426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift */; };
54CA02B12426B2FD003A5E04 /* ServerAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02462426B2FD003A5E04 /* ServerAdapterFactory.swift */; };
54CA02B22426B2FD003A5E04 /* AdapterFactoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02472426B2FD003A5E04 /* AdapterFactoryManager.swift */; };
54CA02B32426B2FD003A5E04 /* HTTPAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02482426B2FD003A5E04 /* HTTPAdapterFactory.swift */; };
54CA02B82426B2FD003A5E04 /* HTTPProxySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA024F2426B2FD003A5E04 /* HTTPProxySocket.swift */; };
54CA02B92426B2FD003A5E04 /* DirectProxySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02502426B2FD003A5E04 /* DirectProxySocket.swift */; };
54CA02BA2426B2FD003A5E04 /* ProxySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02512426B2FD003A5E04 /* ProxySocket.swift */; };
54CA02BB2426B2FD003A5E04 /* SOCKS5ProxySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02522426B2FD003A5E04 /* SOCKS5ProxySocket.swift */; };
54CA02BC2426B2FD003A5E04 /* SocketProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02532426B2FD003A5E04 /* SocketProtocol.swift */; };
54CA02BE2426D4F3003A5E04 /* DDLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02BD2426D4F3003A5E04 /* DDLog.swift */; };
54CA02C32426DCCD003A5E04 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02BF2426DCCC003A5E04 /* GCDAsyncSocket.m */; };
54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02C02426DCCD003A5E04 /* GCDAsyncUdpSocket.m */; };
54CE8BC424B1ED2100CC1756 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */; };
54CE8BC524B1ED2100CC1756 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */; };
54CFE86824E3F401001687DD /* TVCShareRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFE86724E3F401001687DD /* TVCShareRecording.swift */; };
54D8B97A246C9F2000EB2414 /* FilterPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B979246C9F2000EB2414 /* FilterPipeline.swift */; };
54D8B97C2471A7E000EB2414 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97B2471A7E000EB2414 /* String.swift */; };
54D8B97E2471B88900EB2414 /* DBCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97D2471B88900EB2414 /* DBCommon.swift */; };
@@ -135,10 +167,15 @@
54D8B9832471BD8100EB2414 /* DBAppOnly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B9822471BD8100EB2414 /* DBAppOnly.swift */; };
54D8B98624796E9900EB2414 /* GroupedDomainDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */; };
54E540F2247C423200F7C34A /* DomainFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F1247C423200F7C34A /* DomainFilter.swift */; };
54E540F4247D3F2600F7C34A /* TestDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F3247D3F2600F7C34A /* TestDataSource.swift */; };
54E540F4247D3F2600F7C34A /* SimulatorVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F3247D3F2600F7C34A /* SimulatorVPN.swift */; };
54E540F8247DB90F00F7C34A /* RecordingsDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F7247DB90F00F7C34A /* RecordingsDB.swift */; };
54E540FA2482414800F7C34A /* SyncUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F92482414800F7C34A /* SyncUpdate.swift */; };
54EFA4E6248EEE240022D618 /* DatePickerAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EFA4E5248EEE240022D618 /* DatePickerAlert.swift */; };
54E67E4624A8B0FE0025D261 /* PrefsShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4524A8B0FE0025D261 /* PrefsShared.swift */; };
54E67E4924A8B1280025D261 /* Prefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4824A8B1280025D261 /* Prefs.swift */; };
54E67E4B24A8C6370025D261 /* GlassVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4A24A8C6370025D261 /* GlassVPN.swift */; };
54E67E4D24A8E20D0025D261 /* TVCSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542E2A9924051556001462DC /* TVCSettings.swift */; };
54E67E4F24A8E2910025D261 /* Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4E24A8E2910025D261 /* Equatable.swift */; };
54E67E5124A8E8820025D261 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E5024A8E8820025D261 /* View.swift */; };
54EFA4E82491A16A0022D618 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EFA4E72491A16A0022D618 /* Font.swift */; };
/* End PBXBuildFile section */
@@ -167,10 +204,19 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
5404AEEC24A95F3F003B2F54 /* SlideInAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideInAnimation.swift; sourceTree = "<group>"; };
5404AEEE24ACC089003B2F54 /* VCAnalysisBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCAnalysisBar.swift; sourceTree = "<group>"; };
540E677F242D2CF100871BBE /* VCRecordings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCRecordings.swift; sourceTree = "<group>"; };
540E67812433483D00871BBE /* VCEditRecording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCEditRecording.swift; sourceTree = "<group>"; };
540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCPreviousRecords.swift; sourceTree = "<group>"; };
541075CD24C9D43A00D6F1BF /* UNNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotification.swift; sourceTree = "<group>"; };
541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThrottledBatchQueue.swift; sourceTree = "<group>"; };
541075D424CE286200D6F1BF /* CachedConnectionAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedConnectionAlert.swift; sourceTree = "<group>"; };
541075D824CE2C7200D6F1BF /* GlassVPNHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassVPNHook.swift; sourceTree = "<group>"; };
5412F8ED24571B8100A63D7A /* VCDateFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCDateFilter.swift; sourceTree = "<group>"; };
5412FCBF24C628F9000DE429 /* TVCReminderAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TVCReminderAlerts.swift; sourceTree = "<group>"; };
5412FCC024C628F9000DE429 /* TVCChooseAlertTone.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TVCChooseAlertTone.swift; sourceTree = "<group>"; };
5412FCC124C628FA000DE429 /* TVCConnectionAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TVCConnectionAlerts.swift; sourceTree = "<group>"; };
541A957523E602DF00C09C19 /* LaunchIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = LaunchIcon.png; sourceTree = "<group>"; };
541AC5D42399498A00A769D7 /* AppCheck.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppCheck.app; sourceTree = BUILT_PRODUCTS_DIR; };
541AC5D72399498A00A769D7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -178,11 +224,25 @@
541AC5DE2399498B00A769D7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
541AC5E12399498B00A769D7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
541AC5E32399498B00A769D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
541DCA6024A6B0F6005F1A4B /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
541FC47524A12D01009154D8 /* IBViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IBViews.swift; sourceTree = "<group>"; };
541FC47724A1453F009154D8 /* VCCoOccurrence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCCoOccurrence.swift; sourceTree = "<group>"; };
541FC9862497D81C00962623 /* TheGreatDestroyer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TheGreatDestroyer.swift; sourceTree = "<group>"; };
542E2A972404973F001462DC /* TBCMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TBCMain.swift; sourceTree = "<group>"; };
542E2A9924051556001462DC /* TVCSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCSettings.swift; sourceTree = "<group>"; };
5430789F24B5E12200278F2D /* snap2.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = snap2.caf; sourceTree = "<group>"; };
543078A024B5E12200278F2D /* typewriter2.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = typewriter2.caf; sourceTree = "<group>"; };
543078A124B5E12300278F2D /* wood1.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = wood1.caf; sourceTree = "<group>"; };
543078A224B5E12300278F2D /* plop2.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = plop2.caf; sourceTree = "<group>"; };
543078A324B5E12300278F2D /* plop1.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = plop1.caf; sourceTree = "<group>"; };
543078A424B5E12300278F2D /* snap1.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = snap1.caf; sourceTree = "<group>"; };
543078A524B5E12300278F2D /* drum1.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = drum1.caf; sourceTree = "<group>"; };
543078A624B5E12400278F2D /* wood2.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = wood2.caf; sourceTree = "<group>"; };
543078A724B5E12400278F2D /* typewriter1.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = typewriter1.caf; sourceTree = "<group>"; };
543078A824B5E12400278F2D /* clock.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = clock.caf; sourceTree = "<group>"; };
543078A924B5E12500278F2D /* drum2.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = drum2.caf; sourceTree = "<group>"; };
543078C224B60F3B00278F2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Settings.storyboard; sourceTree = "<group>"; };
543078C824B75CD100278F2D /* PushNotificationAppOnly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationAppOnly.swift; sourceTree = "<group>"; };
543CDB1D23EEE61900B7F323 /* GlassVPN.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = GlassVPN.appex; sourceTree = BUILT_PRODUCTS_DIR; };
543CDB1F23EEE61900B7F323 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
543CDB2123EEE61900B7F323 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -190,36 +250,48 @@
54448A2D2486464F00771C96 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
54448A2F248647D900771C96 /* Time.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = "<group>"; };
54448A3124899A4000771C96 /* SearchBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarManager.swift; sourceTree = "<group>"; };
544C95252407B1C700AB89D0 /* SharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedState.swift; sourceTree = "<group>"; };
544F911F24A67EC5001D4B00 /* TVCOccurrenceContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCOccurrenceContext.swift; sourceTree = "<group>"; };
5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCRecordingDetails.swift; sourceTree = "<group>"; };
545DDDCE243E6267003B6544 /* TutorialSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialSheet.swift; sourceTree = "<group>"; };
545DDDD024436983003B6544 /* QuickUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickUI.swift; sourceTree = "<group>"; };
545DDDD324466D37003B6544 /* AutoLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoLayout.swift; sourceTree = "<group>"; };
54686A7524F8062C0084934D /* NotificationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationBanner.swift; sourceTree = "<group>"; };
54686A8424FD0A3F0084934D /* tut-recording-howto.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = "tut-recording-howto.md"; sourceTree = "<group>"; };
54686A8624FD26410084934D /* TinyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TinyMarkdown.swift; sourceTree = "<group>"; };
54686A8824FD31580084934D /* tut-welcome-1.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-welcome-1.md"; sourceTree = "<group>"; };
54686A8924FD31630084934D /* tut-recording-2.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-recording-2.md"; sourceTree = "<group>"; };
54686A8A24FD3F100084934D /* tut-recording-1.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-recording-1.md"; sourceTree = "<group>"; };
54686A8B24FD3F180084934D /* tut-welcome-2.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-welcome-2.md"; sourceTree = "<group>"; };
54686A8C24FD3F630084934D /* tut-cooccurrence.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-cooccurrence.md"; sourceTree = "<group>"; };
54751E502423955000168273 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
548B1F9423D338EC005B047C /* main.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = main.entitlements; sourceTree = "<group>"; };
54953E5E23DEBE840054345C /* TVCDomains.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCDomains.swift; sourceTree = "<group>"; };
54953E6023E0D69A0054345C /* TVCHosts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHosts.swift; sourceTree = "<group>"; };
54953E6E23E44CD00054345C /* TVCHostDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHostDetails.swift; sourceTree = "<group>"; };
54953E7023E473F10054345C /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
549A96D52501198400C565FA /* VCEditText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCEditText.swift; sourceTree = "<group>"; };
549A96D9250419B200C565FA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CoOccurrence.storyboard; sourceTree = "<group>"; };
549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCAppSearch.swift; sourceTree = "<group>"; };
549ECD9C24A7AD550097571C /* CustomAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlert.swift; sourceTree = "<group>"; };
54A0CC0824E30C56009B5EC1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Recordings.storyboard; sourceTree = "<group>"; };
54A0CC0B24E30D6F009B5EC1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Requests.storyboard; sourceTree = "<group>"; };
54B34593240E6343004C53CC /* TVCFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCFilter.swift; sourceTree = "<group>"; };
54B34595240F0513004C53CC /* TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = "<group>"; };
54B345A5241BB982004C53CC /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
54B345A8241BBA0B004C53CC /* Generic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generic.swift; sourceTree = "<group>"; };
54B345A8241BBA0B004C53CC /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
54B345AA241BBA5B004C53CC /* AlertSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertSheet.swift; sourceTree = "<group>"; };
54B345AC241BBB00004C53CC /* DBExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBExtensions.swift; sourceTree = "<group>"; };
54B345AF242264F8004C53CC /* third-level.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "third-level.txt"; sourceTree = "<group>"; };
54B7562223D7B2DC008F0C41 /* DBCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBCore.swift; sourceTree = "<group>"; };
54C056DA23E9E36E00214A3F /* AppInfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoType.swift; sourceTree = "<group>"; };
54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreSearch.swift; sourceTree = "<group>"; };
54C056DC23E9EEF700214A3F /* BundleIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIcon.swift; sourceTree = "<group>"; };
54CA00D62426A803003A5E04 /* CocoaAsyncSocket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CocoaAsyncSocket.h; sourceTree = "<group>"; };
54CA01D22426B23D003A5E04 /* Resolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = "<group>"; };
54CA01D42426B251003A5E04 /* SafeDict.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SafeDict.swift; sourceTree = "<group>"; };
54CA01E22426B2FC003A5E04 /* ConnectSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectSession.swift; sourceTree = "<group>"; };
54CA01E32426B2FC003A5E04 /* HTTPHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPHeader.swift; sourceTree = "<group>"; };
54CA01E42426B2FC003A5E04 /* ResponseGeneratorFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseGeneratorFactory.swift; sourceTree = "<group>"; };
54CA01E62426B2FC003A5E04 /* ProxyServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxyServer.swift; sourceTree = "<group>"; };
54CA01E72426B2FC003A5E04 /* GCDProxyServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GCDProxyServer.swift; sourceTree = "<group>"; };
54CA01E82426B2FC003A5E04 /* GCDSOCKS5ProxyServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GCDSOCKS5ProxyServer.swift; sourceTree = "<group>"; };
54CA01E92426B2FC003A5E04 /* GCDHTTPProxyServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GCDHTTPProxyServer.swift; sourceTree = "<group>"; };
54CA01EF2426B2FC003A5E04 /* NWUDPSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NWUDPSocket.swift; sourceTree = "<group>"; };
54CA01F02426B2FC003A5E04 /* RawTCPSocketProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawTCPSocketProtocol.swift; sourceTree = "<group>"; };
@@ -241,15 +313,8 @@
54CA02062426B2FC003A5E04 /* HTTPAuthentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPAuthentication.swift; sourceTree = "<group>"; };
54CA02072426B2FC003A5E04 /* StreamScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StreamScanner.swift; sourceTree = "<group>"; };
54CA02082426B2FC003A5E04 /* GlobalIntializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalIntializer.swift; sourceTree = "<group>"; };
54CA020A2426B2FC003A5E04 /* DomainListRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListRule.swift; sourceTree = "<group>"; };
54CA020C2426B2FC003A5E04 /* DNSSessionMatchType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSSessionMatchType.swift; sourceTree = "<group>"; };
54CA020D2426B2FC003A5E04 /* DNSFailRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSFailRule.swift; sourceTree = "<group>"; };
54CA020E2426B2FC003A5E04 /* AllRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllRule.swift; sourceTree = "<group>"; };
54CA020F2426B2FC003A5E04 /* DNSSessionMatchResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSSessionMatchResult.swift; sourceTree = "<group>"; };
54CA02102426B2FC003A5E04 /* Rule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Rule.swift; sourceTree = "<group>"; };
54CA02112426B2FC003A5E04 /* DirectRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectRule.swift; sourceTree = "<group>"; };
54CA02122426B2FC003A5E04 /* RuleManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuleManager.swift; sourceTree = "<group>"; };
54CA02132426B2FC003A5E04 /* IPRangeListRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPRangeListRule.swift; sourceTree = "<group>"; };
54CA02152426B2FC003A5E04 /* QueueFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueueFactory.swift; sourceTree = "<group>"; };
54CA02162426B2FC003A5E04 /* Tunnel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = "<group>"; };
54CA02172426B2FC003A5E04 /* ResponseGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseGenerator.swift; sourceTree = "<group>"; };
@@ -268,42 +333,34 @@
54CA02302426B2FC003A5E04 /* EventType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventType.swift; sourceTree = "<group>"; };
54CA02312426B2FC003A5E04 /* ProxySocketEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxySocketEvent.swift; sourceTree = "<group>"; };
54CA02322426B2FC003A5E04 /* TunnelEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelEvent.swift; sourceTree = "<group>"; };
54CA02332426B2FC003A5E04 /* RuleMatchEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuleMatchEvent.swift; sourceTree = "<group>"; };
54CA02342426B2FC003A5E04 /* ObserverFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObserverFactory.swift; sourceTree = "<group>"; };
54CA02372426B2FC003A5E04 /* HTTPAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPAdapter.swift; sourceTree = "<group>"; };
54CA02382426B2FC003A5E04 /* SecureHTTPAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureHTTPAdapter.swift; sourceTree = "<group>"; };
54CA023A2426B2FC003A5E04 /* AdapterSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterSocket.swift; sourceTree = "<group>"; };
54CA023B2426B2FC003A5E04 /* DirectAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectAdapter.swift; sourceTree = "<group>"; };
54CA023C2426B2FC003A5E04 /* SOCKS5Adapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SOCKS5Adapter.swift; sourceTree = "<group>"; };
54CA023D2426B2FC003A5E04 /* RejectAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RejectAdapter.swift; sourceTree = "<group>"; };
54CA02412426B2FC003A5E04 /* AuthenticationServerAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationServerAdapterFactory.swift; sourceTree = "<group>"; };
54CA02422426B2FC003A5E04 /* RejectAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RejectAdapterFactory.swift; sourceTree = "<group>"; };
54CA02432426B2FD003A5E04 /* AdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterFactory.swift; sourceTree = "<group>"; };
54CA02442426B2FD003A5E04 /* SOCKS5AdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SOCKS5AdapterFactory.swift; sourceTree = "<group>"; };
54CA02452426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureHTTPAdapterFactory.swift; sourceTree = "<group>"; };
54CA02462426B2FD003A5E04 /* ServerAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerAdapterFactory.swift; sourceTree = "<group>"; };
54CA02472426B2FD003A5E04 /* AdapterFactoryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterFactoryManager.swift; sourceTree = "<group>"; };
54CA02482426B2FD003A5E04 /* HTTPAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPAdapterFactory.swift; sourceTree = "<group>"; };
54CA024F2426B2FD003A5E04 /* HTTPProxySocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPProxySocket.swift; sourceTree = "<group>"; };
54CA02502426B2FD003A5E04 /* DirectProxySocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectProxySocket.swift; sourceTree = "<group>"; };
54CA02512426B2FD003A5E04 /* ProxySocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxySocket.swift; sourceTree = "<group>"; };
54CA02522426B2FD003A5E04 /* SOCKS5ProxySocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SOCKS5ProxySocket.swift; sourceTree = "<group>"; };
54CA02532426B2FD003A5E04 /* SocketProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketProtocol.swift; sourceTree = "<group>"; };
54CA02BD2426D4F3003A5E04 /* DDLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDLog.swift; sourceTree = "<group>"; };
54CA02BF2426DCCC003A5E04 /* GCDAsyncSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncSocket.m; sourceTree = "<group>"; };
54CA02C02426DCCD003A5E04 /* GCDAsyncUdpSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncUdpSocket.m; sourceTree = "<group>"; };
54CA02C12426DCCD003A5E04 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocket.h; sourceTree = "<group>"; };
54CA02C22426DCCD003A5E04 /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncUdpSocket.h; sourceTree = "<group>"; };
54CE8BC324B1ED2100CC1756 /* PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotification.swift; sourceTree = "<group>"; };
54CFE86724E3F401001687DD /* TVCShareRecording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCShareRecording.swift; sourceTree = "<group>"; };
54D8B979246C9F2000EB2414 /* FilterPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterPipeline.swift; sourceTree = "<group>"; };
54D8B97B2471A7E000EB2414 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
54D8B97D2471B88900EB2414 /* DBCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBCommon.swift; sourceTree = "<group>"; };
54D8B9822471BD8100EB2414 /* DBAppOnly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBAppOnly.swift; sourceTree = "<group>"; };
54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedDomainDataSource.swift; sourceTree = "<group>"; };
54E540F1247C423200F7C34A /* DomainFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainFilter.swift; sourceTree = "<group>"; };
54E540F3247D3F2600F7C34A /* TestDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDataSource.swift; sourceTree = "<group>"; };
54E540F3247D3F2600F7C34A /* SimulatorVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorVPN.swift; sourceTree = "<group>"; };
54E540F7247DB90F00F7C34A /* RecordingsDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingsDB.swift; sourceTree = "<group>"; };
54E540F92482414800F7C34A /* SyncUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncUpdate.swift; sourceTree = "<group>"; };
54EFA4E5248EEE240022D618 /* DatePickerAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerAlert.swift; sourceTree = "<group>"; };
54E67E4524A8B0FE0025D261 /* PrefsShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsShared.swift; sourceTree = "<group>"; };
54E67E4824A8B1280025D261 /* Prefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Prefs.swift; sourceTree = "<group>"; };
54E67E4A24A8C6370025D261 /* GlassVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassVPN.swift; sourceTree = "<group>"; };
54E67E4E24A8E2910025D261 /* Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Equatable.swift; sourceTree = "<group>"; };
54E67E5024A8E8820025D261 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
54EFA4E72491A16A0022D618 /* Font.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Font.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -332,7 +389,8 @@
5412F8ED24571B8100A63D7A /* VCDateFilter.swift */,
54953E6023E0D69A0054345C /* TVCHosts.swift */,
54953E6E23E44CD00054345C /* TVCHostDetails.swift */,
541FC47424A12CE9009154D8 /* Analytics */,
544F911F24A67EC5001D4B00 /* TVCOccurrenceContext.swift */,
541FC47424A12CE9009154D8 /* Analysis */,
);
path = Requests;
sourceTree = "<group>";
@@ -342,6 +400,9 @@
children = (
542E2A9924051556001462DC /* TVCSettings.swift */,
54B34593240E6343004C53CC /* TVCFilter.swift */,
5412FCBF24C628F9000DE429 /* TVCReminderAlerts.swift */,
5412FCC124C628FA000DE429 /* TVCConnectionAlerts.swift */,
5412FCC024C628F9000DE429 /* TVCChooseAlertTone.swift */,
);
path = Settings;
sourceTree = "<group>";
@@ -351,12 +412,27 @@
children = (
540E677F242D2CF100871BBE /* VCRecordings.swift */,
540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */,
540E67812433483D00871BBE /* VCEditRecording.swift */,
5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */,
54CFE86724E3F401001687DD /* TVCShareRecording.swift */,
549A96D52501198400C565FA /* VCEditText.swift */,
540E67812433483D00871BBE /* VCEditRecording.swift */,
549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */,
54B345B12422E029004C53CC /* App Icons */,
);
path = Recordings;
sourceTree = "<group>";
};
541075D324CE284700D6F1BF /* Push Notifications */ = {
isa = PBXGroup;
children = (
541075CD24C9D43A00D6F1BF /* UNNotification.swift */,
54CE8BC324B1ED2100CC1756 /* PushNotification.swift */,
543078C824B75CD100278F2D /* PushNotificationAppOnly.swift */,
541075D424CE286200D6F1BF /* CachedConnectionAlert.swift */,
);
path = "Push Notifications";
sourceTree = "<group>";
};
541AC5CB2399498A00A769D7 = {
isa = PBXGroup;
children = (
@@ -383,15 +459,16 @@
54E540F0247C386500F7C34A /* Data Source */,
54B345A4241BB975004C53CC /* Extensions */,
545DDDD224436A03003B6544 /* Common Classes */,
541075D324CE284700D6F1BF /* Push Notifications */,
548B1F9423D338EC005B047C /* main.entitlements */,
541AC5D72399498A00A769D7 /* AppDelegate.swift */,
54E67E4A24A8C6370025D261 /* GlassVPN.swift */,
541075D824CE2C7200D6F1BF /* GlassVPNHook.swift */,
542E2A972404973F001462DC /* TBCMain.swift */,
540C6454240D5BAE00E948F9 /* Requests */,
540E677E242D2CD200871BBE /* Recordings */,
540C6455240D5BD200E948F9 /* Settings */,
54B345B12422E029004C53CC /* unused */,
541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */,
541AC5DB2399498A00A769D7 /* Main.storyboard */,
54A0CC0D24E314B6009B5EC1 /* GUI */,
541AC5DE2399498B00A769D7 /* Assets.xcassets */,
541AC5E32399498B00A769D7 /* Info.plist */,
54953E7023E473F10054345C /* Settings.bundle */,
@@ -399,23 +476,44 @@
path = main;
sourceTree = "<group>";
};
541FC47424A12CE9009154D8 /* Analytics */ = {
541FC47424A12CE9009154D8 /* Analysis */ = {
isa = PBXGroup;
children = (
5404AEEE24ACC089003B2F54 /* VCAnalysisBar.swift */,
541FC47724A1453F009154D8 /* VCCoOccurrence.swift */,
);
path = Analytics;
path = Analysis;
sourceTree = "<group>";
};
542E2A9B24051F79001462DC /* media */ = {
isa = PBXGroup;
children = (
54686A8324FD0A3F0084934D /* tutorials */,
5430789E24B5E10E00278F2D /* sounds */,
541A957523E602DF00C09C19 /* LaunchIcon.png */,
54B345AF242264F8004C53CC /* third-level.txt */,
);
path = media;
sourceTree = "<group>";
};
5430789E24B5E10E00278F2D /* sounds */ = {
isa = PBXGroup;
children = (
543078A824B5E12400278F2D /* clock.caf */,
543078A524B5E12300278F2D /* drum1.caf */,
543078A924B5E12500278F2D /* drum2.caf */,
543078A324B5E12300278F2D /* plop1.caf */,
543078A224B5E12300278F2D /* plop2.caf */,
543078A424B5E12300278F2D /* snap1.caf */,
5430789F24B5E12200278F2D /* snap2.caf */,
543078A724B5E12400278F2D /* typewriter1.caf */,
543078A024B5E12200278F2D /* typewriter2.caf */,
543078A124B5E12300278F2D /* wood1.caf */,
543078A624B5E12400278F2D /* wood2.caf */,
);
path = sounds;
sourceTree = "<group>";
};
543CDB1E23EEE61900B7F323 /* GlassVPN */ = {
isa = PBXGroup;
children = (
@@ -433,16 +531,48 @@
545DDDD224436A03003B6544 /* Common Classes */ = {
isa = PBXGroup;
children = (
54E67E4824A8B1280025D261 /* Prefs.swift */,
54E67E4524A8B0FE0025D261 /* PrefsShared.swift */,
545DDDD024436983003B6544 /* QuickUI.swift */,
545DDDCE243E6267003B6544 /* TutorialSheet.swift */,
54686A8624FD26410084934D /* TinyMarkdown.swift */,
54D8B979246C9F2000EB2414 /* FilterPipeline.swift */,
54448A3124899A4000771C96 /* SearchBarManager.swift */,
54EFA4E5248EEE240022D618 /* DatePickerAlert.swift */,
549ECD9C24A7AD550097571C /* CustomAlert.swift */,
541FC47524A12D01009154D8 /* IBViews.swift */,
5404AEEC24A95F3F003B2F54 /* SlideInAnimation.swift */,
541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */,
54686A7524F8062C0084934D /* NotificationBanner.swift */,
);
path = "Common Classes";
sourceTree = "<group>";
};
54686A8324FD0A3F0084934D /* tutorials */ = {
isa = PBXGroup;
children = (
54686A8824FD31580084934D /* tut-welcome-1.md */,
54686A8B24FD3F180084934D /* tut-welcome-2.md */,
54686A8A24FD3F100084934D /* tut-recording-1.md */,
54686A8924FD31630084934D /* tut-recording-2.md */,
54686A8424FD0A3F0084934D /* tut-recording-howto.md */,
54686A8C24FD3F630084934D /* tut-cooccurrence.md */,
);
path = tutorials;
sourceTree = "<group>";
};
54A0CC0D24E314B6009B5EC1 /* GUI */ = {
isa = PBXGroup;
children = (
541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */,
541AC5DB2399498A00A769D7 /* Main.storyboard */,
54A0CC0A24E30D6F009B5EC1 /* Requests.storyboard */,
549A96D8250419B200C565FA /* CoOccurrence.storyboard */,
54A0CC0724E30C56009B5EC1 /* Recordings.storyboard */,
543078C124B60F3B00278F2D /* Settings.storyboard */,
);
path = GUI;
sourceTree = "<group>";
};
54B3459A2415651C004C53CC /* DB */ = {
isa = PBXGroup;
children = (
@@ -458,10 +588,12 @@
54B345A4241BB975004C53CC /* Extensions */ = {
isa = PBXGroup;
children = (
544C95252407B1C700AB89D0 /* SharedState.swift */,
54B345A8241BBA0B004C53CC /* Generic.swift */,
54B345A8241BBA0B004C53CC /* Logging.swift */,
54E67E4E24A8E2910025D261 /* Equatable.swift */,
54B345A5241BB982004C53CC /* Notifications.swift */,
54B345AA241BBA5B004C53CC /* AlertSheet.swift */,
54E67E5024A8E8820025D261 /* View.swift */,
541DCA6024A6B0F6005F1A4B /* Color.swift */,
54448A2F248647D900771C96 /* Time.swift */,
54751E502423955000168273 /* URL.swift */,
54EFA4E72491A16A0022D618 /* Font.swift */,
@@ -473,13 +605,13 @@
path = Extensions;
sourceTree = "<group>";
};
54B345B12422E029004C53CC /* unused */ = {
54B345B12422E029004C53CC /* App Icons */ = {
isa = PBXGroup;
children = (
54C056DC23E9EEF700214A3F /* BundleIcon.swift */,
54C056DA23E9E36E00214A3F /* AppInfoType.swift */,
54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */,
);
path = unused;
path = "App Icons";
sourceTree = "<group>";
};
54CA00D52426A7F2003A5E04 /* robbiehanson-CocoaAsyncSocket */ = {
@@ -507,7 +639,6 @@
isa = PBXGroup;
children = (
54CA01E12426B2FC003A5E04 /* Messages */,
54CA01E42426B2FC003A5E04 /* ResponseGeneratorFactory.swift */,
54CA01E52426B2FC003A5E04 /* ProxyServer */,
54CA01EE2426B2FC003A5E04 /* RawSocket */,
54CA01F72426B2FC003A5E04 /* Opt.swift */,
@@ -538,7 +669,6 @@
children = (
54CA01E62426B2FC003A5E04 /* ProxyServer.swift */,
54CA01E72426B2FC003A5E04 /* GCDProxyServer.swift */,
54CA01E82426B2FC003A5E04 /* GCDSOCKS5ProxyServer.swift */,
54CA01E92426B2FC003A5E04 /* GCDHTTPProxyServer.swift */,
);
path = ProxyServer;
@@ -579,15 +709,8 @@
54CA02092426B2FC003A5E04 /* Rule */ = {
isa = PBXGroup;
children = (
54CA020A2426B2FC003A5E04 /* DomainListRule.swift */,
54CA020C2426B2FC003A5E04 /* DNSSessionMatchType.swift */,
54CA020D2426B2FC003A5E04 /* DNSFailRule.swift */,
54CA020E2426B2FC003A5E04 /* AllRule.swift */,
54CA020F2426B2FC003A5E04 /* DNSSessionMatchResult.swift */,
54CA02102426B2FC003A5E04 /* Rule.swift */,
54CA02112426B2FC003A5E04 /* DirectRule.swift */,
54CA02122426B2FC003A5E04 /* RuleManager.swift */,
54CA02132426B2FC003A5E04 /* IPRangeListRule.swift */,
);
path = Rule;
sourceTree = "<group>";
@@ -650,7 +773,6 @@
54CA02302426B2FC003A5E04 /* EventType.swift */,
54CA02312426B2FC003A5E04 /* ProxySocketEvent.swift */,
54CA02322426B2FC003A5E04 /* TunnelEvent.swift */,
54CA02332426B2FC003A5E04 /* RuleMatchEvent.swift */,
);
path = Event;
sourceTree = "<group>";
@@ -668,12 +790,8 @@
54CA02362426B2FC003A5E04 /* AdapterSocket */ = {
isa = PBXGroup;
children = (
54CA02372426B2FC003A5E04 /* HTTPAdapter.swift */,
54CA02382426B2FC003A5E04 /* SecureHTTPAdapter.swift */,
54CA023A2426B2FC003A5E04 /* AdapterSocket.swift */,
54CA023B2426B2FC003A5E04 /* DirectAdapter.swift */,
54CA023C2426B2FC003A5E04 /* SOCKS5Adapter.swift */,
54CA023D2426B2FC003A5E04 /* RejectAdapter.swift */,
54CA023E2426B2FC003A5E04 /* Factory */,
);
path = AdapterSocket;
@@ -682,14 +800,7 @@
54CA023E2426B2FC003A5E04 /* Factory */ = {
isa = PBXGroup;
children = (
54CA02412426B2FC003A5E04 /* AuthenticationServerAdapterFactory.swift */,
54CA02422426B2FC003A5E04 /* RejectAdapterFactory.swift */,
54CA02432426B2FD003A5E04 /* AdapterFactory.swift */,
54CA02442426B2FD003A5E04 /* SOCKS5AdapterFactory.swift */,
54CA02452426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift */,
54CA02462426B2FD003A5E04 /* ServerAdapterFactory.swift */,
54CA02472426B2FD003A5E04 /* AdapterFactoryManager.swift */,
54CA02482426B2FD003A5E04 /* HTTPAdapterFactory.swift */,
);
path = Factory;
sourceTree = "<group>";
@@ -698,9 +809,7 @@
isa = PBXGroup;
children = (
54CA024F2426B2FD003A5E04 /* HTTPProxySocket.swift */,
54CA02502426B2FD003A5E04 /* DirectProxySocket.swift */,
54CA02512426B2FD003A5E04 /* ProxySocket.swift */,
54CA02522426B2FD003A5E04 /* SOCKS5ProxySocket.swift */,
);
path = ProxySocket;
sourceTree = "<group>";
@@ -708,7 +817,7 @@
54E540F0247C386500F7C34A /* Data Source */ = {
isa = PBXGroup;
children = (
54E540F3247D3F2600F7C34A /* TestDataSource.swift */,
54E540F3247D3F2600F7C34A /* SimulatorVPN.swift */,
54E540F92482414800F7C34A /* SyncUpdate.swift */,
54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */,
54E540F1247C423200F7C34A /* DomainFilter.swift */,
@@ -767,7 +876,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1130;
LastUpgradeCheck = 1010;
LastUpgradeCheck = 1200;
ORGANIZATIONNAME = relikd;
TargetAttributes = {
541AC5D32399498A00A769D7 = {
@@ -811,11 +920,32 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
543078AC24B5E12500278F2D /* typewriter2.caf in Resources */,
54953E7123E473F10054345C /* Settings.bundle in Resources */,
54686A9024FD42950084934D /* tut-recording-2.md in Resources */,
543078B024B5E12500278F2D /* plop2.caf in Resources */,
541AC5E22399498B00A769D7 /* LaunchScreen.storyboard in Resources */,
54A0CC0924E30C56009B5EC1 /* Recordings.storyboard in Resources */,
54686A8D24FD428C0084934D /* tut-welcome-1.md in Resources */,
543078B824B5E12500278F2D /* wood2.caf in Resources */,
543078BE24B5E12500278F2D /* drum2.caf in Resources */,
543078B424B5E12500278F2D /* snap1.caf in Resources */,
541AC5DF2399498B00A769D7 /* Assets.xcassets in Resources */,
543078AE24B5E12500278F2D /* wood1.caf in Resources */,
54686A8524FD0A3F0084934D /* tut-recording-howto.md in Resources */,
541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */,
54B345B0242264F8004C53CC /* third-level.txt in Resources */,
54686A8F24FD42950084934D /* tut-recording-1.md in Resources */,
543078BA24B5E12500278F2D /* typewriter1.caf in Resources */,
543078B224B5E12500278F2D /* plop1.caf in Resources */,
543078B624B5E12500278F2D /* drum1.caf in Resources */,
543078BC24B5E12500278F2D /* clock.caf in Resources */,
543078C324B60F3B00278F2D /* Settings.storyboard in Resources */,
543078AA24B5E12500278F2D /* snap2.caf in Resources */,
54A0CC0C24E30D6F009B5EC1 /* Requests.storyboard in Resources */,
549A96DA250419B200C565FA /* CoOccurrence.storyboard in Resources */,
54686A9124FD42950084934D /* tut-cooccurrence.md in Resources */,
54686A8E24FD42950084934D /* tut-welcome-2.md in Resources */,
541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -824,6 +954,17 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
543078AD24B5E12500278F2D /* typewriter2.caf in Resources */,
543078BB24B5E12500278F2D /* typewriter1.caf in Resources */,
543078BF24B5E12500278F2D /* drum2.caf in Resources */,
543078AF24B5E12500278F2D /* wood1.caf in Resources */,
543078B124B5E12500278F2D /* plop2.caf in Resources */,
543078AB24B5E12500278F2D /* snap2.caf in Resources */,
543078B924B5E12500278F2D /* wood2.caf in Resources */,
543078B724B5E12500278F2D /* drum1.caf in Resources */,
543078B524B5E12500278F2D /* snap1.caf in Resources */,
543078B324B5E12500278F2D /* plop1.caf in Resources */,
543078BD24B5E12500278F2D /* clock.caf in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -834,40 +975,59 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54E67E4924A8B1280025D261 /* Prefs.swift in Sources */,
54E540F8247DB90F00F7C34A /* RecordingsDB.swift in Sources */,
54E540F4247D3F2600F7C34A /* TestDataSource.swift in Sources */,
54E67E4F24A8E2910025D261 /* Equatable.swift in Sources */,
54E540F4247D3F2600F7C34A /* SimulatorVPN.swift in Sources */,
541075D924CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */,
541075D524CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */,
5404AEEF24ACC089003B2F54 /* VCAnalysisBar.swift in Sources */,
545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */,
54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */,
54E540F2247C423200F7C34A /* DomainFilter.swift in Sources */,
54D8B97E2471B88900EB2414 /* DBCommon.swift in Sources */,
54D8B9832471BD8100EB2414 /* DBAppOnly.swift in Sources */,
5412FCC224C628FA000DE429 /* TVCReminderAlerts.swift in Sources */,
54E67E4D24A8E20D0025D261 /* TVCSettings.swift in Sources */,
540E67842433FAFE00871BBE /* TVCPreviousRecords.swift in Sources */,
54B345A6241BB982004C53CC /* Notifications.swift in Sources */,
54448A2E2486464F00771C96 /* Array.swift in Sources */,
54E67E4B24A8C6370025D261 /* GlassVPN.swift in Sources */,
54686A7624F8062C0084934D /* NotificationBanner.swift in Sources */,
541FC47824A1453F009154D8 /* VCCoOccurrence.swift in Sources */,
54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */,
544C95262407B1C700AB89D0 /* SharedState.swift in Sources */,
541DCA6124A6B0F6005F1A4B /* Color.swift in Sources */,
545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */,
54B345A9241BBA0B004C53CC /* Generic.swift in Sources */,
54B345A9241BBA0B004C53CC /* Logging.swift in Sources */,
54B34596240F0513004C53CC /* TableView.swift in Sources */,
540E6780242D2CF100871BBE /* VCRecordings.swift in Sources */,
541075D124CDBA0000D6F1BF /* ThrottledBatchQueue.swift in Sources */,
54953E3323DC752E0054345C /* DBCore.swift in Sources */,
54E67E4624A8B0FE0025D261 /* PrefsShared.swift in Sources */,
5412FCC324C628FA000DE429 /* TVCChooseAlertTone.swift in Sources */,
544F912024A67EC5001D4B00 /* TVCOccurrenceContext.swift in Sources */,
54448A30248647D900771C96 /* Time.swift in Sources */,
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */,
549D6ED524D5BFDB0032E498 /* TVCAppSearch.swift in Sources */,
54CFE86824E3F401001687DD /* TVCShareRecording.swift in Sources */,
54751E512423955100168273 /* URL.swift in Sources */,
542E2A9A24051556001462DC /* TVCSettings.swift in Sources */,
54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */,
54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */,
54C056DB23E9E36E00214A3F /* AppStoreSearch.swift in Sources */,
54D8B98624796E9900EB2414 /* GroupedDomainDataSource.swift in Sources */,
541075CE24C9D43A00D6F1BF /* UNNotification.swift in Sources */,
54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */,
54D8B97C2471A7E000EB2414 /* String.swift in Sources */,
54EFA4E6248EEE240022D618 /* DatePickerAlert.swift in Sources */,
54E67E5124A8E8820025D261 /* View.swift in Sources */,
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */,
542E2A982404973F001462DC /* TBCMain.swift in Sources */,
54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */,
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */,
549A96D62501198400C565FA /* VCEditText.swift in Sources */,
541FC9872497D81C00962623 /* TheGreatDestroyer.swift in Sources */,
54686A8724FD27AA0084934D /* TinyMarkdown.swift in Sources */,
5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */,
5412FCC424C628FA000DE429 /* TVCConnectionAlerts.swift in Sources */,
543078C924B75CEA00278F2D /* PushNotificationAppOnly.swift in Sources */,
545DDDD124436983003B6544 /* QuickUI.swift in Sources */,
541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */,
541FC47624A12D01009154D8 /* IBViews.swift in Sources */,
@@ -875,7 +1035,10 @@
54EFA4E82491A16A0022D618 /* Font.swift in Sources */,
54D8B97A246C9F2000EB2414 /* FilterPipeline.swift in Sources */,
54B34594240E6343004C53CC /* TVCFilter.swift in Sources */,
549ECD9D24A7AD550097571C /* CustomAlert.swift in Sources */,
54CE8BC424B1ED2100CC1756 /* PushNotification.swift in Sources */,
54E540FA2482414800F7C34A /* SyncUpdate.swift in Sources */,
5404AEED24A95F3F003B2F54 /* SlideInAnimation.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -886,36 +1049,31 @@
54CA027A2426B2FD003A5E04 /* HTTPURL.swift in Sources */,
54CA025D2426B2FD003A5E04 /* HTTPHeader.swift in Sources */,
54CA02832426B2FD003A5E04 /* DNSSessionMatchResult.swift in Sources */,
54CA02862426B2FD003A5E04 /* RuleManager.swift in Sources */,
54CA02B82426B2FD003A5E04 /* HTTPProxySocket.swift in Sources */,
54CA02C32426DCCD003A5E04 /* GCDAsyncSocket.m in Sources */,
54CA02752426B2FD003A5E04 /* IPRange.swift in Sources */,
54CA02722426B2FD003A5E04 /* IPInterval.swift in Sources */,
54CA029A2426B2FD003A5E04 /* Observer.swift in Sources */,
54CA025C2426B2FD003A5E04 /* ConnectSession.swift in Sources */,
5404AEEB24A90717003B2F54 /* PrefsShared.swift in Sources */,
54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */,
54CA02BA2426B2FD003A5E04 /* ProxySocket.swift in Sources */,
54CA025E2426B2FD003A5E04 /* ResponseGeneratorFactory.swift in Sources */,
54CA02892426B2FD003A5E04 /* Tunnel.swift in Sources */,
54CA029F2426B2FD003A5E04 /* ProxySocketEvent.swift in Sources */,
54CA027D2426B2FD003A5E04 /* GlobalIntializer.swift in Sources */,
54CA026F2426B2FD003A5E04 /* Port.swift in Sources */,
54CA028A2426B2FD003A5E04 /* ResponseGenerator.swift in Sources */,
54CA027C2426B2FD003A5E04 /* StreamScanner.swift in Sources */,
54CA02AF2426B2FD003A5E04 /* SOCKS5AdapterFactory.swift in Sources */,
54CA029E2426B2FD003A5E04 /* EventType.swift in Sources */,
54CA02912426B2FD003A5E04 /* DNSMessage.swift in Sources */,
54CA02712426B2FD003A5E04 /* UInt128.swift in Sources */,
54CA02882426B2FD003A5E04 /* QueueFactory.swift in Sources */,
54CA02A12426B2FD003A5E04 /* RuleMatchEvent.swift in Sources */,
54CA02BE2426D4F3003A5E04 /* DDLog.swift in Sources */,
54CA02962426B2FD003A5E04 /* PacketProtocolParser.swift in Sources */,
54CA02932426B2FD003A5E04 /* DNSServer.swift in Sources */,
54CA02B22426B2FD003A5E04 /* AdapterFactoryManager.swift in Sources */,
54CA02AE2426B2FD003A5E04 /* AdapterFactory.swift in Sources */,
54CA02A82426B2FD003A5E04 /* SOCKS5Adapter.swift in Sources */,
54CA02792426B2FD003A5E04 /* Checksum.swift in Sources */,
54CA02AD2426B2FD003A5E04 /* RejectAdapterFactory.swift in Sources */,
541075D224CDBA0000D6F1BF /* ThrottledBatchQueue.swift in Sources */,
54CA02672426B2FD003A5E04 /* RawTCPSocketProtocol.swift in Sources */,
54CA02602426B2FD003A5E04 /* GCDProxyServer.swift in Sources */,
54CA026B2426B2FD003A5E04 /* GCDTCPSocket.swift in Sources */,
@@ -923,50 +1081,38 @@
54CA01D52426B252003A5E04 /* SafeDict.swift in Sources */,
54CA027B2426B2FD003A5E04 /* HTTPAuthentication.swift in Sources */,
54CA02762426B2FD003A5E04 /* IPAddress.swift in Sources */,
54CA02B02426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift in Sources */,
54CA02A62426B2FD003A5E04 /* AdapterSocket.swift in Sources */,
54CA02742426B2FD003A5E04 /* IPMask.swift in Sources */,
54CA02BB2426B2FD003A5E04 /* SOCKS5ProxySocket.swift in Sources */,
541075CF24C9D43A00D6F1BF /* UNNotification.swift in Sources */,
54D8B97F2471B89100EB2414 /* DBCommon.swift in Sources */,
54CA02A42426B2FD003A5E04 /* SecureHTTPAdapter.swift in Sources */,
54CA02942426B2FD003A5E04 /* DNSResolver.swift in Sources */,
54CA025F2426B2FD003A5E04 /* ProxyServer.swift in Sources */,
54CA02842426B2FD003A5E04 /* Rule.swift in Sources */,
54CA02B92426B2FD003A5E04 /* DirectProxySocket.swift in Sources */,
54CE8BC524B1ED2100CC1756 /* PushNotification.swift in Sources */,
54751E522423955100168273 /* URL.swift in Sources */,
54CA02A92426B2FD003A5E04 /* RejectAdapter.swift in Sources */,
54CA02732426B2FD003A5E04 /* IPPool.swift in Sources */,
54CA027E2426B2FD003A5E04 /* DomainListRule.swift in Sources */,
541075D624CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */,
54CA02782426B2FD003A5E04 /* BinaryDataScanner.swift in Sources */,
54CA02B12426B2FD003A5E04 /* ServerAdapterFactory.swift in Sources */,
54CA02952426B2FD003A5E04 /* DNSEnums.swift in Sources */,
54CA02802426B2FD003A5E04 /* DNSSessionMatchType.swift in Sources */,
54CA02A22426B2FD003A5E04 /* ObserverFactory.swift in Sources */,
54CA02612426B2FD003A5E04 /* GCDSOCKS5ProxyServer.swift in Sources */,
54CA029D2426B2FD003A5E04 /* ProxyServerEvent.swift in Sources */,
54CA02BC2426B2FD003A5E04 /* SocketProtocol.swift in Sources */,
54CA029C2426B2FD003A5E04 /* AdapterSocketEvent.swift in Sources */,
54CA02A72426B2FD003A5E04 /* DirectAdapter.swift in Sources */,
54CA02A32426B2FD003A5E04 /* HTTPAdapter.swift in Sources */,
54CA02622426B2FD003A5E04 /* GCDHTTPProxyServer.swift in Sources */,
54CA02822426B2FD003A5E04 /* AllRule.swift in Sources */,
543CDB2023EEE61900B7F323 /* PacketTunnelProvider.swift in Sources */,
54CA02662426B2FD003A5E04 /* NWUDPSocket.swift in Sources */,
54CA02682426B2FD003A5E04 /* NWTCPSocket.swift in Sources */,
54CA02852426B2FD003A5E04 /* DirectRule.swift in Sources */,
54CA01D32426B23D003A5E04 /* Resolver.swift in Sources */,
54CA028B2426B2FD003A5E04 /* Utils.swift in Sources */,
54CA02972426B2FD003A5E04 /* IPPacket.swift in Sources */,
54CA026A2426B2FD003A5E04 /* RawSocketFactory.swift in Sources */,
54CA02A02426B2FD003A5E04 /* TunnelEvent.swift in Sources */,
546063E523FEFAFE008F505A /* DBCore.swift in Sources */,
54CA02872426B2FD003A5E04 /* IPRangeListRule.swift in Sources */,
54CA02922426B2FD003A5E04 /* DNSSession.swift in Sources */,
54CA026D2426B2FD003A5E04 /* Opt.swift in Sources */,
54CA02B32426B2FD003A5E04 /* HTTPAdapterFactory.swift in Sources */,
54CA02702426B2FD003A5E04 /* HTTPStreamScanner.swift in Sources */,
54CA02812426B2FD003A5E04 /* DNSFailRule.swift in Sources */,
54CA02AC2426B2FD003A5E04 /* AuthenticationServerAdapterFactory.swift in Sources */,
541075DA24CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -997,6 +1143,38 @@
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
543078C124B60F3B00278F2D /* Settings.storyboard */ = {
isa = PBXVariantGroup;
children = (
543078C224B60F3B00278F2D /* Base */,
);
name = Settings.storyboard;
sourceTree = "<group>";
};
549A96D8250419B200C565FA /* CoOccurrence.storyboard */ = {
isa = PBXVariantGroup;
children = (
549A96D9250419B200C565FA /* Base */,
);
name = CoOccurrence.storyboard;
sourceTree = "<group>";
};
54A0CC0724E30C56009B5EC1 /* Recordings.storyboard */ = {
isa = PBXVariantGroup;
children = (
54A0CC0824E30C56009B5EC1 /* Base */,
);
name = Recordings.storyboard;
sourceTree = "<group>";
};
54A0CC0A24E30D6F009B5EC1 /* Requests.storyboard */ = {
isa = PBXVariantGroup;
children = (
54A0CC0B24E30D6F009B5EC1 /* Base */,
);
name = Requests.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
@@ -1026,6 +1204,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -1090,6 +1269,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -1127,7 +1307,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 34;
INFOPLIST_FILE = main/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -1146,7 +1326,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 34;
INFOPLIST_FILE = main/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -1165,7 +1345,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 34;
INFOPLIST_FILE = GlassVPN/Info.plist;
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";
@@ -1183,7 +1363,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 34;
INFOPLIST_FILE = GlassVPN/Info.plist;
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
LastUpgradeVersion = "1200"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction

View File

@@ -1,50 +1,8 @@
import NetworkExtension
fileprivate var filterDomains: [String]!
fileprivate var filterOptions: [(block: Bool, ignore: Bool)]!
// MARK: Backward DNS Binary Tree Lookup
fileprivate func reloadDomainFilter() {
let tmp = AppDB?.loadFilters()?.map({
(String($0.reversed()), $1)
}).sorted(by: { $0.0 < $1.0 }) ?? []
filterDomains = tmp.map { $0.0 }
filterOptions = tmp.map { ($1.contains(.blocked), $1.contains(.ignored)) }
}
fileprivate func filterIndex(for domain: String) -> Int {
let reverseDomain = String(domain.reversed())
var lo = 0, hi = filterDomains.count - 1
while lo <= hi {
let mid = (lo + hi)/2
if filterDomains[mid] < reverseDomain {
lo = mid + 1
} else if reverseDomain < filterDomains[mid] {
hi = mid - 1
} else {
return mid
}
}
if lo > 0, reverseDomain.hasPrefix(filterDomains[lo - 1] + ".") {
return lo - 1
}
return -1
}
private let queue = DispatchQueue.init(label: "PSIGlassDNSQueue", qos: .userInteractive, target: .main)
private func logAsync(_ domain: String, blocked: Bool) {
queue.async {
do {
try AppDB?.logWrite(domain, blocked: blocked)
} catch {
DDLogWarn("Couldn't write: \(error)")
}
}
}
let connectMessage: Data = "CONNECT".data(using: .ascii)!
let swcdUserAgent: Data = "User-Agent: swcd".data(using: .ascii)!
fileprivate var hook : GlassVPNHook!
// MARK: ObserverFactory
@@ -59,14 +17,17 @@ class LDObserverFactory: ObserverFactory {
override func signal(_ event: ProxySocketEvent) {
switch event {
case .receivedRequest(let session, let socket):
let i = filterIndex(for: session.host)
if i >= 0 {
let (block, ignore) = filterOptions[i]
if !ignore { logAsync(session.host, blocked: block) }
if block { socket.forceDisconnect() }
var kill = !hook.isBackgroundRecording && hook.forceDisconnectUnresolvable && session.ipAddress.isEmpty
if kill || socket.isCancelled { // isCancelled is set by branch below
hook.silentlyPrevented(session.host)
} else {
// TODO: disable filter during recordings
logAsync(session.host, blocked: false)
kill = hook.processDNSRequest(session.host)
}
if kill { socket.forceDisconnect() }
case .readData(let data, on: let socket):
if !hook.isBackgroundRecording, hook.forceDisconnectSWCD,
data.starts(with: connectMessage), data.range(of: swcdUserAgent) != nil {
socket.disconnect() // sets isCancelled above
}
default:
break
@@ -80,25 +41,63 @@ class LDObserverFactory: ObserverFactory {
class PacketTunnelProvider: NEPacketTunnelProvider {
let proxyServerPort: UInt16 = 9090
let proxyServerAddress = "127.0.0.1"
var proxyServer: GCDHTTPProxyServer!
private let proxyServerPort: UInt16 = 9090
private let proxyServerAddress = "127.0.0.1"
private var proxyServer: GCDHTTPProxyServer!
// MARK: Delegate
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
DDLogVerbose("startTunnel with with options: \(String(describing: options))")
PrefsShared.registerDefaults()
do {
try SQLiteDatabase.open().initCommonScheme()
} catch {
completionHandler(error)
completionHandler(error) // if we cant open db, fail immediately
return
}
reloadDomainFilter()
if proxyServer != nil {
proxyServer.stop()
}
// stop previous if any
if proxyServer != nil { proxyServer.stop() }
proxyServer = nil
// Create proxy
willInitProxy()
self.setTunnelNetworkSettings(createProxy()) { error in
guard error == nil else {
DDLogError("setTunnelNetworkSettings error: \(error!)")
completionHandler(error)
return
}
self.proxyServer = GCDHTTPProxyServer(address: IPAddress(fromString: self.proxyServerAddress), port: Port(port: self.proxyServerPort))
do {
try self.proxyServer.start()
self.didInitProxy()
completionHandler(nil)
} catch let proxyError {
DDLogError("Error starting proxy server \(proxyError)")
completionHandler(proxyError)
}
}
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
DDLogVerbose("stopTunnel with reason: \(reason)")
shutdown()
completionHandler()
exit(EXIT_SUCCESS)
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
hook.handleAppMessage(messageData)
}
// MARK: Helper
private func willInitProxy() {
hook = GlassVPNHook()
}
private func createProxy() -> NEPacketTunnelNetworkSettings {
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: proxyServerAddress)
settings.mtu = NSNumber(value: 1500)
@@ -115,42 +114,29 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
settings.proxySettings = proxySettings;
RawSocketFactory.TunnelProvider = self
ObserverFactory.currentFactory = LDObserverFactory()
self.setTunnelNetworkSettings(settings) { error in
guard error == nil else {
DDLogError("setTunnelNetworkSettings error: \(String(describing: error))")
completionHandler(error)
return
}
completionHandler(nil)
self.proxyServer = GCDHTTPProxyServer(address: IPAddress(fromString: self.proxyServerAddress), port: Port(port: self.proxyServerPort))
do {
try self.proxyServer.start()
completionHandler(nil)
}
catch let proxyError {
DDLogError("Error starting proxy server \(proxyError)")
completionHandler(proxyError)
}
return settings
}
private func didInitProxy() {
if PrefsShared.RestartReminder.Enabled {
PushNotification.scheduleRestartReminderBadge(on: false)
PushNotification.cancel(.CantStopMeNowReminder)
}
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
DDLogVerbose("stopTunnel with reason: \(reason)")
private func shutdown() {
// proxy
DNSServer.currentServer = nil
RawSocketFactory.TunnelProvider = nil
ObserverFactory.currentFactory = nil
proxyServer.stop()
proxyServer = nil
filterDomains = nil
filterOptions = nil
completionHandler()
exit(EXIT_SUCCESS)
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
reloadDomainFilter()
// custom
hook.cleanUp()
hook = nil
if PrefsShared.RestartReminder.Enabled {
PushNotification.scheduleRestartReminderBadge(on: true)
PushNotification.scheduleRestartReminderBanner()
}
}
}

BIN
GlassVPN/SwiftSocket/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1,16 +0,0 @@
import Foundation
public enum RuleMatchEvent: EventType {
public var description: String {
switch self {
case let .ruleMatched(session, rule: rule):
return "Rule \(rule) matched session \(session)."
case let .ruleDidNotMatch(session, rule: rule):
return "Rule \(rule) did not match session \(session)."
case let .dnsRuleMatched(session, rule: rule, type: type, result: result):
return "Rule \(rule) matched DNS session \(session) of type \(type), the result is \(result)."
}
}
case ruleMatched(ConnectSession, rule: Rule), ruleDidNotMatch(ConnectSession, rule: Rule), dnsRuleMatched(DNSSession, rule: Rule, type: DNSSessionMatchType, result: DNSSessionMatchResult)
}

View File

@@ -20,8 +20,4 @@ open class ObserverFactory {
open func getObserverForProxyServer(_ server: ProxyServer) -> Observer<ProxyServerEvent>? {
return nil
}
open func getObserverForRuleManager(_ manager: RuleManager) -> Observer<RuleMatchEvent>? {
return nil
}
}

View File

@@ -74,8 +74,6 @@ open class DNSServer: DNSResolverDelegate, IPStackProtocol {
return
}
RuleManager.currentManager.matchDNS(session, type: .domain)
switch session.matchResult! {
case .fake:
guard setUpFakeIP(session) else {
@@ -248,10 +246,6 @@ open class DNSServer: DNSResolverDelegate, IPStackProtocol {
session.realIP = message.resolvedIPv4Address
if session.matchResult != .fake && session.matchResult != .real {
RuleManager.currentManager.matchDNS(session, type: .ip)
}
switch session.matchResult! {
case .fake:
if !self.setUpFakeIP(session) {

View File

@@ -7,7 +7,6 @@ open class DNSSession {
open var fakeIP: IPAddress?
open var realResponseMessage: DNSMessage?
var realResponseIPPacket: IPPacket?
open var matchedRule: Rule?
open var matchResult: DNSSessionMatchResult?
var indexToMatch = 0
var expireAt: Date?

View File

@@ -21,9 +21,6 @@ public final class ConnectSession {
/// The requested port.
public let port: Int
/// The rule to use to connect to remote.
public var matchedRule: Rule?
/// Whether If the `requestedHost` is an IP address.
public let fakeIPEnabled: Bool
@@ -126,11 +123,6 @@ public final class ConnectSession {
host = session.requestMessage.queries[0].name
ipAddress = session.realIP?.presentation ?? ""
matchedRule = session.matchedRule
// if session.countryCode != nil {
// country = session.countryCode!
// }
return true
}

View File

@@ -17,7 +17,6 @@ open class HTTPHeader {
// Chunk is not supported yet.
open var contentLength: Int = 0
open var headers: [(String, String)] = []
open var rawHeader: Data?
public init(headerString: String) throws {
let lines = headerString.components(separatedBy: "\r\n")
@@ -127,7 +126,6 @@ open class HTTPHeader {
}
try self.init(headerString: headerString)
rawHeader = headerData
}
open subscript(index: String) -> String? {

View File

@@ -1,24 +0,0 @@
import Foundation
/// The SOCKS5 proxy server.
public final class GCDSOCKS5ProxyServer: GCDProxyServer {
/**
Create an instance of SOCKS5 proxy server.
- parameter address: The address of proxy server.
- parameter port: The port of proxy server.
*/
override public init(address: IPAddress?, port: Port) {
super.init(address: address, port: port)
}
/**
Handle the new accepted socket as a SOCKS5 proxy connection.
- parameter socket: The accepted socket.
*/
override public func handleNewGCDSocket(_ socket: GCDTCPSocket) {
let proxySocket = SOCKS5ProxySocket(socket: socket)
didAcceptNewSocket(proxySocket)
}
}

View File

@@ -141,7 +141,14 @@ public class NWTCPSocket: NSObject, RawTCPSocketProtocol {
connection!.readMinimumLength(1, maximumLength: Opt.MAXNWTCPSocketReadDataSize) { data, error in
guard error == nil else {
DDLogError("NWTCPSocket got an error when reading data: \(String(describing: error))")
let e = error! as NSError
let ignore = (
e.domain == "kNWErrorDomainPOSIX" && e.code == POSIXError.ECANCELED.rawValue // Operation canceled
|| e.domain == NSPOSIXErrorDomain && e.code == POSIXError.ENOTCONN.rawValue // Socket is not connected
)
if !ignore {
DDLogError("NWTCPSocket got an error when reading data: \(String(describing: error))")
}
self.queueCall {
self.disconnect()
}

View File

@@ -1,6 +0,0 @@
import Foundation
open class ResponseGeneratorFactory {
static var HTTPProxyResponseGenerator: ResponseGenerator.Type?
static var SOCKS5ProxyResponseGenerator: ResponseGenerator.Type?
}

View File

@@ -1,48 +0,0 @@
import Foundation
/// The rule matches all DNS and connect sessions.
open class AllRule: Rule {
fileprivate let adapterFactory: AdapterFactory
open override var description: String {
return "<AllRule>"
}
/**
Create a new `AllRule` instance.
- parameter adapterFactory: The factory which builds a corresponding adapter when needed.
*/
public init(adapterFactory: AdapterFactory) {
self.adapterFactory = adapterFactory
super.init()
}
/**
Match DNS session to this rule.
- parameter session: The DNS session to match.
- parameter type: What kind of information is available.
- returns: The result of match.
*/
override open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
// only return real IP when we connect to remote directly
if let _ = adapterFactory as? DirectAdapterFactory {
return .real
} else {
return .fake
}
}
/**
Match connect session to this rule.
- parameter session: connect session to match.
- returns: The configured adapter.
*/
override open func match(_ session: ConnectSession) -> AdapterFactory? {
return adapterFactory
}
}

View File

@@ -1,60 +0,0 @@
import Foundation
/// The rule matches the request which failed to look up.
open class DNSFailRule: Rule {
fileprivate let adapterFactory: AdapterFactory
open override var description: String {
return "<DNSFailRule>"
}
/**
Create a new `DNSFailRule` instance.
- parameter adapterFactory: The factory which builds a corresponding adapter when needed.
*/
public init(adapterFactory: AdapterFactory) {
self.adapterFactory = adapterFactory
super.init()
}
/**
Match DNS request to this rule.
- parameter session: The DNS session to match.
- parameter type: What kind of information is available.
- returns: The result of match.
*/
override open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
guard type == .ip else {
return .unknown
}
// only return real IP when we connect to remote directly
if session.realIP == nil {
if let _ = adapterFactory as? DirectAdapterFactory {
return .real
} else {
return .fake
}
} else {
return .pass
}
}
/**
Match connect session to this rule.
- parameter session: connect session to match.
- returns: The configured adapter.
*/
override open func match(_ session: ConnectSession) -> AdapterFactory? {
if session.ipAddress == "" {
return adapterFactory
} else {
return nil
}
}
}

View File

@@ -1,16 +0,0 @@
import Foundation
/// The rule matches every request and returns direct adapter.
///
/// This is equivalent to create an `AllRule` with a `DirectAdapterFactory`.
open class DirectRule: AllRule {
open override var description: String {
return "<DirectRule>"
}
/**
Create a new `DirectRule` instance.
*/
public init() {
super.init(adapterFactory: DirectAdapterFactory())
}
}

View File

@@ -1,84 +0,0 @@
import Foundation
/// The rule matches the host domain to a list of predefined criteria.
open class DomainListRule: Rule {
public enum MatchCriterion {
case regex(NSRegularExpression), prefix(String), suffix(String), keyword(String), complete(String)
func match(_ domain: String) -> Bool {
switch self {
case .regex(let regex):
return regex.firstMatch(in: domain, options: [], range: NSRange(location: 0, length: domain.utf8.count)) != nil
case .prefix(let prefix):
return domain.hasPrefix(prefix)
case .suffix(let suffix):
return domain.hasSuffix(suffix)
case .keyword(let keyword):
return domain.contains(keyword)
case .complete(let match):
return domain == match
}
}
}
fileprivate let adapterFactory: AdapterFactory
open override var description: String {
return "<DomainListRule>"
}
/// The list of criteria to match to.
open var matchCriteria: [MatchCriterion] = []
/**
Create a new `DomainListRule` instance.
- parameter adapterFactory: The factory which builds a corresponding adapter when needed.
- parameter criteria: The list of criteria to match.
*/
public init(adapterFactory: AdapterFactory, criteria: [MatchCriterion]) {
self.adapterFactory = adapterFactory
self.matchCriteria = criteria
}
/**
Match DNS request to this rule.
- parameter session: The DNS session to match.
- parameter type: What kind of information is available.
- returns: The result of match.
*/
override open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
if matchDomain(session.requestMessage.queries.first!.name) {
if let _ = adapterFactory as? DirectAdapterFactory {
return .real
}
return .fake
}
return .pass
}
/**
Match connect session to this rule.
- parameter session: connect session to match.
- returns: The configured adapter if matched, return `nil` if not matched.
*/
override open func match(_ session: ConnectSession) -> AdapterFactory? {
if matchDomain(session.host) {
return adapterFactory
}
return nil
}
fileprivate func matchDomain(_ domain: String) -> Bool {
for criterion in matchCriteria {
if criterion.match(domain) {
return true
}
}
return false
}
}

View File

@@ -1,75 +0,0 @@
import Foundation
/// The rule matches the ip of the target hsot to a list of IP ranges.
open class IPRangeListRule: Rule {
fileprivate let adapterFactory: AdapterFactory
open override var description: String {
return "<IPRangeList>"
}
/// The list of regular expressions to match to.
open var ranges: [IPRange] = []
/**
Create a new `IPRangeListRule` instance.
- parameter adapterFactory: The factory which builds a corresponding adapter when needed.
- parameter ranges: The list of IP ranges to match. The IP ranges are expressed in CIDR form ("127.0.0.1/8") or range form ("127.0.0.1+16777216").
- throws: The error when parsing the IP range.
*/
public init(adapterFactory: AdapterFactory, ranges: [String]) throws {
self.adapterFactory = adapterFactory
self.ranges = try ranges.map {
let range = try IPRange(withString: $0)
return range
}
}
/**
Match DNS request to this rule.
- parameter session: The DNS session to match.
- parameter type: What kind of information is available.
- returns: The result of match.
*/
override open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
guard type == .ip else {
return .unknown
}
// Probably we should match all answers?
guard let ip = session.realIP else {
return .pass
}
for range in ranges {
if range.contains(ip: ip) {
return .fake
}
}
return .pass
}
/**
Match connect session to this rule.
- parameter session: connect session to match.
- returns: The configured adapter if matched, return `nil` if not matched.
*/
override open func match(_ session: ConnectSession) -> AdapterFactory? {
guard let ip = IPAddress(fromString: session.ipAddress) else {
return nil
}
for range in ranges {
if range.contains(ip: ip) {
return adapterFactory
}
}
return nil
}
}

View File

@@ -1,37 +0,0 @@
import Foundation
/// The rule defines what to do for DNS requests and connect sessions.
open class Rule: CustomStringConvertible {
open var description: String {
return "<Rule>"
}
/**
Create a new rule.
*/
public init() {
}
/**
Match DNS request to this rule.
- parameter session: The DNS session to match.
- parameter type: What kind of information is available.
- returns: The result of match.
*/
open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
return .real
}
/**
Match connect session to this rule.
- parameter session: connect session to match.
- returns: The configured adapter if matched, return `nil` if not matched.
*/
open func match(_ session: ConnectSession) -> AdapterFactory? {
return nil
}
}

View File

@@ -1,80 +0,0 @@
import Foundation
/// The class managing rules.
open class RuleManager {
/// The current used `RuleManager`, there is only one manager should be used at a time.
///
/// - note: This should be set before any DNS or connect sessions.
public static var currentManager: RuleManager = RuleManager(fromRules: [], appendDirect: true)
/// The rule list.
var rules: [Rule] = []
open var observer: Observer<RuleMatchEvent>?
/**
Create a new `RuleManager` from the given rules.
- parameter rules: The rules.
- parameter appendDirect: Whether to append a `DirectRule` at the end of the list so any request does not match with any rule go directly.
*/
public init(fromRules rules: [Rule], appendDirect: Bool = false) {
self.rules = []
if appendDirect || self.rules.count == 0 {
self.rules.append(DirectRule())
}
observer = ObserverFactory.currentFactory?.getObserverForRuleManager(self)
}
/**
Match DNS request to all rules.
- parameter session: The DNS session to match.
- parameter type: What kind of information is available.
*/
func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) {
for (i, rule) in rules[session.indexToMatch..<rules.count].enumerated() {
let result = rule.matchDNS(session, type: type)
observer?.signal(.dnsRuleMatched(session, rule: rule, type: type, result: result))
switch result {
case .fake, .real, .unknown:
session.matchedRule = rule
session.matchResult = result
session.indexToMatch = i + session.indexToMatch // add the offset
return
case .pass:
break
}
}
}
/**
Match connect session to all rules.
- parameter session: connect session to match.
- returns: The matched configured adapter.
*/
func match(_ session: ConnectSession) -> AdapterFactory! {
if session.matchedRule != nil {
observer?.signal(.ruleMatched(session, rule: session.matchedRule!))
return session.matchedRule!.match(session)
}
for rule in rules {
if let adapterFactory = rule.match(session) {
observer?.signal(.ruleMatched(session, rule: rule))
session.matchedRule = rule
return adapterFactory
} else {
observer?.signal(.ruleDidNotMatch(session, rule: rule))
}
}
return nil // this should never happens
}
}

View File

@@ -1,27 +0,0 @@
import Foundation
/// This is a very simple wrapper of a dict of type `[String: AdapterFactory]`.
///
/// Use it as a normal dict.
public class AdapterFactoryManager {
private var factoryDict: [String: AdapterFactory]
public subscript(index: String) -> AdapterFactory? {
get {
if index == "direct" {
return DirectAdapterFactory()
}
return factoryDict[index]
}
set { factoryDict[index] = newValue }
}
/**
Initialize a new factory manager.
- parameter factoryDict: The factory dict.
*/
public init(factoryDict: [String: AdapterFactory]) {
self.factoryDict = factoryDict
}
}

View File

@@ -1,11 +0,0 @@
import Foundation
/// Factory building server adapter which requires authentication.
open class HTTPAuthenticationAdapterFactory: ServerAdapterFactory {
let auth: HTTPAuthentication?
required public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
self.auth = auth
super.init(serverHost: serverHost, serverPort: serverPort)
}
}

View File

@@ -1,21 +0,0 @@
import Foundation
/// Factory building HTTP adapter.
open class HTTPAdapterFactory: HTTPAuthenticationAdapterFactory {
required public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
super.init(serverHost: serverHost, serverPort: serverPort, auth: auth)
}
/**
Get a HTTP adapter.
- parameter session: The connect session.
- returns: The built adapter.
*/
override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
let adapter = HTTPAdapter(serverHost: serverHost, serverPort: serverPort, auth: auth)
adapter.socket = RawSocketFactory.getRawSocket()
return adapter
}
}

View File

@@ -1,13 +0,0 @@
import Foundation
open class RejectAdapterFactory: AdapterFactory {
public let delay: Int
public init(delay: Int = Opt.RejectAdapterDefaultDelay) {
self.delay = delay
}
override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
return RejectAdapter(delay: delay)
}
}

View File

@@ -1,21 +0,0 @@
import Foundation
/// Factory building SOCKS5 adapter.
open class SOCKS5AdapterFactory: ServerAdapterFactory {
override public init(serverHost: String, serverPort: Int) {
super.init(serverHost: serverHost, serverPort: serverPort)
}
/**
Get a SOCKS5 adapter.
- parameter session: The connect session.
- returns: The built adapter.
*/
override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
let adapter = SOCKS5Adapter(serverHost: serverHost, serverPort: serverPort)
adapter.socket = RawSocketFactory.getRawSocket()
return adapter
}
}

View File

@@ -1,21 +0,0 @@
import Foundation
/// Factory building secured HTTP (HTTP with SSL) adapter.
open class SecureHTTPAdapterFactory: HTTPAdapterFactory {
required public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
super.init(serverHost: serverHost, serverPort: serverPort, auth: auth)
}
/**
Get a secured HTTP adapter.
- parameter session: The connect session.
- returns: The built adapter.
*/
override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
let adapter = SecureHTTPAdapter(serverHost: serverHost, serverPort: serverPort, auth: auth)
adapter.socket = RawSocketFactory.getRawSocket()
return adapter
}
}

View File

@@ -1,12 +0,0 @@
import Foundation
/// Factory building adapter with proxy server host and port.
open class ServerAdapterFactory: AdapterFactory {
let serverHost: String
let serverPort: Int
public init(serverHost: String, serverPort: Int) {
self.serverHost = serverHost
self.serverPort = serverPort
}
}

View File

@@ -1,110 +0,0 @@
import Foundation
public enum HTTPAdapterError: Error, CustomStringConvertible {
case invalidURL, serailizationFailure
public var description: String {
switch self {
case .invalidURL:
return "Invalid url when connecting through proxy"
case .serailizationFailure:
return "Failed to serialize HTTP CONNECT header"
}
}
}
/// This adapter connects to remote host through a HTTP proxy.
public class HTTPAdapter: AdapterSocket {
enum HTTPAdapterStatus {
case invalid,
connecting,
readingResponse,
forwarding,
stopped
}
/// The host domain of the HTTP proxy.
let serverHost: String
/// The port of the HTTP proxy.
let serverPort: Int
/// The authentication information for the HTTP proxy.
let auth: HTTPAuthentication?
/// Whether the connection to the proxy should be secured or not.
var secured: Bool
var internalStatus: HTTPAdapterStatus = .invalid
public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
self.serverHost = serverHost
self.serverPort = serverPort
self.auth = auth
secured = false
super.init()
}
override public func openSocketWith(session: ConnectSession) {
super.openSocketWith(session: session)
guard !isCancelled else {
return
}
do {
internalStatus = .connecting
try socket.connectTo(host: serverHost, port: serverPort, enableTLS: secured, tlsSettings: nil)
} catch {}
}
override public func didConnectWith(socket: RawTCPSocketProtocol) {
super.didConnectWith(socket: socket)
guard let url = URL(string: "\(session.host):\(session.port)") else {
observer?.signal(.errorOccured(HTTPAdapterError.invalidURL, on: self))
disconnect()
return
}
let message = CFHTTPMessageCreateRequest(kCFAllocatorDefault, "CONNECT" as CFString, url as CFURL, kCFHTTPVersion1_1).takeRetainedValue()
if let authData = auth {
CFHTTPMessageSetHeaderFieldValue(message, "Proxy-Authorization" as CFString, authData.authString() as CFString?)
}
CFHTTPMessageSetHeaderFieldValue(message, "Host" as CFString, "\(session.host):\(session.port)" as CFString?)
CFHTTPMessageSetHeaderFieldValue(message, "Content-Length" as CFString, "0" as CFString?)
guard let requestData = CFHTTPMessageCopySerializedMessage(message)?.takeRetainedValue() else {
observer?.signal(.errorOccured(HTTPAdapterError.serailizationFailure, on: self))
disconnect()
return
}
internalStatus = .readingResponse
write(data: requestData as Data)
socket.readDataTo(data: Utils.HTTPData.DoubleCRLF)
}
override public func didRead(data: Data, from socket: RawTCPSocketProtocol) {
super.didRead(data: data, from: socket)
switch internalStatus {
case .readingResponse:
internalStatus = .forwarding
observer?.signal(.readyForForward(self))
delegate?.didBecomeReadyToForwardWith(socket: self)
case .forwarding:
observer?.signal(.readData(data, on: self))
delegate?.didRead(data: data, from: self)
default:
return
}
}
override public func didWrite(data: Data?, by socket: RawTCPSocketProtocol) {
super.didWrite(data: data, by: socket)
if internalStatus == .forwarding {
observer?.signal(.wroteData(data, on: self))
delegate?.didWrite(data: data, by: self)
}
}
}

View File

@@ -1,49 +0,0 @@
import Foundation
public class RejectAdapter: AdapterSocket {
public let delay: Int
public init(delay: Int) {
self.delay = delay
}
override public func openSocketWith(session: ConnectSession) {
super.openSocketWith(session: session)
QueueFactory.getQueue().asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(delay)) {
[weak self] in
self?.disconnect()
}
}
/**
Disconnect the socket elegantly.
*/
public override func disconnect(becauseOf error: Error? = nil) {
guard !isCancelled else {
return
}
_cancelled = true
session.disconnected(becauseOf: error, by: .adapter)
observer?.signal(.disconnectCalled(self))
_status = .closed
delegate?.didDisconnectWith(socket: self)
}
/**
Disconnect the socket immediately.
*/
public override func forceDisconnect(becauseOf error: Error? = nil) {
guard !isCancelled else {
return
}
_cancelled = true
session.disconnected(becauseOf: error, by: .adapter)
observer?.signal(.forceDisconnectCalled(self))
_status = .closed
delegate?.didDisconnectWith(socket: self)
}
}

View File

@@ -1,112 +0,0 @@
import Foundation
public class SOCKS5Adapter: AdapterSocket {
enum SOCKS5AdapterStatus {
case invalid,
connecting,
readingMethodResponse,
readingResponseFirstPart,
readingResponseSecondPart,
forwarding
}
public let serverHost: String
public let serverPort: Int
var internalStatus: SOCKS5AdapterStatus = .invalid
let helloData = Data([0x05, 0x01, 0x00])
public enum ReadTag: Int {
case methodResponse = -20000, connectResponseFirstPart, connectResponseSecondPart
}
public enum WriteTag: Int {
case open = -21000, connectIPv4, connectIPv6, connectDomainLength, connectPort
}
public init(serverHost: String, serverPort: Int) {
self.serverHost = serverHost
self.serverPort = serverPort
super.init()
}
public override func openSocketWith(session: ConnectSession) {
super.openSocketWith(session: session)
guard !isCancelled else {
return
}
do {
internalStatus = .connecting
try socket.connectTo(host: serverHost, port: serverPort, enableTLS: false, tlsSettings: nil)
} catch {}
}
public override func didConnectWith(socket: RawTCPSocketProtocol) {
super.didConnectWith(socket: socket)
write(data: helloData)
internalStatus = .readingMethodResponse
socket.readDataTo(length: 2)
}
public override func didRead(data: Data, from socket: RawTCPSocketProtocol) {
super.didRead(data: data, from: socket)
switch internalStatus {
case .readingMethodResponse:
var response: [UInt8]
if session.isIPv4() {
response = [0x05, 0x01, 0x00, 0x01]
let address = IPAddress(fromString: session.host)!
response += [UInt8](address.dataInNetworkOrder)
} else if session.isIPv6() {
response = [0x05, 0x01, 0x00, 0x04]
let address = IPAddress(fromString: session.host)!
response += [UInt8](address.dataInNetworkOrder)
} else {
response = [0x05, 0x01, 0x00, 0x03]
response.append(UInt8(session.host.utf8.count))
response += [UInt8](session.host.utf8)
}
let portBytes: [UInt8] = Utils.toByteArray(UInt16(session.port)).reversed()
response.append(contentsOf: portBytes)
write(data: Data(response))
internalStatus = .readingResponseFirstPart
socket.readDataTo(length: 5)
case .readingResponseFirstPart:
var readLength = 0
switch data[3] {
case 1:
readLength = 3 + 2
case 3:
readLength = Int(data[4]) + 2
case 4:
readLength = 15 + 2
default:
break
}
internalStatus = .readingResponseSecondPart
socket.readDataTo(length: readLength)
case .readingResponseSecondPart:
internalStatus = .forwarding
observer?.signal(.readyForForward(self))
delegate?.didBecomeReadyToForwardWith(socket: self)
case .forwarding:
delegate?.didRead(data: data, from: self)
default:
return
}
}
override open func didWrite(data: Data?, by socket: RawTCPSocketProtocol) {
super.didWrite(data: data, by: socket)
if internalStatus == .forwarding {
delegate?.didWrite(data: data, by: self)
}
}
}

View File

@@ -1,9 +0,0 @@
import Foundation
/// This adapter connects to remote host through a HTTP proxy with SSL.
public class SecureHTTPAdapter: HTTPAdapter {
override public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
super.init(serverHost: serverHost, serverPort: serverPort, auth: auth)
secured = true
}
}

View File

@@ -1,113 +0,0 @@
import Foundation
/// This class just forwards data directly.
/// - note: It is designed to work with tun2socks only.
public class DirectProxySocket: ProxySocket {
enum DirectProxyReadStatus: CustomStringConvertible {
case invalid,
forwarding,
stopped
var description: String {
switch self {
case .invalid:
return "invalid"
case .forwarding:
return "forwarding"
case .stopped:
return "stopped"
}
}
}
enum DirectProxyWriteStatus {
case invalid,
forwarding,
stopped
var description: String {
switch self {
case .invalid:
return "invalid"
case .forwarding:
return "forwarding"
case .stopped:
return "stopped"
}
}
}
private var readStatus: DirectProxyReadStatus = .invalid
private var writeStatus: DirectProxyWriteStatus = .invalid
public var readStatusDescription: String {
return readStatus.description
}
public var writeStatusDescription: String {
return writeStatus.description
}
/**
Begin reading and processing data from the socket.
- note: Since there is nothing to read and process before forwarding data, this just calls `delegate?.didReceiveRequest`.
*/
override public func openSocket() {
super.openSocket()
guard !isCancelled else {
return
}
if let address = socket.destinationIPAddress, let port = socket.destinationPort {
session = ConnectSession(host: address.presentation, port: Int(port.value))
observer?.signal(.receivedRequest(session!, on: self))
delegate?.didReceive(session: session!, from: self)
} else {
forceDisconnect()
}
}
/**
Response to the `AdapterSocket` on the other side of the `Tunnel` which has succefully connected to the remote server.
- parameter adapter: The `AdapterSocket`.
*/
override public func respondTo(adapter: AdapterSocket) {
super.respondTo(adapter: adapter)
guard !isCancelled else {
return
}
readStatus = .forwarding
writeStatus = .forwarding
observer?.signal(.readyForForward(self))
delegate?.didBecomeReadyToForwardWith(socket: self)
}
/**
The socket did read some data.
- parameter data: The data read from the socket.
- parameter from: The socket where the data is read from.
*/
override open func didRead(data: Data, from: RawTCPSocketProtocol) {
super.didRead(data: data, from: from)
delegate?.didRead(data: data, from: self)
}
/**
The socket did send some data.
- parameter data: The data which have been sent to remote (acknowledged). Note this may not be available since the data may be released to save memory.
- parameter by: The socket where the data is sent out.
*/
override open func didWrite(data: Data?, by: RawTCPSocketProtocol) {
super.didWrite(data: data, by: by)
delegate?.didWrite(data: data, by: self)
}
}

View File

@@ -1,244 +0,0 @@
import Foundation
public class SOCKS5ProxySocket: ProxySocket {
enum SOCKS5ProxyReadStatus: CustomStringConvertible {
case invalid,
readingVersionIdentifierAndNumberOfMethods,
readingMethods,
readingConnectHeader,
readingIPv4Address,
readingDomainLength,
readingDomain,
readingIPv6Address,
readingPort,
forwarding,
stopped
var description: String {
switch self {
case .invalid:
return "invalid"
case .readingVersionIdentifierAndNumberOfMethods:
return "reading version and methods"
case .readingMethods:
return "reading methods"
case .readingConnectHeader:
return "reading connect header"
case .readingIPv4Address:
return "IPv4 address"
case .readingDomainLength:
return "domain length"
case .readingDomain:
return "domain"
case .readingIPv6Address:
return "IPv6 address"
case .readingPort:
return "reading port"
case .forwarding:
return "forwarding"
case .stopped:
return "stopped"
}
}
}
enum SOCKS5ProxyWriteStatus: CustomStringConvertible {
case invalid,
sendingResponse,
forwarding,
stopped
var description: String {
switch self {
case .invalid:
return "invalid"
case .sendingResponse:
return "sending response"
case .forwarding:
return "forwarding"
case .stopped:
return "stopped"
}
}
}
/// The remote host to connect to.
public var destinationHost: String!
/// The remote port to connect to.
public var destinationPort: Int!
private var readStatus: SOCKS5ProxyReadStatus = .invalid
private var writeStatus: SOCKS5ProxyWriteStatus = .invalid
public var readStatusDescription: String {
return readStatus.description
}
public var writeStatusDescription: String {
return writeStatus.description
}
/**
Begin reading and processing data from the socket.
*/
override public func openSocket() {
super.openSocket()
guard !isCancelled else {
return
}
readStatus = .readingVersionIdentifierAndNumberOfMethods
socket.readDataTo(length: 2)
}
// swiftlint:disable function_body_length
// swiftlint:disable cyclomatic_complexity
/**
The socket did read some data.
- parameter data: The data read from the socket.
- parameter from: The socket where the data is read from.
*/
override public func didRead(data: Data, from: RawTCPSocketProtocol) {
super.didRead(data: data, from: from)
switch readStatus {
case .forwarding:
delegate?.didRead(data: data, from: self)
case .readingVersionIdentifierAndNumberOfMethods:
data.withUnsafeBytes { pointer in
let p = pointer.bindMemory(to: Int8.self)
guard p.baseAddress!.pointee == 5 else {
// TODO: notify observer
self.disconnect()
return
}
guard p.baseAddress!.successor().pointee > 0 else {
// TODO: notify observer
self.disconnect()
return
}
self.readStatus = .readingMethods
self.socket.readDataTo(length: Int(p.baseAddress!.successor().pointee))
}
case .readingMethods:
// TODO: check for 0x00 in read data
let response = Data([0x05, 0x00])
// we would not be able to read anything before the data is written out, so no need to handle the dataWrote event.
write(data: response)
readStatus = .readingConnectHeader
socket.readDataTo(length: 4)
case .readingConnectHeader:
data.withUnsafeBytes { pointer in
let p = pointer.bindMemory(to: Int8.self)
guard p.baseAddress!.pointee == 5 && p.baseAddress!.successor().pointee == 1 else {
// TODO: notify observer
self.disconnect()
return
}
switch p.baseAddress!.advanced(by: 3).pointee {
case 1:
readStatus = .readingIPv4Address
socket.readDataTo(length: 4)
case 3:
readStatus = .readingDomainLength
socket.readDataTo(length: 1)
case 4:
readStatus = .readingIPv6Address
socket.readDataTo(length: 16)
default:
break
}
}
case .readingIPv4Address:
var address = Data(count: Int(INET_ADDRSTRLEN))
_ = data.withUnsafeBytes { data_ptr in
address.withUnsafeMutableBytes { addr_ptr in
inet_ntop(AF_INET, data_ptr.baseAddress!, addr_ptr.bindMemory(to: Int8.self).baseAddress!, socklen_t(INET_ADDRSTRLEN))
}
}
destinationHost = String(data: address, encoding: .utf8)
readStatus = .readingPort
socket.readDataTo(length: 2)
case .readingIPv6Address:
var address = Data(count: Int(INET6_ADDRSTRLEN))
_ = data.withUnsafeBytes { data_ptr in
address.withUnsafeMutableBytes { addr_ptr in
inet_ntop(AF_INET6, data_ptr.baseAddress!, addr_ptr.bindMemory(to: Int8.self).baseAddress!, socklen_t(INET6_ADDRSTRLEN))
}
}
destinationHost = String(data: address, encoding: .utf8)
readStatus = .readingPort
socket.readDataTo(length: 2)
case .readingDomainLength:
readStatus = .readingDomain
socket.readDataTo(length: Int(data.first!))
case .readingDomain:
destinationHost = String(data: data, encoding: .utf8)
readStatus = .readingPort
socket.readDataTo(length: 2)
case .readingPort:
data.withUnsafeBytes {
destinationPort = Int($0.load(as: UInt16.self).bigEndian)
}
readStatus = .forwarding
session = ConnectSession(host: destinationHost, port: destinationPort)
observer?.signal(.receivedRequest(session!, on: self))
delegate?.didReceive(session: session!, from: self)
default:
return
}
}
/**
The socket did send some data.
- parameter data: The data which have been sent to remote (acknowledged). Note this may not be available since the data may be released to save memory.
- parameter from: The socket where the data is sent out.
*/
override public func didWrite(data: Data?, by: RawTCPSocketProtocol) {
super.didWrite(data: data, by: by)
switch writeStatus {
case .forwarding:
delegate?.didWrite(data: data, by: self)
case .sendingResponse:
writeStatus = .forwarding
observer?.signal(.readyForForward(self))
delegate?.didBecomeReadyToForwardWith(socket: self)
default:
return
}
}
/**
Response to the `AdapterSocket` on the other side of the `Tunnel` which has succefully connected to the remote server.
- parameter adapter: The `AdapterSocket`.
*/
override public func respondTo(adapter: AdapterSocket) {
super.respondTo(adapter: adapter)
guard !isCancelled else {
return
}
var responseBytes = [UInt8](repeating: 0, count: 10)
responseBytes[0...3] = [0x05, 0x00, 0x00, 0x01]
let responseData = Data(responseBytes)
writeStatus = .sendingResponse
write(data: responseData)
}
}

View File

@@ -170,9 +170,7 @@ public class Tunnel: NSObject, SocketDelegate {
return
}
let manager = RuleManager.currentManager
let factory = manager.match(session)!
adapterSocket = factory.getAdapterFor(session: session)
adapterSocket = DirectAdapterFactory().getAdapterFor(session: session)
adapterSocket!.delegate = self
adapterSocket!.openSocketWith(session: session)
}

View File

@@ -16,6 +16,8 @@ Your data belongs to you.
Therefore, monitoring and analysis take place on your device only.
The app does not share any data with us or any other third-party unless you choose to.
Join [Testflight beta](https://testflight.apple.com/join/9jjaFeHO)
### How does it work?
@@ -31,19 +33,25 @@ That means, AppCheck does not have to be active in the foreground all the time.
- See history of previous connections
- Block unwanted traffic based on domain names
- Record app specific activity<sup>1</sup>
- Apply logging filters
**… and soon:**
- Apply logging filters (block or ignore) and display filters (specific range or last x minutes)
- Sort results by time, name, or occurrence count
- Context Analysis
- What other domains occur often at the same time?
- What happened immediately before or after the action?
- Export results for custom analysis
- Alert Monitor & reminder
- Occurrence Context Analysis
- Participate in privacy research
- Contribute your results
- See what others have unveiled
- How much traffic does this app produce?
<sup>1</sup> Due to technical limitations, recording is not limited to any single application. Remember to force-quit all other applications before starting a recording.
<sup>1</sup> Due to technical limitations, recordings can not be restricted to a single application. Remember to force-quit all other applications before starting a recording.
## Research Project
*information will be added soon*
*information will be added soon*
For now, go to the results page at [https://appchk.de/](https://appchk.de/).
Btw. we are searching for [help](https://appchk.de/help/) on our ongoing research project.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 158 KiB

View File

@@ -1,13 +1,9 @@
import UIKit
import NetworkExtension
let VPNConfigBundleIdentifier = "de.uni-bamberg.psi.AppCheck.VPN"
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var managerVPN: NETunnelProviderManager?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if UserDefaults.standard.bool(forKey: "kill_db") {
@@ -19,123 +15,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
db.initAppOnlyScheme()
}
#if IOS_SIMULATOR
TestDataSource.load()
#endif
Prefs.registerDefaults()
PrefsShared.registerDefaults()
loadVPN { mgr in
self.managerVPN = mgr
self.postVPNState()
}
NSNotification.Name.NEVPNStatusDidChange.observe(call: #selector(vpnStatusChanged(_:)), on: self)
NotifyDNSFilterChanged.observe(call: #selector(didChangeDomainFilter), on: self)
#if IOS_SIMULATOR
SimulatorVPN.load()
#endif
sync.start()
return true
}
@objc private func vpnStatusChanged(_ notification: Notification) {
postRawVPNState((notification.object as? NETunnelProviderSession)?.status ?? .invalid)
}
@objc private func didChangeDomainFilter() {
// Notify VPN extension about changes
if let session = self.managerVPN?.connection as? NETunnelProviderSession,
session.status == .connected {
try? session.sendProviderMessage("filter-update".data(using: .ascii)!, responseHandler: nil)
}
}
func setProxyEnabled(_ newState: Bool) {
guard let mgr = self.managerVPN else {
self.createNewVPN { manager in
self.managerVPN = manager
self.setProxyEnabled(newState)
}
return
}
let state = mgr.isEnabled && (mgr.connection.status == .connected)
if state != newState {
self.updateVPN({ mgr.isEnabled = true }) {
newState ? try? mgr.connection.startVPNTunnel() : mgr.connection.stopVPNTunnel()
}
}
}
// MARK: VPN
private func createNewVPN(_ success: @escaping (_ manager: NETunnelProviderManager) -> Void) {
let mgr = NETunnelProviderManager()
mgr.localizedDescription = "AppCheck Monitor"
let proto = NETunnelProviderProtocol()
proto.providerBundleIdentifier = VPNConfigBundleIdentifier
proto.serverAddress = "127.0.0.1"
mgr.protocolConfiguration = proto
mgr.isEnabled = true
mgr.saveToPreferences { error in
guard error == nil else {
self.postProcessedVPNState(.off)
//ErrorAlert(error!).presentIn(self.window?.rootViewController)
return
}
success(mgr)
}
}
private func loadVPN(_ finally: @escaping (_ manager: NETunnelProviderManager?) -> Void) {
NETunnelProviderManager.loadAllFromPreferences { managers, error in
guard let mgrs = managers, mgrs.count > 0 else {
finally(nil)
return
}
for mgr in mgrs {
if let proto = (mgr.protocolConfiguration as? NETunnelProviderProtocol) {
if proto.providerBundleIdentifier == VPNConfigBundleIdentifier {
finally(mgr)
return
}
}
}
finally(nil)
}
}
private func updateVPN(_ body: @escaping () -> Void, _ onSuccess: @escaping () -> Void) {
self.managerVPN?.loadFromPreferences { error in
guard error == nil else { return }
body()
self.managerVPN?.saveToPreferences { error in
guard error == nil else { return }
onSuccess()
}
}
}
private func postVPNState() {
guard let mgr = self.managerVPN else {
self.postRawVPNState(.invalid)
return
}
mgr.loadFromPreferences { _ in
self.postRawVPNState(mgr.connection.status)
}
}
// MARK: Notifications
private func postRawVPNState(_ origState: NEVPNStatus) {
let state: VPNState
switch origState {
case .connected: state = .on
case .connecting, .disconnecting, .reasserting: state = .inbetween
case .invalid, .disconnected: fallthrough
@unknown default: state = .off
}
postProcessedVPNState(state)
}
private func postProcessedVPNState(_ state: VPNState) {
currentVPNState = state
NotifyVPNStateChanged.post(state)
func applicationDidBecomeActive(_ application: UIApplication) {
TheGreatDestroyer.deleteLogs(olderThan: PrefsShared.AutoDeleteLogsDays)
// FIXME: Does not reflect changes performed by GlassVPN auto-delete while app is open.
// It will update whenever app restarts or becomes active again (only if deleteLogs has something to delete!)
// This is a known issue and tolerated.
}
}
extension URL {
@discardableResult func open() -> Bool { UIApplication.shared.openURL(self) }
}

Binary file not shown.

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

View File

@@ -19,5 +19,8 @@
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@@ -19,5 +19,8 @@
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@@ -19,5 +19,8 @@
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,237 @@
import UIKit
class CustomAlert<CustomView: UIView>: UIViewController {
private let alertTitle: String?
private let alertDetail: String?
private let customView: CustomView
private var callback: ((CustomView) -> Void)?
/// Default: `[Cancel, Save]`
lazy var buttonsBar: UIStackView = {
let cancel = QuickUI.button("Cancel", target: self, action: #selector(didTapCancel))
let save = QuickUI.button("Save", target: self, action: #selector(didTapSave))
save.titleLabel?.font = save.titleLabel?.font.bold()
let bar = UIStackView(arrangedSubviews: [cancel, save])
bar.axis = .horizontal
bar.distribution = .equalSpacing
return bar
}()
// MARK: - Init
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
init(title: String? = nil, detail: String? = nil, view custom: CustomView) {
alertTitle = title
alertDetail = detail
customView = custom
super.init(nibName: nil, bundle: nil)
}
override var isModalInPresentation: Bool { set{} get{true} }
override var modalPresentationStyle: UIModalPresentationStyle { set{} get{.custom} }
override var transitioningDelegate: UIViewControllerTransitioningDelegate? {
set {} get {
SlideInTransitioningDelegate(for: .bottom, modal: true)
}
}
internal override func loadView() {
let control = UIView()
control.backgroundColor = .sysBackground
view = control
var tmpPrevivous: UIView? = nil
func adaptive(margin: CGFloat, _ fn: () -> NSLayoutConstraint) {
regularConstraints.append(fn() + margin)
compactConstraints.append(fn() + margin/2)
}
func addLabel(_ lbl: UILabel) {
lbl.numberOfLines = 0
control.addSubview(lbl)
lbl.anchor([.leading, .trailing], to: control.layoutMarginsGuide)
if let p = tmpPrevivous {
adaptive(margin: 16) { lbl.topAnchor =&= p.bottomAnchor }
} else {
adaptive(margin: 12) { lbl.topAnchor =&= control.layoutMarginsGuide.topAnchor }
}
tmpPrevivous = lbl
}
// Alert title & description
if let t = alertTitle {
let lbl = QuickUI.label(t, align: .center, style: .subheadline)
lbl.font = lbl.font.bold()
addLabel(lbl)
}
if let d = alertDetail {
addLabel(QuickUI.label(d, align: .center, style: .footnote))
}
// User content
control.addSubview(customView)
customView.anchor([.leading, .trailing], to: control)
if let p = tmpPrevivous {
customView.topAnchor =&= p.bottomAnchor | .defaultHigh
} else {
customView.topAnchor =&= control.layoutMarginsGuide.topAnchor
}
// Action buttons
control.addSubview(buttonsBar)
buttonsBar.anchor([.leading, .trailing], to: control.layoutMarginsGuide, margin: 8)
buttonsBar.topAnchor =&= customView.bottomAnchor | .defaultHigh
adaptive(margin: 12) { control.layoutMarginsGuide.bottomAnchor =&= buttonsBar.bottomAnchor }
adaptToNewTraits(traitCollection)
view.frame.size = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
}
// MARK: - Adaptive Traits
private var compactConstraints: [NSLayoutConstraint] = []
private var regularConstraints: [NSLayoutConstraint] = []
private func adaptToNewTraits(_ traits: UITraitCollection) {
let flag = traits.verticalSizeClass == .compact
NSLayoutConstraint.deactivate(flag ? regularConstraints : compactConstraints)
NSLayoutConstraint.activate(flag ? compactConstraints : regularConstraints)
view.setNeedsLayout()
}
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
adaptToNewTraits(newCollection)
}
// MARK: - User Interaction
override var keyCommands: [UIKeyCommand]? {
[UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(didTapCancel))]
}
@objc private func didTapCancel() {
callback = nil
dismiss(animated: true)
}
@objc private func didTapSave() {
dismiss(animated: true) {
self.callback?(self.customView)
self.callback = nil
}
}
// MARK: - Present & Dismiss
func present(in viewController: UIViewController, onSuccess: @escaping (CustomView) -> Void) {
callback = onSuccess
viewController.present(self, animated: true)
}
}
// ###################################
// #
// # MARK: - Date Picker Alert
// #
// ###################################
class DatePickerAlert : CustomAlert<UIDatePicker> {
let datePicker = UIDatePicker()
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
init(title: String? = nil, detail: String? = nil, initial date: Date? = nil) {
if let date = date {
datePicker.setDate(date, animated: false)
}
super.init(title: title, detail: detail, view: datePicker)
let now = QuickUI.button("Now", target: self, action: #selector(didTapNow))
now.titleLabel?.font = now.titleLabel?.font.bold()
now.setTitleColor(.sysLabel, for: .normal)
buttonsBar.insertArrangedSubview(now, at: 1)
}
@objc private func didTapNow() {
datePicker.date = Date()
}
func present(in viewController: UIViewController, onSuccess: @escaping (UIDatePicker, Date) -> Void) {
super.present(in: viewController) {
onSuccess($0, $0.date)
}
}
}
// #######################################
// #
// # MARK: - Duration Picker Alert
// #
// #######################################
class DurationPickerAlert: CustomAlert<UIPickerView>, UIPickerViewDataSource, UIPickerViewDelegate {
let pickerView = UIPickerView()
private let dataSource: [[String]]
private let compWidths: [CGFloat]
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
/// - Parameter options: [[List of labels] per component]
/// - Parameter widths: If `nil` set all components to equal width
init(title: String? = nil, detail: String? = nil, options: [[String]], widths: [CGFloat]? = nil) {
assert(widths == nil || widths!.count == options.count, "widths.count != options.count")
dataSource = options
compWidths = widths ?? options.map { _ in 1 / CGFloat(options.count) }
super.init(title: title, detail: detail, view: pickerView)
pickerView.dataSource = self
pickerView.delegate = self
}
func numberOfComponents(in _: UIPickerView) -> Int {
dataSource.count
}
func pickerView(_: UIPickerView, numberOfRowsInComponent c: Int) -> Int {
dataSource[c].count
}
func pickerView(_: UIPickerView, titleForRow r: Int, forComponent c: Int) -> String? {
dataSource[c][r]
}
func pickerView(_ pickerView: UIPickerView, widthForComponent c: Int) -> CGFloat {
compWidths[c] * pickerView.frame.width
}
func present(in viewController: UIViewController, onSuccess: @escaping (UIPickerView, [Int]) -> Void) {
super.present(in: viewController) {
onSuccess($0, $0.selection)
}
}
}
extension UIPickerView {
var selection: [Int] {
get { (0..<numberOfComponents).map { selectedRow(inComponent: $0) } }
set { setSelection(newValue) }
}
/// - Warning: Does not check for boundaries!
func setSelection(_ selection: [Int], animated: Bool = false) {
assert(selection.count == numberOfComponents, "selection.count != components.count")
for (c, i) in selection.enumerated() {
selectRow(i, inComponent: c, animated: animated)
}
}
}

View File

@@ -1,102 +0,0 @@
import UIKit
class DatePickerAlert: UIViewController {
override var keyCommands: [UIKeyCommand]? {
[UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(didTapCancel))]
}
private var callback: (Date) -> Void
private let picker: UIDatePicker = {
let x = UIDatePicker()
let h = x.sizeThatFits(.zero).height
x.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: h)
return x
}()
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
@discardableResult required init(presentIn viewController: UIViewController, configure: ((UIDatePicker) -> Void)? = nil, onSuccess: @escaping (Date) -> Void) {
callback = onSuccess
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .custom
if #available(iOS 13.0, *) {
isModalInPresentation = true
}
presentIn(viewController, configure)
}
internal override func loadView() {
let cancel = QuickUI.button("Discard", target: self, action: #selector(didTapCancel))
let save = QuickUI.button("Save", target: self, action: #selector(didTapSave))
let now = QuickUI.button("Now", target: self, action: #selector(didTapNow))
save.titleLabel?.font = save.titleLabel?.font.bold()
now.titleLabel?.font = now.titleLabel?.font.bold()
now.setTitleColor(.sysFg, for: .normal)
//cancel.setTitleColor(.systemRed, for: .normal)
let buttons = UIStackView(arrangedSubviews: [cancel, now, save])
buttons.axis = .horizontal
buttons.distribution = .equalSpacing
let bg = UIView(frame: picker.frame)
bg.frame.size.height += buttons.frame.height + 15
bg.frame.origin.y = UIScreen.main.bounds.height - bg.frame.height - 15
bg.backgroundColor = .sysBg
bg.addSubview(picker)
bg.addSubview(buttons)
let clearBg = UIView()
clearBg.autoresizingMask = [.flexibleWidth, .flexibleHeight]
clearBg.addSubview(bg)
picker.anchor([.leading, .trailing, .top], to: bg)
picker.bottomAnchor =&= buttons.topAnchor
buttons.anchor([.leading, .trailing], to: bg, margin: 25)
buttons.bottomAnchor =&= bg.bottomAnchor - 15
bg.anchor([.leading, .trailing, .bottom], to: clearBg)
view = clearBg
view.isHidden = true // otherwise picker will flash on present
}
@objc private func didTapNow() {
picker.date = Date()
}
@objc private func didTapSave() {
dismiss(animated: true) {
self.callback(self.picker.date)
}
}
@objc private func didTapCancel() {
dismiss(animated: true)
}
private func presentIn(_ viewController: UIViewController, _ configure: ((UIDatePicker) -> Void)? = nil) {
viewController.present(self, animated: false) {
let control = self.view.subviews.first!
let prev = control.frame.origin.y
control.frame.origin.y += control.frame.height
self.view.isHidden = false
configure?(self.picker)
UIView.animate(withDuration: 0.3) {
self.view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
control.frame.origin.y = prev
}
}
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
UIView.animate(withDuration: 0.3, animations: {
let control = self.view.subviews.first!
self.view.backgroundColor = .clear
control.frame.origin.y += control.frame.height
}) { _ in
super.dismiss(animated: false, completion: completion)
}
}
}

View File

@@ -66,7 +66,7 @@ class TagLabel: UILabel {
@IBDesignable
class MeterBar: UIView {
@IBInspectable var percent: CGFloat = 0 { didSet { setNeedsDisplay() } }
@IBInspectable var barColor: UIColor = .sysFg
@IBInspectable var barColor: UIColor = .sysLink
@IBInspectable var horizontal: Bool = false
private var normPercent: CGFloat { 1 - max(0, min(percent, 1)) }

View File

@@ -0,0 +1,71 @@
import UIKit
struct NotificationBanner {
enum Style {
case fail, ok
}
let view: UIView
init(_ msg: String, style: Style) {
let bg, fg: UIColor
let imgName: String
switch style {
case .fail:
bg = .systemRed
fg = UIColor.black.withAlphaComponent(0.80)
imgName = "circle-x"
case .ok:
bg = .systemGreen
fg = UIColor.black.withAlphaComponent(0.65)
imgName = "circle-check"
}
view = UIView()
view.backgroundColor = bg
let lbl = QuickUI.label(msg, style: .callout)
lbl.textColor = fg
lbl.numberOfLines = 0
lbl.font = lbl.font.bold()
let img = QuickUI.image(UIImage(named: imgName))
img.tintColor = fg
view.addSubview(lbl)
view.addSubview(img)
img.anchor([.centerY], to: lbl)
lbl.anchor([.bottom, .trailing], to: view.layoutMarginsGuide)
img.widthAnchor =&= 25
img.heightAnchor =&= 25
if #available(iOS 11, *) {
img.leadingAnchor =&= view.layoutMarginsGuide.leadingAnchor
lbl.topAnchor =&= view.layoutMarginsGuide.topAnchor
} else {
img.leadingAnchor =&= view.leadingAnchor + 8
lbl.topAnchor =&= view.topAnchor + 8
}
lbl.leadingAnchor =&= img.trailingAnchor + 8
img.bottomAnchor =<= view.bottomAnchor - 8 | .init(rawValue: 999)
lbl.bottomAnchor =<= view.bottomAnchor - 8 | .init(rawValue: 999)
}
/// Animate header banner from the top of the view. Show for `delay` seconds and then hide again.
/// - Parameter onClose: Run after the close animation finishes.
func present(in vc: UIViewController, hideAfter delay: TimeInterval = 3, onClose: (() -> Void)? = nil) {
vc.view.addSubview(view)
view.anchor([.leading, .trailing], to: vc.view!)
view.widthAnchor =&= vc.view!.widthAnchor // Bug? left-right is not sufficient
vc.view.layoutIfNeeded() // sets the height
let h = view.frame.height
let constraint = view.topAnchor =&= vc.view.topAnchor - h
vc.view.layoutIfNeeded() // hide view
UIView.animate(withDuration: 0.3, animations: {
constraint.constant = 0
vc.view.layoutIfNeeded() // animate view
UIView.animate(withDuration: 0.3, delay: delay, options: .curveLinear, animations: {
constraint.constant = -h
vc.view.layoutIfNeeded() // hide again
}, completion: { _ in
self.view.removeFromSuperview()
onClose?()
})
})
}
}

View File

@@ -0,0 +1,126 @@
import Foundation
enum Prefs {
private static var suite: UserDefaults { UserDefaults.standard }
private static func Int(_ key: String) -> Int { suite.integer(forKey: key) }
private static func Int(_ key: String, _ val: Int) { suite.set(val, forKey: key) }
private static func Bool(_ key: String) -> Bool { suite.bool(forKey: key) }
private static func Bool(_ key: String, _ val: Bool) { suite.set(val, forKey: key) }
private static func Str(_ key: String) -> String? { suite.string(forKey: key) }
private static func Str(_ key: String, _ val: String?) { suite.set(val, forKey: key) }
private static func Obj(_ key: String) -> Any? { suite.object(forKey: key) }
private static func Obj(_ key: String, _ val: Any?) { suite.set(val, forKey: key) }
static func registerDefaults() {
suite.register(defaults: [
"RecordingReminderEnabled" : true,
"contextAnalyisCoOccurrenceTime" : 5,
])
}
}
// MARK: - Tutorial
extension Prefs {
enum DidShowTutorial {
static var Welcome: Bool {
get { Prefs.Bool("didShowTutorialAppWelcome") }
set { Prefs.Bool("didShowTutorialAppWelcome", newValue) }
}
static var Recordings: Bool {
get { Prefs.Bool("didShowTutorialRecordings") }
set { Prefs.Bool("didShowTutorialRecordings", newValue) }
}
static var RecordingHowTo: Bool {
get { Prefs.Bool("didShowTutorialRecordingHowTo") }
set { Prefs.Bool("didShowTutorialRecordingHowTo", newValue) }
}
}
}
// MARK: - Date Filter
enum DateFilterKind: Int {
case Off = 0, LastXMin = 1, ABRange = 2;
}
enum DateFilterOrderBy: Int {
case Date = 0, Name = 1, Count = 2;
}
extension Prefs {
enum DateFilter {
static var Kind: DateFilterKind {
get { DateFilterKind(rawValue: Prefs.Int("dateFilterType"))! }
set { Prefs.Int("dateFilterType", newValue.rawValue) }
}
/// Default: `0` (disabled)
static var LastXMin: Int {
get { Prefs.Int("dateFilterLastXMin") }
set { Prefs.Int("dateFilterLastXMin", newValue) }
}
/// Default: `nil` (disabled)
static var RangeA: Timestamp? {
get { Prefs.Obj("dateFilterRangeA") as? Timestamp }
set { Prefs.Obj("dateFilterRangeA", newValue) }
}
/// Default: `nil` (disabled)
static var RangeB: Timestamp? {
get { Prefs.Obj("dateFilterRangeB") as? Timestamp }
set { Prefs.Obj("dateFilterRangeB", newValue) }
}
/// default: `.Date`
static var OrderBy: DateFilterOrderBy {
get { DateFilterOrderBy(rawValue: Prefs.Int("dateFilterOderType"))! }
set { Prefs.Int("dateFilterOderType", newValue.rawValue) }
}
/// default: `false` (Desc)
static var OrderAsc: Bool {
get { Prefs.Bool("dateFilterOderAsc") }
set { Prefs.Bool("dateFilterOderAsc", newValue) }
}
/// - Returns: Timestamp restriction depending on current selected date filter.
/// - `Off` : `(nil, nil)`
/// - `LastXMin` : `(now-LastXMin, nil)`
/// - `ABRange` : `(RangeA, RangeB)`
static func restrictions() -> (type: DateFilterKind, earliest: Timestamp?, latest: Timestamp?) {
let type = Kind
switch type {
case .Off: return (type, nil, nil)
case .LastXMin: return (type, Timestamp.past(minutes: Prefs.DateFilter.LastXMin), nil)
case .ABRange: return (type, Prefs.DateFilter.RangeA, Prefs.DateFilter.RangeB)
}
}
}
}
// MARK: - ContextAnalyis
extension Prefs {
enum ContextAnalyis {
static var CoOccurrenceTime: Int {
get { Prefs.Int("contextAnalyisCoOccurrenceTime") }
set { Prefs.Int("contextAnalyisCoOccurrenceTime", newValue) }
}
}
}
// MARK: - Notifications
extension Prefs {
enum RecordingReminder {
static var Enabled: Bool {
get { Prefs.Bool("RecordingReminderEnabled") }
set { Prefs.Bool("RecordingReminderEnabled", newValue) }
}
static var Sound: String {
get { Prefs.Str("RecordingReminderSound") ?? "#default" }
set { Prefs.Str("RecordingReminderSound", newValue) }
}
}
}

View File

@@ -0,0 +1,105 @@
import Foundation
enum PrefsShared {
private static var suite: UserDefaults { UserDefaults(suiteName: "group.de.uni-bamberg.psi.AppCheck")! }
private static func Int(_ key: String) -> Int { suite.integer(forKey: key) }
private static func Int(_ key: String, _ val: Int) { suite.set(val, forKey: key); suite.synchronize() }
private static func Bool(_ key: String) -> Bool { suite.bool(forKey: key) }
private static func Bool(_ key: String, _ val: Bool) { suite.set(val, forKey: key); suite.synchronize() }
private static func Str(_ key: String) -> String? { suite.string(forKey: key) }
private static func Str(_ key: String, _ val: String?) { suite.set(val, forKey: key); suite.synchronize() }
static func registerDefaults() {
suite.register(defaults: [
"ForceDisconnectSWCD" : true,
"RestartReminderEnabled" : true,
"RestartReminderWithText" : true,
"RestartReminderWithBadge" : true,
"ConnectionAlertsListsElse" : true,
])
}
static var AutoDeleteLogsDays: Int {
get { Int("AutoDeleteLogsDays") }
set { Int("AutoDeleteLogsDays", newValue) }
}
}
// MARK: - Recording State
enum CurrentRecordingState : Int {
case Off = 0, App = 1, Background = 2
}
extension PrefsShared {
static var CurrentlyRecording: CurrentRecordingState {
get { CurrentRecordingState(rawValue: Int("CurrentlyRecording")) ?? .Off }
set { Int("CurrentlyRecording", newValue.rawValue) }
}
static var ForceDisconnectUnresolvableDNS: Bool {
get { PrefsShared.Bool("ForceDisconnectUnresolvableDNS") }
set { PrefsShared.Bool("ForceDisconnectUnresolvableDNS", newValue) }
}
static var ForceDisconnectSWCD: Bool {
get { PrefsShared.Bool("ForceDisconnectSWCD") }
set { PrefsShared.Bool("ForceDisconnectSWCD", newValue) }
}
}
// MARK: - Notifications
extension PrefsShared {
enum RestartReminder {
static var Enabled: Bool {
get { PrefsShared.Bool("RestartReminderEnabled") }
set { PrefsShared.Bool("RestartReminderEnabled", newValue) }
}
static var WithText: Bool {
get { PrefsShared.Bool("RestartReminderWithText") }
set { PrefsShared.Bool("RestartReminderWithText", newValue) }
}
static var WithBadge: Bool {
get { PrefsShared.Bool("RestartReminderWithBadge") }
set { PrefsShared.Bool("RestartReminderWithBadge", newValue) }
}
static var Sound: String {
get { PrefsShared.Str("RestartReminderSound") ?? "#default" }
set { PrefsShared.Str("RestartReminderSound", newValue) }
}
}
enum ConnectionAlerts {
static var Enabled: Bool {
get { PrefsShared.Bool("ConnectionAlertsEnabled") }
set { PrefsShared.Bool("ConnectionAlertsEnabled", newValue) }
}
static var Sound: String {
get { PrefsShared.Str("ConnectionAlertsSound") ?? "#default" }
set { PrefsShared.Str("ConnectionAlertsSound", newValue) }
}
static var ExcludeMode: Bool {
get { PrefsShared.Bool("ConnectionAlertsExcludeMode") }
set { PrefsShared.Bool("ConnectionAlertsExcludeMode", newValue) }
}
enum Lists {
static var CustomA: Bool {
get { PrefsShared.Bool("ConnectionAlertsListsCustomA") }
set { PrefsShared.Bool("ConnectionAlertsListsCustomA", newValue) }
}
static var CustomB: Bool {
get { PrefsShared.Bool("ConnectionAlertsListsCustomB") }
set { PrefsShared.Bool("ConnectionAlertsListsCustomB", newValue) }
}
static var Blocked: Bool {
get { PrefsShared.Bool("ConnectionAlertsListsBlocked") }
set { PrefsShared.Bool("ConnectionAlertsListsBlocked", newValue) }
}
static var Else: Bool {
get { PrefsShared.Bool("ConnectionAlertsListsElse") }
set { PrefsShared.Bool("ConnectionAlertsListsElse", newValue) }
}
}
}
}

View File

@@ -2,10 +2,25 @@ import UIKit
struct QuickUI {
static func label(_ str: String, frame: CGRect = CGRect.zero, align: NSTextAlignment = .natural, style: UIFont.TextStyle = .body) -> UILabel {
let x = UILabel(frame: frame)
x.text = str
x.textAlignment = align
x.font = .preferredFont(forTextStyle: style)
x.constrainHuggingCompression(.horizontal, .defaultLow)
x.constrainHuggingCompression(.vertical, .defaultHigh)
x.sizeToFit()
if #available(iOS 10.0, *) {
x.adjustsFontForContentSizeCategory = true
}
return x
}
static func button(_ title: String, target: Any? = nil, action: Selector? = nil) -> UIButton {
let x = UIButton(type: .roundedRect)
x.setTitle(title, for: .normal)
x.titleLabel?.font = .preferredFont(forTextStyle: .body)
x.constrainHuggingCompression(.vertical, .defaultHigh)
x.sizeToFit()
if let a = action { x.addTarget(target, action: a, for: .touchUpInside) }
if #available(iOS 10.0, *) {

View File

@@ -33,6 +33,13 @@ class SearchBarManager: NSObject, UISearchResultsUpdating {
if #available(iOS 11.0, *) {
tvc?.navigationItem.searchController = controller
} else {
let thv = tvc?.tableView.tableHeaderView
guard thv == nil || thv is UISearchBar else {
// Don't overwrite actions bar (co-occurrence, etc.)
// FIXME: find alternative or iOS 9-10 users can't search in hosts
tvc = nil
return
}
controller.loadViewIfNeeded() // Fix: "Attempting to load the view of a view controller while it is deallocating"
tvc?.definesPresentationContext = true // make search bar disappear if user changes scene (eg. select cell)
//tvc?.tableView.backgroundView = UIView() // iOS 11+ bug: bright white background in dark mode
@@ -42,7 +49,7 @@ class SearchBarManager: NSObject, UISearchResultsUpdating {
}
/// Search callback
func updateSearchResults(for controller: UISearchController) {
internal func updateSearchResults(for controller: UISearchController) {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
perform(#selector(performSearch), with: nil, afterDelay: 0.2)
}

View File

@@ -0,0 +1,182 @@
import UIKit
enum PresentationEdge { case left, top, right, bottom }
// ########################################
// #
// # MARK: - Transitioning Delegate
// #
// ########################################
class SlideInTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
private var edge: PresentationEdge
private var modal: Bool
private var dismissable: Bool
private var shadow: UIColor?
init(for edge: PresentationEdge, modal: Bool, tapAnywhereToDismiss: Bool = false, modalBackgroundColor color: UIColor? = nil) {
self.edge = edge
self.dismissable = tapAnywhereToDismiss
self.shadow = color
self.modal = modal
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
StickyPresentationController(presented: presented, presenting: presenting, stickTo: edge, modal: modal, tapAnywhereToDismiss: dismissable, modalBackgroundColor: shadow)
}
func animationController(forPresented _: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
SlideInAnimationController(from: edge, isPresentation: true)
}
func animationController(forDismissed _: UIViewController) -> UIViewControllerAnimatedTransitioning? {
SlideInAnimationController(from: edge, isPresentation: false)
}
}
// ########################################
// #
// # MARK: - Animated Transitioning
// #
// ########################################
private final class SlideInAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
let edge: PresentationEdge
let appear: Bool
init(from edge: PresentationEdge, isPresentation: Bool) {
self.edge = edge
self.appear = isPresentation
super.init()
}
func transitionDuration(using context: UIViewControllerContextTransitioning?) -> TimeInterval {
(context?.isAnimated ?? true) ? 0.3 : 0.0
}
func animateTransition(using context: UIViewControllerContextTransitioning) {
guard let vc = context.viewController(forKey: appear ? .to : .from) else { return }
var to = context.finalFrame(for: vc)
var from = to
switch edge {
case .left: from.origin.x = -to.width
case .right: from.origin.x = context.containerView.frame.width
case .top: from.origin.y = -to.height
case .bottom: from.origin.y = context.containerView.frame.height
}
if appear { context.containerView.addSubview(vc.view) }
else { swap(&from, &to) }
vc.view.frame = from
UIView.animate(withDuration: transitionDuration(using: context), animations: {
vc.view.frame = to
}, completion: { finished in
if !self.appear { vc.view.removeFromSuperview() }
context.completeTransition(finished)
})
}
}
// #########################################
// #
// # MARK: - Presentation Controller
// #
// #########################################
private class StickyPresentationController: UIPresentationController {
private let stickTo: PresentationEdge
private let isModal: Bool
private let bg = UIView()
private var availableSize: CGSize = .zero // save original size when resizing the container
override var shouldPresentInFullscreen: Bool { false }
override var frameOfPresentedViewInContainerView: CGRect { fittedContentFrame() }
required init(presented: UIViewController, presenting: UIViewController?, stickTo edge: PresentationEdge, modal: Bool = true, tapAnywhereToDismiss: Bool = false, modalBackgroundColor bgColor: UIColor? = nil) {
self.stickTo = edge
self.isModal = modal
super.init(presentedViewController: presented, presenting: presenting)
bg.backgroundColor = bgColor ?? .init(white: 0, alpha: 0.5)
if modal, tapAnywhereToDismiss {
bg.addGestureRecognizer(
UITapGestureRecognizer(target: self, action: #selector(didTapBackground))
)
}
}
// MARK: Present
override func presentationTransitionWillBegin() {
availableSize = containerView!.frame.size
guard isModal else { return }
containerView!.insertSubview(bg, at: 0)
bg.alpha = 0.0
if presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.bg.alpha = 1.0
}) != true { bg.alpha = 1.0 }
}
@objc func didTapBackground(_ sender: UITapGestureRecognizer) {
presentingViewController.dismiss(animated: true)
}
// MARK: Dismiss
override func dismissalTransitionWillBegin() {
if presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.bg.alpha = 0.0
}) != true { bg.alpha = 0.0 }
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
if completed { bg.removeFromSuperview() }
}
// MARK: Update
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
availableSize = size
super.viewWillTransition(to: size, with: coordinator)
}
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
bg.frame = containerView!.bounds
if isModal {
presentedView!.frame = fittedContentFrame()
} else {
containerView!.frame = fittedContentFrame()
presentedView!.frame = containerView!.bounds
}
}
/// Calculate `fittedContentSize()` then offset frame to sticky edge respecting *available* container size .
func fittedContentFrame() -> CGRect {
var frame = CGRect(origin: .zero, size: fittedContentSize())
switch stickTo {
case .right: frame.origin.x = availableSize.width - frame.width
case .bottom: frame.origin.y = availableSize.height - frame.height
default: break
}
return frame
}
/// Calculate best fitting size for available container size and presentation sticky edge.
func fittedContentSize() -> CGSize {
guard let target = presentedView else { return availableSize }
let full = availableSize
let preferred = presentedViewController.preferredContentSize
switch stickTo {
case .left, .right:
let fitted = target.fittingSize(fixedHeight: full.height, preferredWidth: preferred.width)
return CGSize(width: min(fitted.width, full.width), height: full.height)
case .top, .bottom:
let fitted = target.fittingSize(fixedWidth: full.width, preferredHeight: preferred.height)
return CGSize(width: full.width, height: min(fitted.height, full.height))
}
}
}

View File

@@ -0,0 +1,31 @@
import Foundation
class ThrottledBatchQueue<T> {
private var cache: [T] = []
private var scheduled: Bool = false
private let queue: DispatchQueue
private let delay: Double
init(_ delay: Double, using queue: DispatchQueue) {
self.queue = queue
self.delay = delay
}
func addDelayed(_ elem: T, afterDelay closure: @escaping ([T]) -> Void) {
queue.sync {
cache.append(elem)
guard !scheduled else {
return
}
scheduled = true
queue.asyncAfter(deadline: .now() + delay) {
let aCopy = self.cache
self.cache.removeAll(keepingCapacity: true)
self.scheduled = false
DispatchQueue.main.async {
closure(aCopy)
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
import UIKit
struct TinyMarkdown {
/// Load markdown file and run through a (very) simple parser (see below).
/// - Parameters:
/// - filename: Will automatically append `.md` extension
/// - replacements: Replace a single occurrence of search string with an attributed replacement.
static func load(_ filename: String, replacements: [String : NSMutableAttributedString] = [:]) -> UITextView {
let url = Bundle.main.url(forResource: filename, withExtension: "md")!
let str = NSMutableAttributedString(withMarkdown: try! String(contentsOf: url))
for (key, val) in replacements {
guard let r = str.string.range(of: key) else {
QLog.Debug("WARN: markdown key '\(key)' does not exist in \(filename)")
continue
}
str.replaceCharacters(in: NSRange(r, in: str.string), with: val)
}
return QuickUI.text(attributed: str)
}
}
extension NSMutableAttributedString {
/// Supports only: `#h1`, `##h2`, `###h3`, `_italic_`, `__bold__`, `___boldItalic___`
convenience init(withMarkdown content: String) {
self.init()
let emph = try! NSRegularExpression(pattern: #"(?<=(^|\W))(_{1,3})(\S|\S.*?\S)\2"#, options: [])
beginEditing()
content.enumerateLines { (line, _) in
if line.starts(with: "#") {
var h = 0
for char in line {
if char == "#" { h += 1 }
else { break }
}
var line = line
line.removeFirst(h)
line = line.trimmingCharacters(in: CharacterSet(charactersIn: " "))
switch h {
case 1: self.h1(line + "\n")
case 2: self.h2(line + "\n")
default: self.h3(line + "\n")
}
} else {
let nsline = line as NSString
let range = NSRange(location: 0, length: nsline.length)
var i = 0
for x in emph.matches(in: line, options: [], range: range) {
let r = x.range
self.normal(nsline.substring(from: i, to: r.location))
i = r.upperBound
let before = nsline.substring(with: r)
let after = before.trimmingCharacters(in: CharacterSet(charactersIn: "_"))
switch (before.count - after.count) / 2 {
case 1: self.italic(after)
case 2: self.bold(after)
default: self.boldItalic(after)
}
}
if i < range.length {
self.normal(nsline.substring(from: i, to: range.length) + "\n")
} else {
self.normal("\n")
}
}
}
endEditing()
}
}

View File

@@ -25,7 +25,7 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
private let sheetBg: UIView = {
let x = UIView(frame: uniRect)
x.autoresizingMask = [.flexibleWidth, .flexibleHeight]
x.backgroundColor = .sysBg
x.backgroundColor = .sysBackground
x.layer.cornerRadius = cornerRadius
x.layer.shadowColor = UIColor.black.cgColor
x.layer.shadowRadius = 10
@@ -37,8 +37,8 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
private let pager: UIPageControl = {
let x = UIPageControl(frame: uniRect)
x.frame.size.height = x.size(forNumberOfPages: 1).height
x.currentPageIndicatorTintColor = UIColor.sysFg.withAlphaComponent(0.5)
x.pageIndicatorTintColor = UIColor.sysFg.withAlphaComponent(0.25)
x.currentPageIndicatorTintColor = UIColor.sysLabel.withAlphaComponent(0.5)
x.pageIndicatorTintColor = UIColor.sysLabel.withAlphaComponent(0.25)
x.numberOfPages = 0
x.hidesForSinglePage = true
x.addTarget(self, action: #selector(pagerDidChange), for: .valueChanged)
@@ -59,7 +59,7 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
return x
}()
private let button: UIButton = {
private lazy var button: UIButton = {
let x = QuickUI.button("", target: self, action: #selector(buttonTapped))
x.contentEdgeInsets = UIEdgeInsets(all: 8)
return x
@@ -132,9 +132,9 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
sheetBg.addSubview(button)
pager.anchor([.top, .left, .right], to: sheetBg)
pageScroll.topAnchor =&= pager.bottomAnchor
pageScroll.topAnchor =&= pager.bottomAnchor | .defaultHigh
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: sheetInset) | .defaultHigh
button.topAnchor =&= pageScroll.bottomAnchor
button.topAnchor =&= pageScroll.bottomAnchor | .defaultHigh
button.anchor([.bottom, .centerX], to: sheetBg)
// button.bottomAnchor =&= sheetBg.bottomAnchor - 30
// button.centerXAnchor =&= sheetBg.centerXAnchor

View File

@@ -20,27 +20,19 @@ extension SQLiteDatabase {
try ifStep(stmt, SQLITE_ROW)
return sqlite3_column_int(stmt, 0)
}
if version != 1 {
if version != 2 {
QLog.Info("migrate db \(version) -> 2")
// version 0 -> 1: req(domain) -> heap(fqdn, domain)
if version == 0 {
try tempMigrate()
// version 1 -> 2: rec(+subtitle, +opt)
if version == 1 {
transaction("""
ALTER TABLE rec ADD COLUMN subtitle TEXT;
ALTER TABLE rec ADD COLUMN uploadkey TEXT;
""")
}
try run(sql: "PRAGMA user_version = 1;")
try run(sql: "PRAGMA user_version = 2;")
}
}
private func tempMigrate() throws { // TODO: remove with next internal release
do {
try run(sql: "SELECT 1 FROM req LIMIT 1;") // fails if req doesnt exist
createFunction("domainof") { ($0.first as! String).extractDomain() }
try run(sql: """
BEGIN TRANSACTION;
INSERT INTO heap(ts,fqdn,domain,opt) SELECT ts,domain,domainof(domain),nullif(logOpt,0) FROM req;
DROP TABLE req;
COMMIT;
""")
} catch { /* no need to migrate */ }
}
}
private enum TableName: String {
@@ -54,6 +46,10 @@ extension SQLiteDatabase {
return sqlite3_column_int64($0, 0)
}) ?? 0
}
fileprivate func col_ts(_ stmt: OpaquePointer, _ col: Int32) -> Timestamp {
sqlite3_column_int64(stmt, col)
}
}
class WhereClauseBuilder: CustomStringConvertible {
@@ -105,6 +101,7 @@ struct GroupedDomain {
var options: FilterOptions? = nil
}
typealias GroupedTsOccurrence = (ts: Timestamp, total: Int32, blocked: Int32)
typealias DomainTsPair = (domain: String, ts: Timestamp)
extension SQLiteDatabase {
@@ -116,11 +113,9 @@ extension SQLiteDatabase {
guard lastRowId(.cache) > 0 else { return nil }
let before = lastRowId(.heap) + 1
createFunction("domainof") { ($0.first as! String).extractDomain() }
try? run(sql:"""
BEGIN TRANSACTION;
transaction("""
INSERT INTO heap(ts,fqdn,domain,opt) SELECT ts,dns,domainof(dns),nullif(opt&1,0) FROM cache;
DELETE FROM cache;
COMMIT;
""")
let after = lastRowId(.heap)
return (before > after) ? nil : (before, after)
@@ -150,7 +145,7 @@ extension SQLiteDatabase {
func dnsLogsMinDate() -> Timestamp? {
try? run(sql:"SELECT min(ts) FROM heap") {
try ifStep($0, SQLITE_ROW)
return sqlite3_column_int64($0, 0)
return col_ts($0, 0)
}
}
@@ -164,8 +159,19 @@ extension SQLiteDatabase {
let Where = WhereClauseBuilder().and(in: range).and(min: ts1, max: ts2)
return try? run(sql:"SELECT min(rowid), max(rowid) FROM heap \(Where);", bind: Where.bindings) {
try ifStep($0, SQLITE_ROW)
let max = sqlite3_column_int64($0, 1)
return (max == 0) ? nil : (sqlite3_column_int64($0, 0), max)
let max = col_ts($0, 1)
return (max == 0) ? nil : (col_ts($0, 0), max)
}
}
/// Get raw logs between two timestamps. `ts >= ? AND ts <= ?`
/// - Returns: List sorted by `ts` in descending order (newest entries first).
func dnsLogs(between ts1: Timestamp, and ts2: Timestamp) -> [DomainTsPair]? {
try? run(sql: "SELECT fqdn, ts FROM heap WHERE ts >= ? AND ts <= ? ORDER BY ts DESC, rowid ASC;",
bind: [BindInt64(ts1), BindInt64(ts2)]) {
allRows($0) {
(col_text($0, 0) ?? "", col_ts($0, 1))
}
}
}
@@ -189,10 +195,10 @@ extension SQLiteDatabase {
}
return try? run(sql: "SELECT \(col), COUNT(*), COUNT(opt), MAX(ts) FROM heap \(Where) GROUP BY \(col);", bind: Where.bindings) {
allRows($0) {
GroupedDomain(domain: readText($0, 0) ?? "",
GroupedDomain(domain: col_text($0, 0) ?? "",
total: sqlite3_column_int($0, 1),
blocked: sqlite3_column_int($0, 2),
lastModified: sqlite3_column_int64($0, 3))
lastModified: col_ts($0, 3))
}
}
}
@@ -206,7 +212,7 @@ extension SQLiteDatabase {
let Where = WhereClauseBuilder().and(in: range).and("fqdn = ?", BindText(fqdn))
return try? run(sql: "SELECT ts, COUNT(ts), COUNT(opt) FROM heap \(Where) GROUP BY ts ORDER BY ts DESC;", bind: Where.bindings) {
allRows($0) {
(sqlite3_column_int64($0, 0), sqlite3_column_int($0, 1), sqlite3_column_int($0, 2))
(col_ts($0, 0), sqlite3_column_int($0, 1), sqlite3_column_int($0, 2))
}
}
}
@@ -228,9 +234,10 @@ extension SQLiteDatabase {
}
/// Get sorted, unique list of `ts` with given `fqdn`.
func dnsLogsUniqTs(_ fqdn: String) -> [Timestamp]? {
try? run(sql: "SELECT DISTINCT ts FROM heap WHERE fqdn = ? ORDER BY ts;", bind: [BindText(fqdn)]) {
allRows($0) { sqlite3_column_int64($0, 0) }
func dnsLogsUniqTs(_ domain: String, isFQDN flag: Bool) -> [Timestamp]? {
try? run(sql: "SELECT DISTINCT ts FROM heap WHERE \(flag ? "fqdn" : "domain") = ? ORDER BY ts;",
bind: [BindText(domain)]) {
allRows($0) { col_ts($0, 0) }
}
}
@@ -241,7 +248,7 @@ extension SQLiteDatabase {
/// - dt: Search for `ts - dt <= X <= ts + dt`
/// - fqdn: Rows matching this domain will be excluded from the result set.
/// - Returns: List of tuples ordered by rank (ASC).
func contextAnalysis(coOccurrence times: [Timestamp], plusMinus dt: Timestamp, exclude fqdn: String) -> [ContextAnalysisResult]? {
func contextAnalysis(coOccurrence times: [Timestamp], plusMinus dt: Timestamp, exclude domain: String, isFQDN flag: Bool) -> [ContextAnalysisResult]? {
guard times.count > 0 else { return nil }
createFunction("fnDist") {
let x = $0.first as! Timestamp
@@ -266,12 +273,12 @@ extension SQLiteDatabase {
SELECT fqdn, count, avg, (\(fnRank)) rank FROM (
SELECT fqdn, COUNT(*) count, AVG(dist) avg FROM (
SELECT fqdn, fnDist(ts) dist FROM heap
WHERE ts BETWEEN ? AND ? AND fqdn != ? AND dist <= ?
WHERE ts BETWEEN ? AND ? AND \(flag ? "fqdn" : "domain") != ? AND dist <= ?
) GROUP BY fqdn
) ORDER BY rank ASC LIMIT 99;
""", bind: [BindInt64(dt), BindInt64(low), BindInt64(high), BindText(fqdn), BindInt64(dt)]) {
""", bind: [BindInt64(dt), BindInt64(low), BindInt64(high), BindText(domain), BindInt64(dt)]) {
allRows($0) {
(readText($0, 0) ?? "", sqlite3_column_int($0, 1), sqlite3_column_double($0, 2), sqlite3_column_double($0, 3))
(col_text($0, 0) ?? "", sqlite3_column_int($0, 1), sqlite3_column_double($0, 2), sqlite3_column_double($0, 3))
}
}
}
@@ -282,7 +289,7 @@ extension SQLiteDatabase {
// MARK: - Recordings
extension CreateTable {
/// `id`: Primary, `start`: Timestamp, `stop`: Timestamp, `appid`: String, `title`: String, `notes`: String
/// `id`: Primary, `start`: Timestamp, `stop`: Timestamp, `appid`: String, `title`: String, `subtitle`: String, `notes`: String
static var rec: String {"""
CREATE TABLE IF NOT EXISTS rec(
id INTEGER PRIMARY KEY,
@@ -290,19 +297,25 @@ extension CreateTable {
stop INTEGER,
appid TEXT,
title TEXT,
notes TEXT
subtitle TEXT,
notes TEXT,
uploadkey TEXT
);
"""}
}
let readRecordingSelect = "id, start, stop, appid, title, subtitle, notes, uploadkey"
struct Recording {
let id: sqlite3_int64
let start: Timestamp
let stop: Timestamp?
var appId: String? = nil
var title: String? = nil
var subtitle: String? = nil
var notes: String? = nil
var uploadkey: String? = nil
}
typealias AppBundleInfo = (bundleId: String, name: String?, author: String?)
extension SQLiteDatabase {
@@ -329,8 +342,9 @@ extension SQLiteDatabase {
/// Update given recording by replacing `title`, `appid`, and `notes` with new values.
func recordingUpdate(_ r: Recording) {
try? run(sql: "UPDATE rec SET title = ?, appid = ?, notes = ? WHERE id = ? LIMIT 1;",
bind: [BindTextOrNil(r.title), BindTextOrNil(r.appId), BindTextOrNil(r.notes), BindInt64(r.id)]) { stmt -> Void in
try? run(sql: "UPDATE rec SET appid = ?, title = ?, subtitle = ?, notes = ?, uploadkey = ? WHERE id = ? LIMIT 1;",
bind: [BindTextOrNil(r.appId), BindTextOrNil(r.title), BindTextOrNil(r.subtitle),
BindTextOrNil(r.notes), BindTextOrNil(r.uploadkey), BindInt64(r.id)]) { stmt -> Void in
sqlite3_step(stmt)
}
}
@@ -348,37 +362,55 @@ extension SQLiteDatabase {
// MARK: read
private func readRecording(_ stmt: OpaquePointer) -> Recording {
let end = sqlite3_column_int64(stmt, 2)
let end = col_ts(stmt, 2)
return Recording(id: sqlite3_column_int64(stmt, 0),
start: sqlite3_column_int64(stmt, 1),
start: col_ts(stmt, 1),
stop: end == 0 ? nil : end,
appId: readText(stmt, 3),
title: readText(stmt, 4),
notes: readText(stmt, 5))
appId: col_text(stmt, 3),
title: col_text(stmt, 4),
subtitle: col_text(stmt, 5),
notes: col_text(stmt, 6),
uploadkey: col_text(stmt, 7))
}
/// `WHERE stop IS NULL`
func recordingGetOngoing() -> Recording? {
try? run(sql: "SELECT * FROM rec WHERE stop IS NULL LIMIT 1;") {
try? run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE stop IS NULL LIMIT 1;") {
try ifStep($0, SQLITE_ROW)
return readRecording($0)
}
}
/// Get `Timestamp` of last recording.
func recordingLastTimestamp() -> Timestamp? {
try? run(sql: "SELECT stop FROM rec WHERE stop IS NOT NULL ORDER BY rowid DESC LIMIT 1;") {
try ifStep($0, SQLITE_ROW)
return col_ts($0, 0)
}
}
/// `WHERE stop IS NOT NULL`
func recordingGetAll() -> [Recording]? {
try? run(sql: "SELECT * FROM rec WHERE stop IS NOT NULL;") {
try? run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE stop IS NOT NULL;") {
allRows($0) { readRecording($0) }
}
}
/// `WHERE id = ?`
private func recordingGet(withID: sqlite3_int64) throws -> Recording {
try run(sql: "SELECT * FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(withID)]) {
try run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(withID)]) {
try ifStep($0, SQLITE_ROW)
return readRecording($0)
}
}
func appBundleList() -> [AppBundleInfo]? {
try? run(sql: "SELECT appid, title, subtitle FROM rec WHERE appid IS NOT NULL GROUP BY appid ORDER BY lower(title) ASC;") {
allRows($0) {
AppBundleInfo(col_text($0, 0)!, col_text($0, 1), col_text($0, 2))
}
}
}
}
@@ -396,8 +428,6 @@ extension CreateTable {
"""}
}
typealias RecordLog = (domain: String, count: Int32)
extension SQLiteDatabase {
// MARK: write
@@ -426,13 +456,24 @@ extension SQLiteDatabase {
}
}
/// Delete one recording log entry with given `recording id`, matching `domain`, and `ts`.
/// - Returns: `true` if row was deleted
func recordingLogsDelete(_ recId: sqlite3_int64, singleEntry ts: Timestamp, domain: String) throws -> Bool {
try run(sql: "DELETE FROM recLog WHERE rid = ? AND ts = ? AND domain = ? LIMIT 1;",
bind: [BindInt64(recId), BindInt64(ts), BindText(domain)]) {
try ifStep($0, SQLITE_DONE)
return numberOfChanges > 0
}
}
// MARK: read
/// List of domains and count occurences for given recording.
func recordingLogsGetGrouped(_ r: Recording) -> [RecordLog]? {
try? run(sql: "SELECT domain, COUNT() FROM recLog WHERE rid = ? GROUP BY domain;",
/// - Returns: List of `(domain, ts)` pairs. Sorted by `ts` in ascending order (oldest first)
func recordingLogsGet(_ r: Recording) -> [DomainTsPair]? {
try? run(sql: "SELECT domain, ts FROM recLog WHERE rid = ? ORDER BY ts ASC, rowid DESC;",
bind: [BindInt64(r.id)]) {
allRows($0) { (readText($0, 0) ?? "", sqlite3_column_int($0, 1)) }
allRows($0) { (col_text($0, 0) ?? "", col_ts($0, 1)) }
}
}
}

View File

@@ -39,9 +39,26 @@ extension SQLiteDatabase {
/// `INSERT INTO cache (dns, opt) VALUES (?, ?);`
func logWrite(_ domain: String, blocked: Bool = false) throws {
try self.run(sql: "INSERT INTO cache (dns, opt) VALUES (?, ?);",
bind: [BindText(domain), BindInt32(blocked ? 1 : 0)])
bind: [BindText(domain), BindInt32(blocked ? 1 : 0)])
{ try ifStep($0, SQLITE_DONE) }
}
/// `DELETE FROM cache WHERE ts < (now - ? days);`
/// - Parameter days: if `0` or negative, this function does nothing.
/// - Returns: `true` if at least one row was deleted.
@discardableResult func dnsLogsDeleteOlderThan(days: Int) throws -> Bool {
guard days > 0 else { return false }
func delFrom(_ table: String) throws -> Bool {
return try self.run(sql: "DELETE FROM \(table) WHERE ts < strftime('%s', 'now', ?);",
bind: [BindText("-\(days) days")]) {
try ifStep($0, SQLITE_DONE)
return numberOfChanges > 0
}
}
let didDelHeap = try delFrom("heap")
let didDelCache = try delFrom("cache")
return didDelHeap || didDelCache
}
}
@@ -62,7 +79,9 @@ struct FilterOptions: OptionSet {
static let none = FilterOptions([])
static let blocked = FilterOptions(rawValue: 1 << 0)
static let ignored = FilterOptions(rawValue: 1 << 1)
static let any = FilterOptions(rawValue: 0b11)
static let customA = FilterOptions(rawValue: 1 << 2)
static let customB = FilterOptions(rawValue: 1 << 3)
static let any = FilterOptions(rawValue: 0b1111)
}
extension SQLiteDatabase {
@@ -71,7 +90,7 @@ extension SQLiteDatabase {
return try? run(sql: "SELECT domain, opt FROM filter \(rv>0 ? "WHERE opt & ?" : "");",
bind: rv>0 ? [BindInt32(rv)] : []) {
allRowsKeyed($0) {
(key: readText($0, 0) ?? "",
(key: col_text($0, 0) ?? "",
value: FilterOptions(rawValue: sqlite3_column_int($0, 1)))
}
}

View File

@@ -48,6 +48,7 @@ class SQLiteDatabase {
static func open(path: String = URL.internalDB().relativePath) throws -> SQLiteDatabase {
var db: OpaquePointer?
if sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK {
sqlite3_busy_timeout(db, 800)
return SQLiteDatabase(dbPointer: db)
} else {
defer { sqlite3_close_v2(db) }
@@ -91,15 +92,20 @@ class SQLiteDatabase {
}
}
/// `BEGIN TRANSACTION; \(sql); COMMIT;` on exception rollback.
func transaction(_ sql: String) {
do { try run(sql: "BEGIN TRANSACTION; \(sql); COMMIT;") }
catch { rollback() }
}
func ifStep(_ stmt: OpaquePointer, _ expected: Int32) throws {
guard sqlite3_step(stmt) == expected else {
throw SQLiteError.Step(message: errorMessage)
}
}
func vacuum() {
try? run(sql: "VACUUM;")
}
func vacuum() { NSLog("[SQL] VACUUM"); try? run(sql: "VACUUM;"); }
func rollback() { NSLog("[SQL] ROLLBACK"); try? run(sql: "ROLLBACK;"); }
}
@@ -163,6 +169,10 @@ protocol DBBinding {
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32
}
struct BindNull : DBBinding {
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32 { sqlite3_bind_null(stmt, col) }
}
struct BindInt32 : DBBinding {
let raw: Int32
init(_ value: Int32) { raw = value }
@@ -193,7 +203,7 @@ extension SQLiteDatabase {
var numberOfChanges: Int32 { get { sqlite3_changes(dbPointer) } }
var lastInsertedRow: SQLiteRowID { get { sqlite3_last_insert_rowid(dbPointer) } }
func readText(_ stmt: OpaquePointer, _ col: Int32) -> String? {
func col_text(_ stmt: OpaquePointer, _ col: Int32) -> String? {
let val = sqlite3_column_text(stmt, col)
return (val != nil ? String(cString: val!) : nil)
}

View File

@@ -33,8 +33,13 @@ extension FilterOptions {
}
extension Recording {
var fallbackTitle: String { get { "Unnamed Recording #\(id)" } }
var duration: Timestamp? { get { stop == nil ? nil : stop! - start } }
var durationString: String? { get { stop == nil ? nil : TimeFormat.from(duration!) } }
static let minTimeLongTerm: Timestamp = .hours(1)
var fallbackTitle: String { get {
isLongTerm ? "Background Recording" : "Unnamed Recording #\(id)"
} }
var duration: Timestamp { get { (stop ?? .now()) - start } }
var isLongTerm: Bool { duration > Recording.minTimeLongTerm }
var isShared: Bool { uploadkey?.count ?? 0 > 0}
}

View File

@@ -15,7 +15,7 @@ struct TheGreatDestroyer {
}
}
/// Fired when user taps on Settings -> Delete All Logs
/// Fired when user taps on Settings -> "Delete All Logs"
static func deleteAllLogs() {
sync.pause()
DispatchQueue.global().async {
@@ -26,4 +26,21 @@ struct TheGreatDestroyer {
} catch {}
}
}
/// Fired when user changes Settings -> "Auto-delete logs" and every time the App enters foreground
static func deleteLogs(olderThan days: Int) {
guard days > 0 else { return }
sync.pause()
DispatchQueue.global().async {
defer { sync.continue() }
QLog.Info("Auto-delete logs")
do {
if try AppDB!.dnsLogsDeleteOlderThan(days: days) {
sync.needsReloadDB()
}
} catch {
QLog.Warning("Couldn't auto-delete logs, \(error)")
}
}
}
}

View File

@@ -21,10 +21,13 @@ enum DomainFilter {
}
/// Get total number of blocked and ignored domains. Shown in settings overview.
static func counts() -> (blocked: Int, ignored: Int) {
data.reduce(into: (0, 0)) {
static func counts() -> (blocked: Int, ignored: Int, listCustomA: Int, listCustomB: Int) {
data.reduce(into: (0, 0, 0, 0)) {
if $1.1.contains(.blocked) { $0.0 += 1 }
if $1.1.contains(.ignored) { $0.1 += 1 } }
if $1.1.contains(.ignored) { $0.1 += 1 }
if $1.1.contains(.customA) { $0.2 += 1 }
if $1.1.contains(.customB) { $0.3 += 1 }
}
}
/// Union `filter` with set.

View File

@@ -55,8 +55,8 @@ class GroupedDomainDataSource: FilterPipelineDelegate, SyncUpdateDelegate {
/// Read user defaults and apply new sorting order. Either by setting a new or reversing the current.
/// - Parameter force: If `true` set new sorting even if the type does not differ.
private func resetSortingOrder(force: Bool = false) {
let orderAscChanged = (orderAsc <-? Pref.DateFilter.OrderAsc)
let orderTypChanged = (currentOrder <-? Pref.DateFilter.OrderBy)
let orderAscChanged = (orderAsc <-? Prefs.DateFilter.OrderAsc)
let orderTypChanged = (currentOrder <-? Prefs.DateFilter.OrderBy)
if orderTypChanged || force {
switch currentOrder {
case .Date:

View File

@@ -13,6 +13,9 @@ enum RecordingsDB {
/// Get list of all recordings
static func list() -> [Recording] { AppDB?.recordingGetAll() ?? [] }
/// Get `Timestamp` of latest recording
static func lastTimestamp() -> Timestamp? { AppDB?.recordingLastTimestamp() }
/// Copy log entries from generic `heap` table to recording specific `recLog` table
static func persist(_ r: Recording) {
sync.syncNow { // persist changes in cache before copying recording details
@@ -21,8 +24,20 @@ enum RecordingsDB {
}
/// Get list of domains that occured during the recording
static func details(_ r: Recording) -> [RecordLog] {
AppDB?.recordingLogsGetGrouped(r) ?? []
static func details(_ r: Recording) -> [DomainTsPair] {
AppDB?.recordingLogsGet(r) ?? []
}
/// Get dictionary of domains with `ts` in ascending order.
static func detailCluster(_ r: Recording) -> [String : [Timestamp]] {
var cluster: [String : [Timestamp]] = [:]
for (dom, ts) in details(r) {
if cluster[dom] == nil {
cluster[dom] = []
}
cluster[dom]!.append(ts - r.start)
}
return cluster
}
/// Update `title`, `appid`, and `notes` and post `NotifyRecordingChanged` notification.
@@ -43,5 +58,16 @@ enum RecordingsDB {
static func deleteDetails(_ r: Recording, domain: String) -> Bool {
((try? AppDB?.recordingLogsDelete(r.id, matchingDomain: domain)) ?? 0) > 0
}
/// Delete individual entries from recording while keeping the recording alive.
/// - Returns: `true` if at least one row is deleted.
static func deleteSingle(_ r: Recording, domain: String, ts: Timestamp) -> Bool {
(try? AppDB?.recordingLogsDelete(r.id, singleEntry: ts, domain: domain)) ?? false
}
/// Return list of previously used apps found in all recordings.
static func appList() -> [AppBundleInfo] {
AppDB?.appBundleList() ?? []
}
}

View File

@@ -2,7 +2,10 @@ import Foundation
#if IOS_SIMULATOR
class TestDataSource {
fileprivate var hook : GlassVPNHook!
class SimulatorVPN {
static var timer: Timer?
static func load() {
QLog.Debug("SQLite path: \(URL.internalDB())")
@@ -27,13 +30,36 @@ class TestDataSource {
db.setFilter("bi.test.com", [.blocked, .ignored])
QLog.Debug("Done")
Timer.repeating(2, call: #selector(insertRandom), on: self)
}
static func start() {
hook = GlassVPNHook()
timer = Timer.repeating(2, call: #selector(insertRandom), on: self)
}
static func stop() {
timer?.invalidate()
timer = nil
hook.cleanUp()
hook = nil
}
@objc static func insertRandom() {
//QLog.Debug("Inserting 1 periodic log entry")
try? AppDB?.logWrite("\(arc4random() % 5).count.test.com", blocked: true)
let rand = arc4random() % 8
let domain: String
switch rand {
case 6: domain = "tmp.b.test.com"
case 7: domain = "tmp.i.test.com"
case 8: domain = "tmp.bi.test.com"
default: domain = "\(rand).count.test.com"
}
let kill = hook.processDNSRequest(domain)
if kill { QLog.Info("Blocked: \(domain)") }
}
static func sendMsg(_ messageData: Data) {
hook.handleAppMessage(messageData)
}
}
#endif

View File

@@ -1,5 +1,7 @@
import UIKit
let sync = SyncUpdate(periodic: 7)
class SyncUpdate {
private var lastSync: TimeInterval = 0
private var timer: Timer!
@@ -18,8 +20,8 @@ class SyncUpdate {
private(set) var tsLatest: Timestamp? // as set per user, not actual latest
init(periodic interval: TimeInterval) {
(filterType, tsEarliest, tsLatest) = Pref.DateFilter.restrictions()
fileprivate init(periodic interval: TimeInterval) {
(filterType, tsEarliest, tsLatest) = Prefs.DateFilter.restrictions()
reloadRangeFromDB()
NotifyDateFilterChanged.observe(call: #selector(didChangeDateFilter), on: self)
@@ -33,7 +35,7 @@ class SyncUpdate {
/// Callback fired when user changes `DateFilter` on root tableView controller
@objc private func didChangeDateFilter() {
self.pause()
let filter = Pref.DateFilter.restrictions()
let filter = Prefs.DateFilter.restrictions()
filterType = filter.type
DispatchQueue.global().async {
// Not necessary, but improve execution order (delete then insert).
@@ -109,7 +111,7 @@ class SyncUpdate {
}
}
if filterType == .LastXMin {
set(newEarliest: Timestamp.past(minutes: Pref.DateFilter.LastXMin))
set(newEarliest: Timestamp.past(minutes: Prefs.DateFilter.LastXMin))
}
// TODO: periodic hard delete old logs (will reset rowids!)
}

View File

@@ -31,12 +31,21 @@ func ErrorAlert(_ errorDescription: String, buttonText: String = "Dismiss") -> U
/// - Parameters:
/// - buttonText: Default: `"Continue"`
/// - buttonStyle: Default: `.default`
func AskAlert(title: String?, text: String?, buttonText: String = "Continue", buttonStyle: UIAlertAction.Style = .default, action: @escaping (UIAlertController) -> Void) -> UIAlertController {
let alert = Alert(title: title, text: text, buttonText: "Cancel")
func AskAlert(title: String?, text: String?, buttonText: String = "Continue", cancelButton: String = "Cancel", buttonStyle: UIAlertAction.Style = .default, action: @escaping (UIAlertController) -> Void) -> UIAlertController {
let alert = Alert(title: title, text: text, buttonText: cancelButton)
alert.addAction(UIAlertAction(title: buttonText, style: buttonStyle) { _ in action(alert) })
return alert
}
/// Show alert hinting the user to go to system settings and re-enable notifications.
func NotificationsDisabledAlert(presentIn viewController: UIViewController) {
AskAlert(title: "Notifications Disabled",
text: "Go to System Settings > Notifications > AppCheck to re-enable notifications.",
buttonText: "Open settings") { _ in
URL(string: UIApplication.openSettingsURLString)?.open()
}.presentIn(viewController)
}
// MARK: Alert with multiple options
/// - Parameters:

View File

@@ -47,6 +47,15 @@ extension NSLayoutConstraint {
@discardableResult static func |(l: NSLayoutConstraint, r: UILayoutPriority) -> NSLayoutConstraint { l.priority = r; return l }
}
extension NSLayoutDimension {
/// Create and activate an `equal` constraint with constant value. Format: `A.anchor =&= constant | priority`
@discardableResult static func =&= (l: NSLayoutDimension, r: CGFloat) -> NSLayoutConstraint { l.constraint(equalToConstant: r).on() }
/// Create and activate a `lessThan` constraint with constant value. Format: `A.anchor =<= constant | priority`
@discardableResult static func =<= (l: NSLayoutDimension, r: CGFloat) -> NSLayoutConstraint { l.constraint(lessThanOrEqualToConstant: r).on() }
/// Create and activate a `greaterThan` constraint with constant value. Format: `A.anchor =>= constant | priority`
@discardableResult static func =>= (l: NSLayoutDimension, r: CGFloat) -> NSLayoutConstraint { l.constraint(greaterThanOrEqualToConstant: r).on() }
}
/*
UIView extension to generate multiple constraints at once
@@ -73,6 +82,12 @@ extension UIView {
return NSLayoutConstraint(item: A, attribute: $0, relatedBy: rel, toItem: B, attribute: $0, multiplier: 1, constant: margin).on()
}
}
/// Sets the priority with which a view resists being made smaller and larger than its intrinsic size.
func constrainHuggingCompression(_ axis: NSLayoutConstraint.Axis, _ priotity: UILayoutPriority) {
setContentHuggingPriority(priotity, for: axis)
setContentCompressionResistancePriority(priotity, for: axis)
}
}
extension Array where Element: NSLayoutConstraint {

View File

@@ -0,0 +1,25 @@
import UIKit
// See: https://noahgilmore.com/blog/dark-mode-uicolor-compatibility/
extension UIColor {
/// `.systemBackground ?? .white`
static var sysBackground: UIColor { if #available(iOS 13.0, *) { return .systemBackground } else { return .white } }
/// `.link ?? .systemBlue`
static var sysLink: UIColor { if #available(iOS 13.0, *) { return .link } else { return .systemBlue } }
/// `.label ?? .black`
static var sysLabel: UIColor { if #available(iOS 13.0, *) { return .label } else { return .black } }
/// `.secondaryLabel ?? rgba(60, 60, 67, 0.6)`
static var sysLabel2: UIColor { if #available(iOS 13.0, *) { return .secondaryLabel } else { return .init(red: 60/255.0, green: 60/255.0, blue: 67/255.0, alpha: 0.6) } }
/// `.tertiaryLabel ?? rgba(60, 60, 67, 0.3)`
static var sysLabel3: UIColor { if #available(iOS 13.0, *) { return .tertiaryLabel } else { return .init(red: 60/255.0, green: 60/255.0, blue: 67/255.0, alpha: 0.3) } }
}
extension NSMutableAttributedString {
func withColor(_ color: UIColor, fromBack: Int) -> Self {
let l = length - fromBack
let r = (l < 0) ? NSMakeRange(0, length) : NSMakeRange(l, fromBack)
self.addAttribute(.foregroundColor, value: color, range: r)
return self
}
}

View File

@@ -0,0 +1,33 @@
precedencegroup CompareAssignPrecedence {
assignment: true
associativity: left
higherThan: ComparisonPrecedence
}
infix operator <-? : CompareAssignPrecedence
infix operator <-/ : CompareAssignPrecedence
extension Equatable {
/// Assign a new value to `lhs` if `newValue` differs from the previous value. Return `false` if they are equal.
/// - Returns: `true` if `lhs` was overwritten with another value
static func <-?(lhs: inout Self, newValue: Self) -> Bool {
if lhs != newValue {
lhs = newValue
return true
}
return false
}
/// Assign a new value to `lhs` if `newValue` differs from the previous value.
/// Return tuple with both values. Or `nil` if they are equal.
/// - Returns: `nil` if `previousValue == newValue`
static func <-/(lhs: inout Self, newValue: Self) -> (previousValue: Self, newValue: Self)? {
let previousValue = lhs
if previousValue != newValue {
lhs = newValue
return (previousValue, newValue)
}
return nil
}
}

View File

@@ -6,6 +6,7 @@ extension UIFont {
}
func bold() -> UIFont { withTraits(traits: .traitBold) }
func italic() -> UIFont { withTraits(traits: .traitItalic) }
func boldItalic() -> UIFont { withTraits(traits: [.traitBold, .traitItalic]) }
func monoSpace() -> UIFont {
let traits = fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any] ?? [:]
let weight = (traits[.weight] as? CGFloat) ?? UIFont.Weight.regular.rawValue
@@ -13,39 +14,35 @@ extension UIFont {
}
}
extension NSAttributedString {
static func image(_ img: UIImage) -> Self {
extension NSMutableAttributedString {
convenience init(image: UIImage, centered: Bool = false) {
self.init()
let att = NSTextAttachment()
att.image = img
return self.init(attachment: att)
att.image = image
append(.init(attachment: att))
if centered {
let ps = NSMutableParagraphStyle()
ps.alignment = .center
addAttribute(.paragraphStyle, value: ps, range: .init(location: 0, length: length))
}
}
}
extension NSMutableAttributedString {
static private var def: UIFont = .preferredFont(forTextStyle: .body)
@discardableResult func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) }
@discardableResult func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) }
@discardableResult func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) }
@discardableResult func boldItalic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).boldItalic()) }
func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) }
func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) }
func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) }
func h1(_ str: String) -> Self { normal(str, .title1) }
func h2(_ str: String) -> Self { normal(str, .title2) }
func h3(_ str: String) -> Self { normal(str, .title3) }
@discardableResult func h1(_ str: String) -> Self { normal(str, .title1) }
@discardableResult func h2(_ str: String) -> Self { normal(str, .title2) }
@discardableResult func h3(_ str: String) -> Self { normal(str, .title3) }
private func append(_ str: String, withFont: UIFont) -> Self {
append(NSAttributedString(string: str, attributes: [
.font : withFont,
.foregroundColor : UIColor.sysFg
.foregroundColor : UIColor.sysLabel
]))
return self
}
func centered(_ content: NSAttributedString) -> Self {
let before = length
append(content)
let ps = NSMutableParagraphStyle()
ps.alignment = .center
addAttribute(.paragraphStyle, value: ps, range: .init(location: before, length: content.length))
return self
}
}

View File

@@ -1,60 +0,0 @@
import UIKit
struct QLog {
private init() {}
static func m(_ message: String) { write("", message) }
static func Info(_ message: String) { write("[INFO] ", message) }
#if DEBUG
static func Debug(_ message: String) { write("[DEBUG] ", message) }
#else
static func Debug(_ _: String) {}
#endif
static func Error(_ message: String) { write("[ERROR] ", message) }
static func Warning(_ message: String) { write("[WARN] ", message) }
private static func write(_ tag: String, _ message: String) {
print(String(format: "%1.3f %@%@", Date().timeIntervalSince1970, tag, message))
}
}
extension UIColor {
static var sysBg: UIColor { get { if #available(iOS 13.0, *) { return .systemBackground } else { return .white } }}
static var sysFg: UIColor { get { if #available(iOS 13.0, *) { return .label } else { return .black } }}
}
extension UIEdgeInsets {
init(all: CGFloat = 0, top: CGFloat? = nil, left: CGFloat? = nil, bottom: CGFloat? = nil, right: CGFloat? = nil) {
self.init(top: top ?? all, left: left ?? all, bottom: bottom ?? all, right: right ?? all)
}
}
precedencegroup CompareAssignPrecedence {
assignment: true
associativity: left
higherThan: ComparisonPrecedence
}
infix operator <-? : CompareAssignPrecedence
infix operator <-/ : CompareAssignPrecedence
extension Equatable {
/// Assign a new value to `lhs` if `newValue` differs from the previous value. Return `false` if they are equal.
/// - Returns: `true` if `lhs` was overwritten with another value
static func <-?(lhs: inout Self, newValue: Self) -> Bool {
if lhs != newValue {
lhs = newValue
return true
}
return false
}
/// Assign a new value to `lhs` if `newValue` differs from the previous value.
/// Return tuple with both values. Or `nil` if they are equal.
/// - Returns: `nil` if `previousValue == newValue`
static func <-/(lhs: inout Self, newValue: Self) -> (previousValue: Self, newValue: Self)? {
let previousValue = lhs
if previousValue != newValue {
lhs = newValue
return (previousValue, newValue)
}
return nil
}
}

View File

@@ -0,0 +1,17 @@
import Foundation
struct QLog {
private init() {}
static func m(_ message: String) { write("", message) }
static func Info(_ message: String) { write("[INFO] ", message) }
#if DEBUG
static func Debug(_ message: String) { write("[DEBUG] ", message) }
#else
static func Debug(_ _: String) {}
#endif
static func Error(_ message: String) { write("[ERROR] ", message) }
static func Warning(_ message: String) { write("[WARN] ", message) }
private static func write(_ tag: String, _ message: String) {
print(String(format: "%1.3f %@%@", Date().timeIntervalSince1970, tag, message))
}
}

View File

@@ -1,6 +1,6 @@
import Foundation
let NotifyVPNStateChanged = NSNotification.Name("GlassVPNStateChanged") // VPNState!
let NotifyVPNStateChanged = NSNotification.Name("GlassVPNStateChanged") // nil!
let NotifyDNSFilterChanged = NSNotification.Name("PSIDNSFilterSettingsChanged") // domain: String!
let NotifyDateFilterChanged = NSNotification.Name("PSIDateFilterSettingsChanged") // nil!
let NotifySortOrderChanged = NSNotification.Name("PSIDateFilterSortOrderChanged") // nil!

View File

@@ -1,84 +0,0 @@
import Foundation
var currentVPNState: VPNState = .off
let sync = SyncUpdate(periodic: 7)
public enum VPNState : Int {
case on = 1, inbetween, off
}
enum Pref {
static func Int(_ key: String) -> Int { UserDefaults.standard.integer(forKey: key) }
static func Int(_ val: Int, _ key: String) { UserDefaults.standard.set(val, forKey: key) }
static func Bool(_ key: String) -> Bool { UserDefaults.standard.bool(forKey: key) }
static func Bool(_ val: Bool, _ key: String) { UserDefaults.standard.set(val, forKey: key) }
static func `Any`(_ key: String) -> Any? { UserDefaults.standard.object(forKey: key) }
static func `Any`(_ val: Any?, _ key: String) { UserDefaults.standard.set(val, forKey: key) }
enum DidShowTutorial {
static var Welcome: Bool {
get { Pref.Bool("didShowTutorialAppWelcome") }
set { Pref.Bool(newValue, "didShowTutorialAppWelcome") }
}
static var Recordings: Bool {
get { Pref.Bool("didShowTutorialRecordings") }
set { Pref.Bool(newValue, "didShowTutorialRecordings") }
}
}
enum ContextAnalyis {
static var CoOccurrenceTime: Int? {
get { Pref.Any("contextAnalyisCoOccurrenceTime") as? Int }
set { Pref.Any(newValue, "contextAnalyisCoOccurrenceTime") }
}
}
enum DateFilter {
static var Kind: DateFilterKind {
get { DateFilterKind(rawValue: Pref.Int("dateFilterType"))! }
set { Pref.Int(newValue.rawValue, "dateFilterType") }
}
/// Default: `0` (disabled)
static var LastXMin: Int {
get { Pref.Int("dateFilterLastXMin") }
set { Pref.Int(newValue, "dateFilterLastXMin") }
}
/// Default: `nil` (disabled)
static var RangeA: Timestamp? {
get { Pref.Any("dateFilterRangeA") as? Timestamp }
set { Pref.Any(newValue, "dateFilterRangeA") }
}
/// Default: `nil` (disabled)
static var RangeB: Timestamp? {
get { Pref.Any("dateFilterRangeB") as? Timestamp }
set { Pref.Any(newValue, "dateFilterRangeB") }
}
/// default: `.Date`
static var OrderBy: DateFilterOrderBy {
get { DateFilterOrderBy(rawValue: Pref.Int("dateFilterOderType"))! }
set { Pref.Int(newValue.rawValue, "dateFilterOderType") }
}
/// default: `false` (Desc)
static var OrderAsc: Bool {
get { Pref.Bool("dateFilterOderAsc") }
set { Pref.Bool(newValue, "dateFilterOderAsc") }
}
/// - Returns: Timestamp restriction depending on current selected date filter.
/// - `Off` : `(nil, nil)`
/// - `LastXMin` : `(now-LastXMin, nil)`
/// - `ABRange` : `(RangeA, RangeB)`
static func restrictions() -> (type: DateFilterKind, earliest: Timestamp?, latest: Timestamp?) {
let type = Kind
switch type {
case .Off: return (type, nil, nil)
case .LastXMin: return (type, Timestamp.past(minutes: Pref.DateFilter.LastXMin), nil)
case .ABRange: return (type, Pref.DateFilter.RangeA, Pref.DateFilter.RangeB)
}
}
}
}
enum DateFilterKind: Int {
case Off = 0, LastXMin = 1, ABRange = 2;
}
enum DateFilterOrderBy: Int {
case Date = 0, Name = 1, Count = 2;
}

View File

@@ -1,14 +1,5 @@
import UIKit
extension NSMutableAttributedString {
func withColor(_ color: UIColor, fromBack: Int) -> Self {
let l = length - fromBack
let r = (l < 0) ? NSMakeRange(0, length) : NSMakeRange(l, fromBack)
self.addAttribute(.foregroundColor, value: color, range: r)
return self
}
}
extension String {
/// Check if string is equal to `domain` or ends with `.domain`
func isSubdomain(of domain: String) -> Bool { self == domain || self.hasSuffix("." + domain) }
@@ -34,6 +25,13 @@ extension String {
let parts = components(separatedBy: ".")
return parts.count == 2 && listOfSLDs[parts.last!]?[parts.first!] ?? false
}
func isValidBundleId() -> Bool {
let regex = try! NSRegularExpression(pattern: #"^[A-Za-z0-9\.\-]{1,155}$"#, options: .anchorsMatchLines)
let range = NSRange(location: 0, length: self.utf16.count)
let matches = regex.matches(in: self, options: .anchored, range: range)
return matches.count == 1
}
}
private var listOfSLDs: [String : [String : Bool]] = {
@@ -49,3 +47,9 @@ private var listOfSLDs: [String : [String : Bool]] = {
}
return res
}()
extension NSString {
func substring(from: Int, to: Int) -> String {
substring(with: NSRange(location: from, length: to - from))
}
}

View File

@@ -43,14 +43,6 @@ extension UITableView {
func safeMoveRow(_ from: Int, to: Int) {
isFrontmost ? moveRow(at: IndexPath(row: from), to: IndexPath(row: to)) : reloadData()
}
/// Recalculate and apply new `tableHeaderView` height.
func sizeHeaderToFit() {
if let head = tableHeaderView {
head.frame.size.height = head.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
tableHeaderView = head
}
}
}
@@ -98,3 +90,34 @@ extension EditActionsRemove where Self: UITableViewController {
func editableRowActions(_: IndexPath) -> [(RowAction, String)] { [(.delete, "Remove")] }
func editableRowActionColor(_: IndexPath, _: RowAction) -> UIColor? { nil }
}
// MARK: - Table Cell Tap Menu
struct TableCellTapMenu {
private var index: Int = Int.max
mutating func reset() { index = Int.max }
/// Create a new tap manu and shows it immediatelly. With optional buttons.
mutating func start(_ tableView: UITableView, _ indexPath: IndexPath, items: [UIMenuItem]? = nil) -> Bool {
let menu = UIMenuController.shared
if index == indexPath.row {
menu.setMenuVisible(false, animated: true)
reset()
return false
}
index = indexPath.row
let cell = tableView.cellForRow(at: indexPath)!
menu.setTargetRect(cell.bounds, in: cell)
menu.menuItems = items
menu.setMenuVisible(true, animated: true)
return true
}
/// Returns the item if the array index is in bounds.
func getSelected<T>(_ source: [T]) -> T? {
guard index < source.count else { return nil }
return source[index]
}
}

Some files were not shown because too many files have changed in this diff Show More