1 module beangle.task.server;
2 
3 import vibe.core.core;
4 import vibe.core.log;
5 import vibe.http.fileserver : serveStaticFiles;
6 import vibe.http.router : URLRouter;
7 import vibe.http.server;
8 import vibe.http.websockets : WebSocket, handleWebSockets;
9 
10 import core.time;
11 import std.conv : to;
12 import std.algorithm;
13 import std.string;
14 import std.array;
15 import beangle.util : removeFromArray;
16 import beangle.task.channel;
17 import beangle.task.config;
18 import beangle.xml;
19 
20 //name -> channel
21 private AgentChannel[string] agents;
22 //id -> channel
23 private AdminChannel[string] admins;
24 private ServerSetting setting;
25 
26 int startServer(string xml, string[] args){
27   setting = ServerSetting.parse(xml);
28   auto router = new URLRouter(setting.path);
29   router.get("/ws", handleWebSockets(&handleWebSocketConnection));
30   router.get("/call/:agent", &callAgent);
31 
32   auto hs = new HTTPServerSettings();
33   hs.port = setting.port;
34   hs.bindAddresses = setting.ips;
35 
36   auto listener = listenHTTP(hs, router);
37   return runApplication(&args);
38 }
39 
40 /**
41  * Call agent directly
42  */
43 void callAgent(HTTPServerRequest req, HTTPServerResponse res){
44   string agentName = req.params.get("agent");
45   string commands = req.query.get("commands", "").strip();
46   if (commands.length==0){
47     res.writeBody("commands needed,using ?commands=some commands in query string", 400);
48   } else {
49     import std.stdio;
50     auto client = req.peer();
51     if ( client.startsWith("127.0.0.1") || client.startsWith("[0:0:0:0:0:0:0:1]")){
52       auto caller = new WebCaller();
53       if (agentName in agents){
54         res.writeBody(caller.invoke(agents[agentName], commands));
55       }else {
56         res.writeBody("Cannot find agent named " ~ agentName);
57       }
58     }else {
59       res.writeBody("service for localhost only.", 500);
60     }
61   }
62 }
63 
64 bool verify(string name, string secret){
65   if (name == "admin"){
66     return secret == setting.secret;
67   }else {
68     if (name in setting.agentSecrets){
69       return secret == setting.agentSecrets.get(name, "changeit");
70     }else {
71       return false;
72     }
73   }
74 }
75 
76 /**
77   Handle Websocket messages,do message broking and login.
78  */
79 void handleWebSocketConnection(scope WebSocket socket){
80   socket.waitForData(1.seconds);
81   string loginText = socket.receiveText;
82   // Server-side validation of results
83   Channel channel = null;
84   bool isAdmin = false;
85   if (loginText !is null && loginText.startsWith("/login ")) {
86     string[] credentials = loginText["/login ".length .. $].split(" ");
87     string name = credentials[0].strip();
88     string password = (credentials.length==1)? name : credentials[1].strip();
89     if (!verify(name, password)){
90       socket.send("invalid secret");
91       return;
92     }else {
93       if (name == "admin"){
94         auto newc = AdminChannel.create(name, socket);
95         admins[newc.id] = newc;
96         channel = newc;
97         isAdmin = true;
98       }else {
99         auto newc = AgentChannel.create(name, socket);
100         if (name in agents){
101           close(agents[name]);
102         }
103         agents[name] = newc;
104         channel = newc;
105       }
106       socket.send(name ~ " is connected.");
107       auto r = cast(HTTPServerRequest)socket.request;
108       logInfo("%s connected @ %s.", name, r.peer());
109     }
110   }else {
111     socket.send("Invalid name,using /login username password");
112     socket.close();
113     return;
114   }
115   // message loop
116   while (socket.waitForData) {
117     if (!socket.connected) break;
118     auto text = socket.receiveText.strip();
119     if (text == "/close") {
120       break;
121     }
122     if (isAdmin){
123       if (text == "/sessions"){
124         auto s = agents.values.map!(x=> x.toString()).join(",");
125         channel.send(s);
126       }else if (text.startsWith("/connect")){
127         if (text.length > "/connect ".length){
128           auto agentName = text["/connect ".length .. $];
129           auto result = agents.get(agentName, null);
130           if (result !is null && result.socket.connected){
131             AdminChannel admin = cast(AdminChannel)channel;
132             admin.setAgent(result);
133             channel.send(result.id);
134           }else {
135             channel.send(agentName ~" is offline");
136           }
137         }else {
138           channel.send("using /connect user_name");
139         }
140       } else {
141         auto admin = cast(AdminChannel)channel;
142         if (text.startsWith("@")){
143           auto spaceIdx = text.indexOf(' ');
144           if (spaceIdx == -1){
145             admin.send("using @agentname commands ...");
146           }else {
147             auto agentName = text[0..spaceIdx].strip();
148             auto agent = agents[agentName];
149             if (agent !is null && agent.socket.connected){
150               agent.invoke(SocketCaller.create(admin), text[spaceIdx..$].strip());
151             }else {
152               channel.send("offline");
153             }
154           }
155         }else {
156           if (admin.agent !is null){
157             if(text == "cd"){
158               admin.changeWorkdir("~");
159               admin.send("set workdir " ~ admin.workdir);
160             }else if (text.startsWith("cd ") && text.indexOf(";") == -1){
161               admin.changeWorkdir(text[3..$].strip());
162               admin.send("set workdir " ~ admin.workdir);
163             }else {
164               auto prefix = "cd " ~ admin.workdir ~ ";";
165               admin.agent.invoke(SocketCaller.create(admin), prefix ~ text);
166             }
167           }else {
168             admin.send("using @agentname commands ... or connect a exist agent using /connect");
169           }
170         }
171       }
172     }else {
173         (cast(AgentChannel)channel).reply(text);
174     }
175   }
176   close(channel);
177 }
178 
179 /** Close channel and socket.
180  */
181 void close(Channel channel){
182   agents.remove(channel.name);
183   admins.remove(channel.id);
184   if (channel.socket.connected){
185     channel.send("disconneted.");
186     channel.socket.close();
187   }
188   logInfo("%s disconnected", channel.name);
189 }
190