Switch to gRPC client in Swift app
11
Apple/UI/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/100.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/1024.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/114.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/120.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/128.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/144.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/152.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/16.png
Normal file
|
After Width: | Height: | Size: 684 B |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/167.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/172.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/180.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/196.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/20.png
Normal file
|
After Width: | Height: | Size: 927 B |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/216.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/256.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/29.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/32.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/40.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/48.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/50.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/512.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/55.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/57.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/58.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/60.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/64.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/72.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/76.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/80.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/87.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
Apple/UI/Assets.xcassets/AppIcon.appiconset/88.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
344
Apple/UI/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "57.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "1x",
|
||||
"size" : "57x57"
|
||||
},
|
||||
{
|
||||
"filename" : "114.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "57x57"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "20.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "50.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "50x50"
|
||||
},
|
||||
{
|
||||
"filename" : "100.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "50x50"
|
||||
},
|
||||
{
|
||||
"filename" : "72.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "72x72"
|
||||
},
|
||||
{
|
||||
"filename" : "144.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "72x72"
|
||||
},
|
||||
{
|
||||
"filename" : "76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "48.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "notificationCenter",
|
||||
"scale" : "2x",
|
||||
"size" : "24x24",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"filename" : "55.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "notificationCenter",
|
||||
"scale" : "2x",
|
||||
"size" : "27.5x27.5",
|
||||
"subtype" : "42mm"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "companionSettings",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "87.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "companionSettings",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "notificationCenter",
|
||||
"scale" : "2x",
|
||||
"size" : "33x33",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"filename" : "88.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "44x44",
|
||||
"subtype" : "40mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "46x46",
|
||||
"subtype" : "41mm"
|
||||
},
|
||||
{
|
||||
"filename" : "100.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "50x50",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "51x51",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "54x54",
|
||||
"subtype" : "49mm"
|
||||
},
|
||||
{
|
||||
"filename" : "172.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "86x86",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"filename" : "196.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "98x98",
|
||||
"subtype" : "42mm"
|
||||
},
|
||||
{
|
||||
"filename" : "216.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "108x108",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "117x117",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "129x129",
|
||||
"subtype" : "49mm"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "watch-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
6
Apple/UI/Assets.xcassets/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
20
Apple/UI/Assets.xcassets/HackClub.colorset/Contents.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x50",
|
||||
"green" : "0x37",
|
||||
"red" : "0xEC"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
12
Apple/UI/Assets.xcassets/HackClub.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "flag-standalone-wtransparent.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Apple/UI/Assets.xcassets/HackClub.imageset/flag-standalone-wtransparent.pdf
vendored
Normal file
20
Apple/UI/Assets.xcassets/WireGuard.colorset/Contents.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x1A",
|
||||
"green" : "0x17",
|
||||
"red" : "0x88"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
15
Apple/UI/Assets.xcassets/WireGuard.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "WireGuard.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
||||
6
Apple/UI/Assets.xcassets/WireGuard.imageset/WireGuard.svg
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="0 0 46 79" xmlns="http://www.w3.org/2000/svg">
|
||||
<g stroke-width=".265" transform="matrix(1, 0, 0, 1, 144.316635, -78.301682)">
|
||||
<path d="M -131.805 103.359 C -123.863 98.5 -113.717 101.469 -109.915 108.776 C -109.195 110.161 -109.103 112.293 -109.559 113.746 C -111.135 118.761 -114.855 121.574 -119.961 122.769 C -118.455 121.48 -117.257 120.019 -116.876 117.999 C -116.474 116.064 -116.911 114.05 -118.078 112.455 C -119.937 109.901 -123.263 108.888 -126.23 109.971 C -129.373 111.164 -131.095 114.033 -130.785 117.56 C -130.497 120.836 -128.011 122.959 -123.36 123.765 C -124.056 124.133 -124.591 124.404 -125.115 124.696 C -127.245 125.863 -129.099 127.475 -130.55 129.423 C -131.022 130.06 -131.347 130.112 -132.066 129.672 C -141.415 123.955 -142.016 109.605 -131.806 103.359 L -131.805 103.359 Z M -138.803 138.688 C -140.305 139.07 -141.761 139.634 -143.296 140.138 C -142.545 135.071 -136.612 130.404 -131.594 130.936 C -133.048 132.939 -133.896 135.317 -134.039 137.787 C -135.707 138.094 -137.278 138.301 -138.803 138.688 Z M -106.844 89.217 C -105.36 89.271 -103.873 89.248 -102.388 89.284 C -102.017 89.308 -101.649 89.359 -101.285 89.437 C -101.617 89.947 -101.992 90.428 -102.406 90.875 C -102.937 91.37 -103.537 91.853 -104.302 91.101 C -104.486 90.92 -104.921 90.962 -105.241 90.958 C -106.718 90.938 -108.197 90.891 -109.672 90.947 C -110.951 90.988 -112.227 91.118 -113.488 91.336 C -113.725 91.379 -114.078 92.165 -113.969 92.455 C -113.713 93.139 -113.339 93.893 -112.785 94.331 C -110.737 95.947 -108.559 97.399 -106.501 99.004 C -104.502 100.564 -102.641 102.274 -101.507 104.628 C -100.03 107.694 -99.987 110.91 -100.624 114.139 C -101.688 119.531 -104.416 123.998 -108.835 127.242 C -110.615 128.55 -112.819 129.292 -114.858 130.231 C -116.652 131.057 -118.498 131.769 -120.295 132.586 C -123.536 134.06 -125.357 137.577 -124.822 141.235 C -124.33 144.591 -121.386 147.392 -118.013 147.97 C -113.967 148.664 -109.792 146.034 -108.802 141.922 C -107.689 137.297 -110.202 133.168 -114.905 131.917 C -115.112 131.862 -115.32 131.81 -115.752 131.698 C -114.494 131.136 -113.407 130.735 -112.404 130.183 C -110.654 129.22 -108.936 128.201 -107.249 127.124 C -106.754 126.807 -106.486 126.807 -106.063 127.173 C -102.828 129.969 -100.899 133.448 -100.358 137.713 C -99.462 144.773 -102.804 151.259 -109.108 154.584 C -118.86 159.727 -130.794 153.873 -132.948 143.061 C -134.794 133.799 -128.257 125.399 -120.391 123.777 C -117.008 123.079 -113.914 121.671 -111.509 119.065 C -109.957 117.384 -109.205 115.942 -108.948 115.291 C -108.471 114.071 -108.227 112.772 -108.228 111.462 C -108.28 110.329 -108.546 109.216 -109.013 108.182 C -109.834 106.31 -112.98 103.332 -113.759 102.704 L -121.168 96.904 C -121.429 96.689 -121.723 96.705 -122.36 96.748 C -123.117 96.799 -125.053 96.906 -125.888 96.688 C -125.212 96.176 -123.371 95.432 -122.58 94.834 C -124.98 93.212 -127.721 93.798 -130.237 93.313 C -129.655 92.23 -126.776 90.564 -125.139 90.379 C -125.236 89.464 -125.385 88.556 -125.585 87.659 C -125.685 87.291 -126.096 86.934 -126.455 86.723 C -127.324 86.214 -128.246 85.793 -129.246 85.286 C -128.35 84.707 -127.313 84.386 -126.247 84.358 C -125.238 84.32 -124.228 84.418 -123.245 84.651 C -121.461 85.058 -120.037 84.792 -118.618 83.58 C -119.735 83.13 -120.852 82.719 -121.935 82.233 C -123.003 81.746 -124.043 81.202 -125.052 80.604 C -122.241 80.994 -119.523 82.048 -116.649 81.663 C -116.625 81.532 -116.6 81.402 -116.576 81.271 C -118.724 80.771 -120.873 80.271 -123.251 79.717 C -119.272 79.353 -115.567 79.293 -112.059 81.002 C -111.072 81.482 -110.039 81.88 -109.093 82.43 C -108.631 82.697 -108.321 83.225 -107.942 83.636 C -107.641 83.962 -107.4 84.399 -107.03 84.595 C -105.628 85.341 -104.084 85.37 -102.512 85.333 C -102.5 85.154 -102.489 84.986 -102.477 84.806 C -100.894 85.3 -99.113 87.125 -99.116 88.458 C -101.68 88.458 -104.242 88.449 -106.804 88.473 C -107.077 88.475 -107.349 88.675 -107.622 88.784 C -107.363 88.935 -107.108 89.207 -106.844 89.217 Z" style="fill: rgb(255, 255, 255);"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4 KiB |
21
Apple/UI/Assets.xcassets/WireGuardTitle.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "WireGuardTitle.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
3
Apple/UI/Assets.xcassets/WireGuardTitle.imageset/WireGuardTitle.svg
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="1538" height="210" viewBox="0 0 1538 210" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M65.7132 204.476L0.500244 4.49222H24.9555L74.5901 158.827L125.493 4.49222H147.774L198.496 158.467L248.311 4.49222H272.041L207.009 204.476H188.895L136.182 43.4409L84.0113 204.476H65.7132ZM267.577 204.476V49.4109H292.213V204.476H267.577ZM349.324 136.908V204.476H325.051V50.1407H431.568C447.387 50.1407 459.766 53.9449 468.703 61.5533C477.638 69.1618 482.107 79.7288 482.107 93.2544C482.288 103.759 478.504 113.946 471.51 121.785C464.445 129.818 455.237 134.739 443.885 136.548L486.817 204.478H460.551L416.713 136.91L349.324 136.908ZM349.324 114.265H431.565C439.898 114.265 446.268 112.453 450.676 108.83C455.083 105.207 457.287 100.014 457.288 93.2518C457.288 86.49 455.084 81.3275 450.676 77.7643C446.266 74.2033 439.896 72.422 431.565 72.4202H349.324V114.265ZM504.99 204.476V49.7808H650.085V72.4241H529.259V111.008H608.602V133.289H529.259V182.198H656.785V204.48H504.98L504.99 204.476ZM827.46 150.318V120.972H772.934V97.9663H852.277V158.107C842.614 174.773 830.025 187.543 814.508 196.419C798.989 205.296 781.387 209.734 761.704 209.734C731.752 209.734 707.026 199.861 687.524 180.116C668.02 160.371 658.269 135.343 658.269 105.031C658.269 74.5986 668.051 49.5097 687.615 29.7643C707.179 10.0196 731.876 0.147217 761.704 0.147217C780.181 0.147217 797.028 4.19276 812.244 12.2839C827.443 20.3602 840.25 32.2924 849.379 46.8828L828.547 61.7363C822.413 49.9333 812.97 40.1755 801.375 33.6582C789.281 26.8286 775.592 23.3281 761.704 23.5135C739.242 23.5135 720.644 31.2122 705.911 46.6097C691.175 62.0066 683.808 81.4796 683.81 105.029C683.81 128.578 691.177 148.021 705.911 163.358C720.643 178.696 739.241 186.365 761.704 186.363C775.711 186.363 788.18 183.344 799.111 177.306C810.039 171.269 819.488 162.274 827.46 150.318ZM879.358 50.1436H903.631V149.231C903.631 164.085 907.134 174.108 914.138 179.301C921.141 184.494 935.029 187.091 955.802 187.09C976.693 187.09 990.642 184.494 997.647 179.301C1004.65 174.11 1008.15 164.087 1008.15 149.231V50.1436H1032.25V155.57C1032.25 174.531 1026.24 188.238 1014.22 196.691C1002.2 205.142 982.61 209.369 955.439 209.372C928.386 209.372 908.943 205.205 897.11 196.872C885.273 188.539 879.355 174.772 879.357 155.57L879.358 50.1436ZM1028.45 204.476L1107.07 49.7808H1122.29L1201.63 204.476H1175.73L1155.62 165.167H1073.92L1053.99 204.476H1028.45ZM1084.43 144.698H1144.93L1114.86 85.4626L1084.43 144.698ZM1228.55 136.908V204.476H1204.27V50.1406H1310.79C1326.61 50.1406 1338.99 53.9448 1347.93 61.5532C1356.86 69.1617 1361.33 79.7287 1361.33 93.2543C1361.51 103.759 1357.73 113.945 1350.73 121.784C1343.67 129.817 1334.46 134.739 1323.11 136.548L1366.04 204.478H1339.77L1295.94 136.91L1228.55 136.908ZM1228.55 114.265H1310.79C1319.12 114.265 1325.49 112.453 1329.9 108.83C1334.31 105.207 1336.51 100.014 1336.51 93.2517C1336.51 86.4899 1334.31 81.3274 1329.9 77.7642C1325.49 74.2032 1319.12 72.4218 1310.79 72.4201H1228.55V114.265ZM1453.24 49.7818C1478.48 49.7818 1498.83 56.9973 1514.29 71.4281C1529.74 85.861 1537.47 104.61 1537.47 127.674C1537.47 150.983 1529.9 169.611 1514.74 183.559C1499.58 197.506 1479.08 204.48 1453.24 204.481H1384.59V49.786H1453.24L1453.24 49.7818ZM1453.6 72.0631H1408.86V182.2H1453.6C1471.96 182.2 1486.39 177.278 1496.9 167.436C1507.4 157.595 1512.66 144.221 1512.66 127.312C1512.66 111.009 1507.22 97.7246 1496.35 87.4596C1485.48 77.1966 1471.23 72.0651 1453.6 72.0631Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
75
Apple/UI/BurrowView.swift
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import AuthenticationServices
|
||||
import SwiftUI
|
||||
|
||||
#if !os(macOS)
|
||||
public struct BurrowView: View {
|
||||
@Environment(\.webAuthenticationSession)
|
||||
private var webAuthenticationSession
|
||||
|
||||
public var body: some View {
|
||||
NavigationStack {
|
||||
VStack {
|
||||
HStack {
|
||||
Text("Networks")
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
Spacer()
|
||||
Menu {
|
||||
Button("Hack Club", action: addHackClubNetwork)
|
||||
Button("WireGuard", action: addWireGuardNetwork)
|
||||
} label: {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.title)
|
||||
.accessibilityLabel("Add")
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
NetworkCarouselView()
|
||||
Spacer()
|
||||
TunnelStatusView()
|
||||
TunnelButton()
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding()
|
||||
.handleOAuth2Callback()
|
||||
}
|
||||
}
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
private func addHackClubNetwork() {
|
||||
Task {
|
||||
try await authenticateWithSlack()
|
||||
}
|
||||
}
|
||||
|
||||
private func addWireGuardNetwork() {
|
||||
}
|
||||
|
||||
private func authenticateWithSlack() async throws {
|
||||
guard
|
||||
let authorizationEndpoint = URL(string: "https://slack.com/openid/connect/authorize"),
|
||||
let tokenEndpoint = URL(string: "https://slack.com/api/openid.connect.token"),
|
||||
let redirectURI = URL(string: "https://burrow.rs/callback/oauth2") else { return }
|
||||
let session = OAuth2.Session(
|
||||
authorizationEndpoint: authorizationEndpoint,
|
||||
tokenEndpoint: tokenEndpoint,
|
||||
redirectURI: redirectURI,
|
||||
scopes: ["openid", "profile"],
|
||||
clientID: "2210535565.6884042183125",
|
||||
clientSecret: "2793c8a5255cae38830934c664eeb62d"
|
||||
)
|
||||
let response = try await session.authorize(webAuthenticationSession)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct NetworkView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BurrowView()
|
||||
.environment(\.tunnel, PreviewTunnel())
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
50
Apple/UI/FloatingButtonStyle.swift
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import SwiftUI
|
||||
|
||||
struct FloatingButtonStyle: ButtonStyle {
|
||||
static let duration = 0.08
|
||||
|
||||
var color: Color
|
||||
var cornerRadius: CGFloat
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.frame(minHeight: 48)
|
||||
.padding(.horizontal)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
configuration.isPressed ? color.opacity(0.9) : color.opacity(0.9),
|
||||
configuration.isPressed ? color.opacity(0.9) : color
|
||||
],
|
||||
startPoint: .init(x: 0.2, y: 0),
|
||||
endPoint: .init(x: 0.8, y: 1)
|
||||
)
|
||||
)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(configuration.isPressed ? .black : .white)
|
||||
)
|
||||
)
|
||||
.shadow(color: .black.opacity(configuration.isPressed ? 0.0 : 0.1), radius: 2.5, x: 0, y: 2)
|
||||
.scaleEffect(configuration.isPressed ? 0.975 : 1.0)
|
||||
.padding(.bottom, 2)
|
||||
.animation(
|
||||
configuration.isPressed ? .easeOut(duration: Self.duration) : .easeIn(duration: Self.duration),
|
||||
value: configuration.isPressed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension ButtonStyle where Self == FloatingButtonStyle {
|
||||
static var floating: FloatingButtonStyle {
|
||||
floating()
|
||||
}
|
||||
|
||||
static func floating(color: Color = .accentColor, cornerRadius: CGFloat = 10) -> FloatingButtonStyle {
|
||||
FloatingButtonStyle(color: color, cornerRadius: cornerRadius)
|
||||
}
|
||||
}
|
||||
67
Apple/UI/MenuItemToggleView.swift
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// MenuItemToggleView.swift
|
||||
// App
|
||||
//
|
||||
// Created by Thomas Stubblefield on 5/13/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct MenuItemToggleView: View {
|
||||
@Environment(\.tunnel)
|
||||
var tunnel: Tunnel
|
||||
|
||||
public var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Burrow")
|
||||
.font(.headline)
|
||||
Text(tunnel.status.description)
|
||||
.font(.subheadline)
|
||||
}
|
||||
Spacer()
|
||||
Toggle(isOn: tunnel.toggleIsOn) {
|
||||
}
|
||||
.disabled(tunnel.toggleDisabled)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.padding(.horizontal, 4)
|
||||
.padding(10)
|
||||
.frame(minWidth: 300, minHeight: 32, maxHeight: 32)
|
||||
}
|
||||
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
||||
extension Tunnel {
|
||||
@MainActor fileprivate var toggleDisabled: Bool {
|
||||
switch status {
|
||||
case .disconnected, .permissionRequired, .connected, .disconnecting:
|
||||
false
|
||||
case .unknown, .disabled, .connecting, .reasserting, .invalid, .configurationReadWriteFailed:
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor var toggleIsOn: Binding<Bool> {
|
||||
Binding {
|
||||
switch status {
|
||||
case .connecting, .reasserting, .connected:
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
} set: { newValue in
|
||||
switch (status, newValue) {
|
||||
case (.permissionRequired, true):
|
||||
enable()
|
||||
case (_, true):
|
||||
start()
|
||||
case (_, false):
|
||||
stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Apple/UI/NetworkCarouselView.swift
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import SwiftUI
|
||||
|
||||
struct NetworkCarouselView: View {
|
||||
var networks: [any Network] = [
|
||||
HackClub(id: 1),
|
||||
HackClub(id: 2),
|
||||
WireGuard(id: 4),
|
||||
HackClub(id: 5)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal) {
|
||||
LazyHStack {
|
||||
ForEach(networks, id: \.id) { network in
|
||||
NetworkView(network: network)
|
||||
.containerRelativeFrame(.horizontal, count: 10, span: 7, spacing: 0, alignment: .center)
|
||||
.scrollTransition(.interactive, axis: .horizontal) { content, phase in
|
||||
content
|
||||
.scaleEffect(1.0 - abs(phase.value) * 0.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollTargetLayout()
|
||||
.scrollClipDisabled()
|
||||
.scrollIndicators(.hidden)
|
||||
.defaultScrollAnchor(.center)
|
||||
.scrollTargetBehavior(.viewAligned)
|
||||
.containerRelativeFrame(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct NetworkCarouselView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NetworkCarouselView()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
45
Apple/UI/NetworkExtension+Async.swift
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import NetworkExtension
|
||||
|
||||
extension NEVPNManager: @unchecked @retroactive Sendable {
|
||||
func remove() async throws {
|
||||
_ = try await withUnsafeThrowingContinuation { continuation in
|
||||
removeFromPreferences(completionHandler: completion(continuation))
|
||||
}
|
||||
}
|
||||
|
||||
func save() async throws {
|
||||
_ = try await withUnsafeThrowingContinuation { continuation in
|
||||
saveToPreferences(completionHandler: completion(continuation))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NETunnelProviderManager: @unchecked @retroactive Sendable {
|
||||
class var managers: [NETunnelProviderManager] {
|
||||
get async throws {
|
||||
try await withUnsafeThrowingContinuation { continuation in
|
||||
loadAllFromPreferences(completionHandler: completion(continuation))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func completion(_ continuation: UnsafeContinuation<Void, Error>) -> (Error?) -> Void {
|
||||
return { error in
|
||||
if let error {
|
||||
continuation.resume(throwing: error)
|
||||
} else {
|
||||
continuation.resume(returning: ())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func completion<T: Sendable>(_ continuation: UnsafeContinuation<T, Error>) -> (T?, Error?) -> Void {
|
||||
return { value, error in
|
||||
if let error {
|
||||
continuation.resume(throwing: error)
|
||||
} else if let value {
|
||||
continuation.resume(returning: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
171
Apple/UI/NetworkExtensionTunnel.swift
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import BurrowCore
|
||||
import NetworkExtension
|
||||
|
||||
@Observable
|
||||
public final class NetworkExtensionTunnel: Tunnel {
|
||||
@MainActor public private(set) var status: TunnelStatus = .unknown
|
||||
@MainActor private var error: NEVPNError?
|
||||
|
||||
private let logger = Logger.logger(for: Tunnel.self)
|
||||
private let bundleIdentifier: String
|
||||
private let configurationChanged: Task<Void, Error>
|
||||
private let statusChanged: Task<Void, Error>
|
||||
|
||||
// Each manager corresponds to one entry in the Settings app.
|
||||
// Our goal is to maintain a single manager, so we create one if none exist and delete any extra.
|
||||
@MainActor private var managers: [NEVPNManager]? {
|
||||
didSet { Task { await updateStatus() } }
|
||||
}
|
||||
|
||||
@MainActor private var currentStatus: TunnelStatus {
|
||||
guard let managers = managers else {
|
||||
guard let error = error else {
|
||||
return .unknown
|
||||
}
|
||||
|
||||
switch error.code {
|
||||
case .configurationReadWriteFailed:
|
||||
return .configurationReadWriteFailed
|
||||
default:
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
guard let manager = managers.first else {
|
||||
return .permissionRequired
|
||||
}
|
||||
|
||||
guard manager.isEnabled else {
|
||||
return .disabled
|
||||
}
|
||||
|
||||
return manager.connection.tunnelStatus
|
||||
}
|
||||
|
||||
public init(bundleIdentifier: String) {
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
|
||||
let center = NotificationCenter.default
|
||||
let tunnel: OSAllocatedUnfairLock<NetworkExtensionTunnel?> = .init(initialState: .none)
|
||||
configurationChanged = Task {
|
||||
for try await _ in center.notifications(named: .NEVPNConfigurationChange) {
|
||||
try Task.checkCancellation()
|
||||
await tunnel.withLock { $0 }?.update()
|
||||
}
|
||||
}
|
||||
statusChanged = Task {
|
||||
for try await _ in center.notifications(named: .NEVPNStatusDidChange) {
|
||||
try Task.checkCancellation()
|
||||
await tunnel.withLock { $0 }?.updateStatus()
|
||||
}
|
||||
}
|
||||
tunnel.withLock { $0 = self }
|
||||
|
||||
Task { await update() }
|
||||
}
|
||||
|
||||
private func update() async {
|
||||
do {
|
||||
let result = try await NETunnelProviderManager.managers
|
||||
await MainActor.run {
|
||||
managers = result
|
||||
status = currentStatus
|
||||
}
|
||||
await self.updateStatus()
|
||||
} catch let vpnError as NEVPNError {
|
||||
await MainActor.run {
|
||||
error = vpnError
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to update VPN configurations: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStatus() async {
|
||||
await MainActor.run {
|
||||
status = currentStatus
|
||||
}
|
||||
}
|
||||
|
||||
func configure() async throws {
|
||||
let managers = try await NETunnelProviderManager.managers
|
||||
if managers.count > 1 {
|
||||
try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in
|
||||
for manager in managers.suffix(from: 1) {
|
||||
group.addTask { try await manager.remove() }
|
||||
}
|
||||
try await group.waitForAll()
|
||||
}
|
||||
}
|
||||
|
||||
guard managers.isEmpty else { return }
|
||||
|
||||
let manager = NETunnelProviderManager()
|
||||
manager.localizedDescription = "Burrow"
|
||||
|
||||
let proto = NETunnelProviderProtocol()
|
||||
proto.providerBundleIdentifier = bundleIdentifier
|
||||
proto.serverAddress = "hackclub.com"
|
||||
|
||||
manager.protocolConfiguration = proto
|
||||
try await manager.save()
|
||||
}
|
||||
|
||||
public func start() {
|
||||
Task {
|
||||
guard let manager = try await NETunnelProviderManager.managers.first else { return }
|
||||
do {
|
||||
if !manager.isEnabled {
|
||||
manager.isEnabled = true
|
||||
try await manager.save()
|
||||
}
|
||||
try manager.connection.startVPNTunnel()
|
||||
} catch {
|
||||
logger.error("Failed to start: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
Task {
|
||||
guard let manager = try await NETunnelProviderManager.managers.first else { return }
|
||||
manager.connection.stopVPNTunnel()
|
||||
}
|
||||
}
|
||||
|
||||
public func enable() {
|
||||
Task {
|
||||
do {
|
||||
try await configure()
|
||||
} catch {
|
||||
logger.error("Failed to enable: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
configurationChanged.cancel()
|
||||
statusChanged.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
extension NEVPNConnection {
|
||||
fileprivate var tunnelStatus: TunnelStatus {
|
||||
switch status {
|
||||
case .connected:
|
||||
.connected(connectedDate!)
|
||||
case .connecting:
|
||||
.connecting
|
||||
case .disconnecting:
|
||||
.disconnecting
|
||||
case .disconnected:
|
||||
.disconnected
|
||||
case .reasserting:
|
||||
.reasserting
|
||||
case .invalid:
|
||||
.invalid
|
||||
@unknown default:
|
||||
.unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Apple/UI/NetworkView.swift
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import SwiftUI
|
||||
|
||||
struct NetworkView<Content: View>: View {
|
||||
var color: Color
|
||||
var content: () -> Content
|
||||
|
||||
private var gradient: LinearGradient {
|
||||
LinearGradient(
|
||||
colors: [
|
||||
color.opacity(0.8),
|
||||
color
|
||||
],
|
||||
startPoint: .init(x: 0.2, y: 0),
|
||||
endPoint: .init(x: 0.8, y: 1)
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
content()
|
||||
.frame(maxWidth: .infinity, minHeight: 175, maxHeight: 175)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(gradient)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(.white)
|
||||
)
|
||||
)
|
||||
.shadow(color: .black.opacity(0.1), radius: 3.0, x: 0, y: 2)
|
||||
}
|
||||
}
|
||||
|
||||
extension NetworkView where Content == AnyView {
|
||||
init(network: any Network) {
|
||||
color = network.backgroundColor
|
||||
content = { AnyView(network.label) }
|
||||
}
|
||||
}
|
||||
27
Apple/UI/Networks/HackClub.swift
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import BurrowCore
|
||||
import SwiftUI
|
||||
|
||||
struct HackClub: Network {
|
||||
typealias NetworkType = Burrow_WireGuardNetwork
|
||||
static let type: Burrow_NetworkType = .hackClub
|
||||
|
||||
var id: Int32
|
||||
var backgroundColor: Color { .init("HackClub") }
|
||||
|
||||
@MainActor var label: some View {
|
||||
GeometryReader { reader in
|
||||
VStack(alignment: .leading) {
|
||||
Image("HackClub")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: reader.size.height / 4)
|
||||
Spacer()
|
||||
Text("@conradev")
|
||||
.foregroundStyle(.white)
|
||||
.font(.body.monospaced())
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Apple/UI/Networks/Network.swift
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import Atomics
|
||||
import BurrowCore
|
||||
import SwiftProtobuf
|
||||
import SwiftUI
|
||||
|
||||
protocol Network {
|
||||
associatedtype NetworkType: Message
|
||||
associatedtype Label: View
|
||||
|
||||
static var type: Burrow_NetworkType { get }
|
||||
|
||||
var id: Int32 { get }
|
||||
var backgroundColor: Color { get }
|
||||
|
||||
@MainActor var label: Label { get }
|
||||
}
|
||||
|
||||
@Observable
|
||||
@MainActor
|
||||
final class NetworkViewModel: Sendable {
|
||||
private(set) var networks: [Burrow_Network] = []
|
||||
|
||||
private var task: Task<Void, Error>!
|
||||
|
||||
init(socketURL: URL) {
|
||||
task = Task { [weak self] in
|
||||
let client = NetworksClient.unix(socketURL: socketURL)
|
||||
for try await networks in client.networkList(.init()) {
|
||||
guard let viewModel = self else { continue }
|
||||
Task { @MainActor in
|
||||
viewModel.networks = networks.network
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Apple/UI/Networks/WireGuard.swift
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import BurrowCore
|
||||
import SwiftUI
|
||||
|
||||
struct WireGuard: Network {
|
||||
typealias NetworkType = Burrow_WireGuardNetwork
|
||||
static let type: BurrowCore.Burrow_NetworkType = .wireGuard
|
||||
|
||||
var id: Int32
|
||||
var backgroundColor: Color { .init("WireGuard") }
|
||||
|
||||
@MainActor var label: some View {
|
||||
GeometryReader { reader in
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Image("WireGuard")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
Image("WireGuardTitle")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: reader.size.width / 2)
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: reader.size.height / 4)
|
||||
Spacer()
|
||||
Text("@conradev")
|
||||
.foregroundStyle(.white)
|
||||
.font(.body.monospaced())
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
293
Apple/UI/OAuth2.swift
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
import AuthenticationServices
|
||||
import Foundation
|
||||
import os
|
||||
import SwiftUI
|
||||
|
||||
enum OAuth2 {
|
||||
enum Error: Swift.Error {
|
||||
case unknown
|
||||
case invalidAuthorizationURL
|
||||
case invalidCallbackURL
|
||||
case invalidRedirectURI
|
||||
}
|
||||
|
||||
struct Credential {
|
||||
var accessToken: String
|
||||
var refreshToken: String?
|
||||
var expirationDate: Date?
|
||||
}
|
||||
|
||||
struct Session {
|
||||
var authorizationEndpoint: URL
|
||||
var tokenEndpoint: URL
|
||||
var redirectURI: URL
|
||||
var responseType = OAuth2.ResponseType.code
|
||||
var scopes: Set<String>
|
||||
var clientID: String
|
||||
var clientSecret: String
|
||||
|
||||
fileprivate static let queue: OSAllocatedUnfairLock<[Int: CheckedContinuation<URL, Swift.Error>]> = {
|
||||
.init(initialState: [:])
|
||||
}()
|
||||
|
||||
fileprivate static func handle(url: URL) {
|
||||
let continuations = queue.withLock { continuations in
|
||||
let copy = continuations
|
||||
continuations.removeAll()
|
||||
return copy
|
||||
}
|
||||
for (_, continuation) in continuations {
|
||||
continuation.resume(returning: url)
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
authorizationEndpoint: URL,
|
||||
tokenEndpoint: URL,
|
||||
redirectURI: URL,
|
||||
scopes: Set<String>,
|
||||
clientID: String,
|
||||
clientSecret: String
|
||||
) {
|
||||
self.authorizationEndpoint = authorizationEndpoint
|
||||
self.tokenEndpoint = tokenEndpoint
|
||||
self.redirectURI = redirectURI
|
||||
self.scopes = scopes
|
||||
self.clientID = clientID
|
||||
self.clientSecret = clientSecret
|
||||
}
|
||||
|
||||
private var authorizationURL: URL {
|
||||
get throws {
|
||||
var queryItems: [URLQueryItem] = [
|
||||
.init(name: "client_id", value: clientID),
|
||||
.init(name: "response_type", value: responseType.rawValue),
|
||||
.init(name: "redirect_uri", value: redirectURI.absoluteString)
|
||||
]
|
||||
if !scopes.isEmpty {
|
||||
queryItems.append(.init(name: "scope", value: scopes.joined(separator: ",")))
|
||||
}
|
||||
guard var components = URLComponents(url: authorizationEndpoint, resolvingAgainstBaseURL: false) else {
|
||||
throw OAuth2.Error.invalidAuthorizationURL
|
||||
}
|
||||
components.queryItems = queryItems
|
||||
guard let authorizationURL = components.url else { throw OAuth2.Error.invalidAuthorizationURL }
|
||||
return authorizationURL
|
||||
}
|
||||
}
|
||||
|
||||
private func handle(callbackURL: URL) async throws -> OAuth2.AccessTokenResponse {
|
||||
switch responseType {
|
||||
case .code:
|
||||
guard let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false) else {
|
||||
throw OAuth2.Error.invalidCallbackURL
|
||||
}
|
||||
return try await handle(response: try components.decode(OAuth2.CodeResponse.self))
|
||||
default:
|
||||
throw OAuth2.Error.invalidCallbackURL
|
||||
}
|
||||
}
|
||||
|
||||
private func handle(response: OAuth2.CodeResponse) async throws -> OAuth2.AccessTokenResponse {
|
||||
var components = URLComponents()
|
||||
components.queryItems = [
|
||||
.init(name: "client_id", value: clientID),
|
||||
.init(name: "client_secret", value: clientSecret),
|
||||
.init(name: "grant_type", value: GrantType.authorizationCode.rawValue),
|
||||
.init(name: "code", value: response.code),
|
||||
.init(name: "redirect_uri", value: redirectURI.absoluteString)
|
||||
]
|
||||
let httpBody = Data(components.percentEncodedQuery!.utf8)
|
||||
|
||||
var request = URLRequest(url: tokenEndpoint)
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = httpBody
|
||||
|
||||
let session = URLSession(configuration: .ephemeral)
|
||||
let (data, _) = try await session.data(for: request)
|
||||
return try OAuth2.decoder.decode(OAuth2.AccessTokenResponse.self, from: data)
|
||||
}
|
||||
|
||||
func authorize(_ session: WebAuthenticationSession) async throws -> Credential {
|
||||
let authorizationURL = try authorizationURL
|
||||
let callbackURL = try await session.start(
|
||||
url: authorizationURL,
|
||||
redirectURI: redirectURI
|
||||
)
|
||||
return try await handle(callbackURL: callbackURL).credential
|
||||
}
|
||||
}
|
||||
|
||||
private struct CodeResponse: Codable {
|
||||
var code: String
|
||||
var state: String?
|
||||
}
|
||||
|
||||
private struct AccessTokenResponse: Codable {
|
||||
var accessToken: String
|
||||
var tokenType: TokenType
|
||||
var expiresIn: Double?
|
||||
var refreshToken: String?
|
||||
|
||||
var credential: Credential {
|
||||
.init(
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
expirationDate: expiresIn.map { Date(timeIntervalSinceNow: $0) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum TokenType: Codable, RawRepresentable {
|
||||
case bearer
|
||||
case unknown(String)
|
||||
|
||||
init(rawValue: String) {
|
||||
self = switch rawValue.lowercased() {
|
||||
case "bearer": .bearer
|
||||
default: .unknown(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
var rawValue: String {
|
||||
switch self {
|
||||
case .bearer: "bearer"
|
||||
case .unknown(let type): type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum GrantType: Codable, RawRepresentable {
|
||||
case authorizationCode
|
||||
case unknown(String)
|
||||
|
||||
init(rawValue: String) {
|
||||
self = switch rawValue.lowercased() {
|
||||
case "authorization_code": .authorizationCode
|
||||
default: .unknown(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
var rawValue: String {
|
||||
switch self {
|
||||
case .authorizationCode: "authorization_code"
|
||||
case .unknown(let type): type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ResponseType: Codable, RawRepresentable {
|
||||
case code
|
||||
case idToken
|
||||
case unknown(String)
|
||||
|
||||
init(rawValue: String) {
|
||||
self = switch rawValue.lowercased() {
|
||||
case "code": .code
|
||||
case "id_token": .idToken
|
||||
default: .unknown(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
var rawValue: String {
|
||||
switch self {
|
||||
case .code: "code"
|
||||
case .idToken: "id_token"
|
||||
case .unknown(let type): type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate static var decoder: JSONDecoder {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
return decoder
|
||||
}
|
||||
|
||||
fileprivate static var encoder: JSONEncoder {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
return encoder
|
||||
}
|
||||
}
|
||||
|
||||
extension WebAuthenticationSession: @unchecked @retroactive Sendable {
|
||||
}
|
||||
|
||||
extension WebAuthenticationSession {
|
||||
#if canImport(BrowserEngineKit)
|
||||
@available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *)
|
||||
fileprivate static func callback(for redirectURI: URL) throws -> ASWebAuthenticationSession.Callback {
|
||||
switch redirectURI.scheme {
|
||||
case "https":
|
||||
guard let host = redirectURI.host else { throw OAuth2.Error.invalidRedirectURI }
|
||||
return .https(host: host, path: redirectURI.path)
|
||||
case "http":
|
||||
throw OAuth2.Error.invalidRedirectURI
|
||||
case .some(let scheme):
|
||||
return .customScheme(scheme)
|
||||
case .none:
|
||||
throw OAuth2.Error.invalidRedirectURI
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
fileprivate func start(url: URL, redirectURI: URL) async throws -> URL {
|
||||
#if canImport(BrowserEngineKit)
|
||||
if #available(iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, *) {
|
||||
return try await authenticate(
|
||||
using: url,
|
||||
callback: try Self.callback(for: redirectURI),
|
||||
additionalHeaderFields: [:]
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
return try await withThrowingTaskGroup(of: URL.self) { group in
|
||||
group.addTask {
|
||||
return try await authenticate(using: url, callbackURLScheme: redirectURI.scheme ?? "")
|
||||
}
|
||||
|
||||
let id = Int.random(in: 0..<Int.max)
|
||||
group.addTask {
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
OAuth2.Session.queue.withLock { $0[id] = continuation }
|
||||
}
|
||||
}
|
||||
guard let url = try await group.next() else { throw OAuth2.Error.invalidCallbackURL }
|
||||
group.cancelAll()
|
||||
OAuth2.Session.queue.withLock { $0[id] = nil }
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func handleOAuth2Callback() -> some View {
|
||||
onOpenURL { url in OAuth2.Session.handle(url: url) }
|
||||
}
|
||||
}
|
||||
|
||||
extension URLComponents {
|
||||
fileprivate func decode<T: Decodable>(_ type: T.Type) throws -> T {
|
||||
guard let queryItems else {
|
||||
throw DecodingError.valueNotFound(
|
||||
T.self,
|
||||
.init(codingPath: [], debugDescription: "Missing query items")
|
||||
)
|
||||
}
|
||||
let data = try OAuth2.encoder.encode(try queryItems.values)
|
||||
return try OAuth2.decoder.decode(T.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
extension Sequence where Element == URLQueryItem {
|
||||
fileprivate var values: [String: String?] {
|
||||
get throws {
|
||||
try Dictionary(map { ($0.name, $0.value) }) { _, _ in
|
||||
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Duplicate query items"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
Apple/UI/Tunnel.swift
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import BurrowConfiguration
|
||||
import NetworkExtension
|
||||
import SwiftUI
|
||||
|
||||
protocol Tunnel: Sendable {
|
||||
@MainActor var status: TunnelStatus { get }
|
||||
|
||||
func start()
|
||||
func stop()
|
||||
func enable()
|
||||
}
|
||||
|
||||
public enum TunnelStatus: Sendable, Equatable, Hashable {
|
||||
case unknown
|
||||
case permissionRequired
|
||||
case disabled
|
||||
case connecting
|
||||
case connected(Date)
|
||||
case disconnecting
|
||||
case disconnected
|
||||
case reasserting
|
||||
case invalid
|
||||
case configurationReadWriteFailed
|
||||
}
|
||||
|
||||
struct TunnelKey: EnvironmentKey {
|
||||
static var defaultValue: any Tunnel {
|
||||
NetworkExtensionTunnel(bundleIdentifier: Constants.networkExtensionBundleIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var tunnel: any Tunnel {
|
||||
get { self[TunnelKey.self] }
|
||||
set { self[TunnelKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@Observable
|
||||
@MainActor
|
||||
final class PreviewTunnel: Tunnel {
|
||||
private(set) var status: TunnelStatus = .permissionRequired
|
||||
|
||||
nonisolated func start() {
|
||||
set(.connected(.now))
|
||||
}
|
||||
|
||||
nonisolated func stop() {
|
||||
set(.disconnected)
|
||||
}
|
||||
|
||||
nonisolated func enable() {
|
||||
set(.disconnected)
|
||||
}
|
||||
|
||||
nonisolated private func set(_ status: TunnelStatus) {
|
||||
Task { @MainActor in self.status = status }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
73
Apple/UI/TunnelButton.swift
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import SwiftUI
|
||||
|
||||
struct TunnelButton: View {
|
||||
@Environment(\.tunnel)
|
||||
var tunnel: any Tunnel
|
||||
|
||||
private var action: Action? { tunnel.action }
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if let action {
|
||||
tunnel.perform(action)
|
||||
}
|
||||
} label: {
|
||||
Text(action.description)
|
||||
}
|
||||
.disabled(action.isDisabled)
|
||||
.padding(.horizontal)
|
||||
.buttonStyle(.floating)
|
||||
}
|
||||
}
|
||||
|
||||
extension Tunnel {
|
||||
@MainActor fileprivate var action: TunnelButton.Action? {
|
||||
switch status {
|
||||
case .permissionRequired, .invalid:
|
||||
.enable
|
||||
case .disabled, .disconnecting, .disconnected:
|
||||
.start
|
||||
case .connecting, .connected, .reasserting:
|
||||
.stop
|
||||
case .unknown, .configurationReadWriteFailed:
|
||||
nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelButton {
|
||||
fileprivate enum Action {
|
||||
case enable
|
||||
case start
|
||||
case stop
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelButton.Action? {
|
||||
var description: LocalizedStringKey {
|
||||
switch self {
|
||||
case .enable: "Enable"
|
||||
case .start: "Start"
|
||||
case .stop: "Stop"
|
||||
case .none: "Start"
|
||||
}
|
||||
}
|
||||
|
||||
var isDisabled: Bool {
|
||||
if case .none = self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Tunnel {
|
||||
fileprivate func perform(_ action: TunnelButton.Action) {
|
||||
switch action {
|
||||
case .enable: enable()
|
||||
case .start: start()
|
||||
case .stop: stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Apple/UI/TunnelStatusView.swift
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import SwiftUI
|
||||
|
||||
struct TunnelStatusView: View {
|
||||
@Environment(\.tunnel)
|
||||
var tunnel: any Tunnel
|
||||
|
||||
var body: some View {
|
||||
Text(tunnel.status.description)
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelStatus: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .unknown:
|
||||
"Unknown"
|
||||
case .permissionRequired:
|
||||
"Permission Required"
|
||||
case .disconnected:
|
||||
"Disconnected"
|
||||
case .disabled:
|
||||
"Disabled"
|
||||
case .connecting:
|
||||
"Connecting…"
|
||||
case .connected:
|
||||
"Connected"
|
||||
case .disconnecting:
|
||||
"Disconnecting…"
|
||||
case .reasserting:
|
||||
"Reasserting…"
|
||||
case .invalid:
|
||||
"Invalid"
|
||||
case .configurationReadWriteFailed:
|
||||
"System Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Apple/UI/UI.xcconfig
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#include "../Configuration/Framework.xcconfig"
|
||||
|
||||
ENABLE_PREVIEWS = YES
|
||||