Switch to gRPC client in Swift app

This commit is contained in:
Conrad Kramer 2024-07-13 18:08:43 -07:00
parent 25a0f7c421
commit 083ec73613
93 changed files with 1666 additions and 1327 deletions

View file

@ -1,5 +1,6 @@
#if os(macOS)
import AppKit
import BurrowUI
import SwiftUI
@main

View file

@ -1,11 +0,0 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -1,344 +0,0 @@
{
"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
}
}

View file

@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -1,20 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x50",
"green" : "0x37",
"red" : "0xEC"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "flag-standalone-wtransparent.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -1,20 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1A",
"green" : "0x17",
"red" : "0x88"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "WireGuard.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View file

@ -1,6 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4 KiB

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "WireGuardTitle.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -1,6 +1,7 @@
#if !os(macOS)
import BurrowUI
import SwiftUI
#if !os(macOS)
@MainActor
@main
struct BurrowApp: App {

View file

@ -1,72 +0,0 @@
import AuthenticationServices
import SwiftUI
#if !os(macOS)
struct BurrowView: View {
@Environment(\.webAuthenticationSession)
private var webAuthenticationSession
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()
}
}
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

View file

@ -1,50 +0,0 @@
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)
}
}

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23077.2" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23091" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23077.2"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23091"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">

View file

@ -1,64 +0,0 @@
//
// MenuItemToggleView.swift
// App
//
// Created by Thomas Stubblefield on 5/13/23.
//
import SwiftUI
struct MenuItemToggleView: View {
@Environment(\.tunnel)
var tunnel: Tunnel
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)
}
}
extension Tunnel {
fileprivate var toggleDisabled: Bool {
switch status {
case .disconnected, .permissionRequired, .connected, .disconnecting:
false
case .unknown, .disabled, .connecting, .reasserting, .invalid, .configurationReadWriteFailed:
true
}
}
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()
}
}
}
}

View file

@ -1,39 +0,0 @@
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

View file

@ -1,45 +0,0 @@
import NetworkExtension
extension NEVPNManager {
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 {
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>(_ 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)
}
}
}

View file

@ -1,167 +0,0 @@
import BurrowShared
import NetworkExtension
@Observable
class NetworkExtensionTunnel: Tunnel {
@MainActor private(set) var status: TunnelStatus = .unknown
private var error: NEVPNError?
private let logger = Logger.logger(for: Tunnel.self)
private let bundleIdentifier: String
private var tasks: [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.
private var managers: [NEVPNManager]? {
didSet { Task { await updateStatus() } }
}
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
}
convenience init() {
self.init(Constants.networkExtensionBundleIdentifier)
}
init(_ bundleIdentifier: String) {
self.bundleIdentifier = bundleIdentifier
let center = NotificationCenter.default
let configurationChanged = Task { [weak self] in
for try await _ in center.notifications(named: .NEVPNConfigurationChange).map({ _ in () }) {
await self?.update()
}
}
let statusChanged = Task { [weak self] in
for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) {
await self?.updateStatus()
}
}
tasks = [configurationChanged, statusChanged]
Task { await update() }
}
private func update() async {
do {
managers = try await NETunnelProviderManager.managers
await self.updateStatus()
} catch let vpnError as NEVPNError {
error = vpnError
} catch {
logger.error("Failed to update VPN configurations: \(error)")
}
}
private func updateStatus() async {
await MainActor.run {
status = currentStatus
}
}
func configure() async throws {
if managers == nil {
await update()
}
guard let managers = managers else { return }
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()
}
func start() {
guard let manager = managers?.first else { return }
Task {
do {
if !manager.isEnabled {
manager.isEnabled = true
try await manager.save()
}
try manager.connection.startVPNTunnel()
} catch {
logger.error("Failed to start: \(error)")
}
}
}
func stop() {
guard let manager = managers?.first else { return }
manager.connection.stopVPNTunnel()
}
func enable() {
Task {
do {
try await configure()
} catch {
logger.error("Failed to enable: \(error)")
}
}
}
deinit {
tasks.forEach { $0.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
}
}
}

View file

@ -1,38 +0,0 @@
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) }
}
}

View file

@ -1,23 +0,0 @@
import SwiftUI
struct HackClub: Network {
var id: String
var backgroundColor: Color { .init("HackClub") }
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)
}
}
}

View file

@ -1,10 +0,0 @@
import SwiftUI
protocol Network {
associatedtype Label: View
var id: String { get }
var backgroundColor: Color { get }
var label: Label { get }
}

View file

@ -1,30 +0,0 @@
import SwiftUI
struct WireGuard: Network {
var id: String
var backgroundColor: Color { .init("WireGuard") }
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)
}
}
}

View file

@ -1,284 +0,0 @@
import AuthenticationServices
import Foundation
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 var queue: [Int: CheckedContinuation<URL, Swift.Error>] = [:]
fileprivate static func handle(url: URL) {
let continuations = queue
queue.removeAll()
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 {
#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[id] = continuation
}
}
guard let url = try await group.next() else { throw OAuth2.Error.invalidCallbackURL }
group.cancelAll()
OAuth2.Session.queue[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"))
}
}
}
}

View file

@ -1,50 +0,0 @@
import SwiftUI
protocol Tunnel {
var status: TunnelStatus { get }
func start()
func stop()
func enable()
}
enum TunnelStatus: 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 let defaultValue: any Tunnel = NetworkExtensionTunnel()
}
extension EnvironmentValues {
var tunnel: any Tunnel {
get { self[TunnelKey.self] }
set { self[TunnelKey.self] = newValue }
}
}
#if DEBUG
@Observable
class PreviewTunnel: Tunnel {
var status: TunnelStatus = .permissionRequired
func start() {
status = .connected(.now)
}
func stop() {
status = .disconnected
}
func enable() {
status = .disconnected
}
}
#endif

View file

@ -1,73 +0,0 @@
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 {
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()
}
}
}

View file

@ -1,37 +0,0 @@
import SwiftUI
struct TunnelStatusView: View {
@Environment(\.tunnel)
var tunnel: any Tunnel
var body: some View {
Text(tunnel.status.description)
}
}
extension TunnelStatus: CustomStringConvertible {
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"
}
}
}