Skip to content

Commit b4d8df7

Browse files
committed
Initial app implementation
1 parent 88a6997 commit b4d8df7

File tree

115 files changed

+9684
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

115 files changed

+9684
-1
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ bin/ghost
22
bin/ghost.*.linux*
33
bin/ghost.py*
44
bin/overlordd
5-
bin/ovl.py.bin
5+
bin/*.pybin*
66
bin/webroot
77
build
88
webroot/apps
99
webroot/index.html
1010
node_modules
1111
.vite
1212
dist
13+
UserInterfaceState.xcuserstate
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import SwiftUI
2+
3+
struct ContentView: View {
4+
@EnvironmentObject private var authViewModel: AuthViewModel
5+
6+
var body: some View {
7+
if authViewModel.isAuthenticated {
8+
DashboardView()
9+
} else {
10+
LoginView()
11+
}
12+
}
13+
}
14+
15+
struct ContentView_Previews: PreviewProvider {
16+
static var previews: some View {
17+
ContentView()
18+
.environmentObject(AuthViewModel())
19+
}
20+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Foundation
2+
3+
struct Camera: Identifiable {
4+
let id: String
5+
let clientId: String
6+
var isMinimized: Bool = false
7+
var position: CGPoint = CGPoint(x: 100, y: 100)
8+
var size: CGSize = CGSize(width: 400, height: 300)
9+
10+
init(id: String = UUID().uuidString, clientId: String) {
11+
self.id = id
12+
self.clientId = clientId
13+
}
14+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import Foundation
2+
3+
struct Client: Identifiable, Codable {
4+
var id: String { mid }
5+
let mid: String
6+
let name: String?
7+
var properties: [String: String]?
8+
var lastSeen: Date
9+
var hasCamera: Bool
10+
11+
enum CodingKeys: String, CodingKey {
12+
case mid, name, properties
13+
}
14+
15+
init(from decoder: Decoder) throws {
16+
let container = try decoder.container(keyedBy: CodingKeys.self)
17+
mid = try container.decode(String.self, forKey: .mid)
18+
name = try container.decodeIfPresent(String.self, forKey: .name)
19+
properties = try container.decodeIfPresent([String: String].self, forKey: .properties)
20+
lastSeen = Date()
21+
22+
// Determine if client has camera based on properties
23+
hasCamera = properties?["has_camera"] == "true"
24+
}
25+
26+
init(mid: String, name: String? = nil, properties: [String: String]? = nil) {
27+
self.mid = mid
28+
self.name = name
29+
self.properties = properties
30+
self.lastSeen = Date()
31+
self.hasCamera = properties?["has_camera"] == "true"
32+
}
33+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Foundation
2+
3+
struct Terminal: Identifiable {
4+
let id: String
5+
let clientId: String
6+
var title: String
7+
var output: String = ""
8+
var isMinimized: Bool = false
9+
var position: CGPoint = CGPoint(x: 100, y: 100)
10+
var size: CGSize = CGSize(width: 600, height: 400)
11+
12+
init(id: String = UUID().uuidString, clientId: String, title: String) {
13+
self.id = id
14+
self.clientId = clientId
15+
self.title = title
16+
}
17+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Foundation
2+
3+
struct UploadProgress: Identifiable {
4+
let id: String
5+
let filename: String
6+
let clientId: String
7+
var progress: Double
8+
var status: UploadStatus
9+
var startTime: Date
10+
11+
enum UploadStatus: String {
12+
case uploading
13+
case completed
14+
case failed
15+
}
16+
17+
init(id: String = UUID().uuidString, filename: String, clientId: String) {
18+
self.id = id
19+
self.filename = filename
20+
self.clientId = clientId
21+
self.progress = 0.0
22+
self.status = .uploading
23+
self.startTime = Date()
24+
}
25+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import SwiftUI
2+
3+
@main
4+
struct OverlordApp: App {
5+
@StateObject private var authViewModel = AuthViewModel()
6+
7+
var body: some Scene {
8+
WindowGroup {
9+
ContentView()
10+
.environmentObject(authViewModel)
11+
}
12+
}
13+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Foundation
2+
import Combine
3+
4+
class APIService {
5+
static let baseURL = "http://your-server-address/api" // Replace with your actual server address
6+
7+
private let session: URLSession
8+
private var cancellables = Set<AnyCancellable>()
9+
10+
init(session: URLSession = .shared) {
11+
self.session = session
12+
}
13+
14+
func getClients(token: String) -> AnyPublisher<[Client], Error> {
15+
let url = URL(string: "\(APIService.baseURL)/agents/list")!
16+
var request = URLRequest(url: url)
17+
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
18+
19+
return session.dataTaskPublisher(for: request)
20+
.map { $0.data }
21+
.decode(type: [Client].self, decoder: JSONDecoder())
22+
.eraseToAnyPublisher()
23+
}
24+
25+
func getClientProperties(mid: String, token: String) -> AnyPublisher<[String: String], Error> {
26+
let url = URL(string: "\(APIService.baseURL)/agent/properties/\(mid)")!
27+
var request = URLRequest(url: url)
28+
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
29+
30+
return session.dataTaskPublisher(for: request)
31+
.map { $0.data }
32+
.decode(type: [String: String].self, decoder: JSONDecoder())
33+
.eraseToAnyPublisher()
34+
}
35+
36+
func downloadFile(sid: String, token: String) {
37+
let url = URL(string: "\(APIService.baseURL)/file/download/\(sid)?token=\(token)")!
38+
UIApplication.shared.open(url)
39+
}
40+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import Foundation
2+
import Combine
3+
4+
class WebSocketService: ObservableObject {
5+
private var webSocket: URLSessionWebSocketTask?
6+
private var session: URLSession
7+
private var isStarted = false
8+
private var reconnectTimer: Timer?
9+
private var reconnectAttempts = 0
10+
private let maxReconnectAttempts = 5
11+
12+
@Published var isConnected = false
13+
14+
private var eventHandlers: [String: [(String) -> Void]] = [:]
15+
16+
init(session: URLSession = .shared) {
17+
self.session = session
18+
}
19+
20+
func start(token: String) {
21+
guard !isStarted else { return }
22+
23+
isStarted = true
24+
reconnectAttempts = 0
25+
connect(token: token)
26+
}
27+
28+
func stop() {
29+
isStarted = false
30+
31+
reconnectTimer?.invalidate()
32+
reconnectTimer = nil
33+
34+
webSocket?.cancel(with: .normalClosure, reason: nil)
35+
webSocket = nil
36+
37+
isConnected = false
38+
}
39+
40+
private func connect(token: String) {
41+
guard isStarted, webSocket == nil else { return }
42+
43+
let urlString = "\(APIService.baseURL.replacingOccurrences(of: "http", with: "ws"))/monitor?token=\(token)"
44+
guard let url = URL(string: urlString) else {
45+
print("Invalid WebSocket URL")
46+
return
47+
}
48+
49+
webSocket = session.webSocketTask(with: url)
50+
webSocket?.resume()
51+
52+
receiveMessage()
53+
54+
isConnected = true
55+
}
56+
57+
private func receiveMessage() {
58+
webSocket?.receive { [weak self] result in
59+
guard let self = self else { return }
60+
61+
switch result {
62+
case .success(let message):
63+
switch message {
64+
case .string(let text):
65+
self.handleMessage(text)
66+
case .data(let data):
67+
if let text = String(data: data, encoding: .utf8) {
68+
self.handleMessage(text)
69+
}
70+
@unknown default:
71+
break
72+
}
73+
74+
// Continue receiving messages
75+
self.receiveMessage()
76+
77+
case .failure(let error):
78+
print("WebSocket receive error: \(error)")
79+
self.handleDisconnect()
80+
}
81+
}
82+
}
83+
84+
private func handleMessage(_ text: String) {
85+
guard let data = text.data(using: .utf8),
86+
let message = try? JSONDecoder().decode(WebSocketMessage.self, from: data) else {
87+
return
88+
}
89+
90+
let handlers = eventHandlers[message.event] ?? []
91+
let messageData = message.data.first ?? ""
92+
93+
DispatchQueue.main.async {
94+
handlers.forEach { handler in
95+
handler(messageData)
96+
}
97+
}
98+
}
99+
100+
private func handleDisconnect() {
101+
isConnected = false
102+
webSocket = nil
103+
104+
guard isStarted else { return }
105+
106+
reconnectAttempts += 1
107+
108+
if reconnectAttempts >= maxReconnectAttempts {
109+
// Too many failed attempts, stop trying
110+
stop()
111+
112+
// Notify that authentication might have failed
113+
NotificationCenter.default.post(name: .webSocketAuthenticationFailed, object: nil)
114+
return
115+
}
116+
117+
// Try to reconnect after a delay
118+
reconnectTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { [weak self] _ in
119+
guard let self = self, let token = UserDefaults.standard.string(forKey: "authToken") else { return }
120+
self.connect(token: token)
121+
}
122+
}
123+
124+
func on(event: String, handler: @escaping (String) -> Void) {
125+
if eventHandlers[event] == nil {
126+
eventHandlers[event] = []
127+
}
128+
129+
eventHandlers[event]?.append(handler)
130+
}
131+
132+
func off(event: String) {
133+
eventHandlers[event] = nil
134+
}
135+
}
136+
137+
struct WebSocketMessage: Codable {
138+
let event: String
139+
let data: [String]
140+
}
141+
142+
extension Notification.Name {
143+
static let webSocketAuthenticationFailed = Notification.Name("webSocketAuthenticationFailed")
144+
}

0 commit comments

Comments
 (0)