Skip to content

Commit ccac303

Browse files
committed
disconnect connected client from web interface
1 parent 229a618 commit ccac303

File tree

4 files changed

+137
-9
lines changed

4 files changed

+137
-9
lines changed

ser2tcp/html/app.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,14 +229,27 @@ function renderPortCard(port, index) {
229229
div.appendChild(el('p', 'port: ' + ser.port, 'port-config-detail'));
230230
}
231231
const ul = el('ul');
232-
(port.servers || []).forEach(s => {
232+
(port.servers || []).forEach((s, si) => {
233233
const proto = (s.protocol || 'tcp').toUpperCase();
234234
const addr = proto === 'SOCKET' ? s.address : s.address + ':' + s.port;
235235
const li = el('li', proto + ' \u2014 ' + addr);
236236
const clients = s.connections || [];
237237
if (clients.length) {
238238
const cul = el('ul');
239-
clients.forEach(c => cul.appendChild(el('li', c.address)));
239+
clients.forEach((c, ci) => {
240+
const cli = el('li');
241+
cli.appendChild(document.createTextNode(c.address + ' '));
242+
const dcBtn = document.createElement('button');
243+
dcBtn.className = 'btn-disconnect';
244+
dcBtn.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14">'
245+
+ '<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12'
246+
+ ' 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"'
247+
+ ' fill="currentColor"/></svg>';
248+
dcBtn.title = 'Disconnect ' + c.address;
249+
dcBtn.onclick = () => disconnectClient(index, si, ci);
250+
cli.appendChild(dcBtn);
251+
cul.appendChild(cli);
252+
});
240253
li.appendChild(cul);
241254
} else {
242255
li.appendChild(el('em', ' no connections'));
@@ -817,6 +830,12 @@ function savePort(index) {
817830
});
818831
}
819832

833+
function disconnectClient(portIdx, srvIdx, conIdx) {
834+
api('DELETE', '/api/ports/' + portIdx + '/connections/' + srvIdx + '/' + conIdx)
835+
.then(() => loadPorts())
836+
.catch(e => { if (e !== 'unauthorized') alert(e); });
837+
}
838+
820839
function deletePort(index) {
821840
if (!confirm('Delete port ' + index + '?')) return;
822841
api('DELETE', '/api/ports/' + index).then(() => loadPorts()).catch(e => {

ser2tcp/html/style.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,8 @@ nav button:hover { color: #2a7edf; }
8282
.detected-section { margin-top: 2em; padding-top: 1em;
8383
border-top: 1px solid #ddd; }
8484
.detected-section h3 { color: #888; margin: 0 0 0.5em; }
85+
.btn-disconnect { background: none; border: none; color: #ccc; padding: 0 0.2em;
86+
cursor: pointer; vertical-align: middle; }
87+
.btn-disconnect:hover { color: #e55; }
8588
.detect-link { color: #4a9eff; text-decoration: none; cursor: pointer; }
8689
.detect-link:hover { text-decoration: underline; }

ser2tcp/http_server.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -266,18 +266,33 @@ def _get_ports_config(self):
266266
return ports
267267

268268
def _route_api_ports_item(self, client, user):
269-
"""Route /api/ports/<index> requests"""
269+
"""Route /api/ports/<index>/... requests"""
270+
rest = client.path[len('/api/ports/'):]
271+
parts = rest.split('/')
270272
try:
271-
index = int(client.path[len('/api/ports/'):])
273+
index = int(parts[0])
272274
except ValueError:
273275
self._error(client, 'Invalid port index', 400)
274276
return
275-
if client.method == 'PUT':
276-
self._handle_api_ports_update(client, user, index)
277-
elif client.method == 'DELETE':
278-
self._handle_api_ports_delete(client, user, index)
277+
if len(parts) == 1:
278+
if client.method == 'PUT':
279+
self._handle_api_ports_update(client, user, index)
280+
elif client.method == 'DELETE':
281+
self._handle_api_ports_delete(client, user, index)
282+
else:
283+
self._error(client, 'Method not allowed', 405)
284+
elif len(parts) == 4 and parts[1] == 'connections' \
285+
and client.method == 'DELETE':
286+
try:
287+
srv_idx = int(parts[2])
288+
con_idx = int(parts[3])
289+
except ValueError:
290+
self._error(client, 'Invalid index', 400)
291+
return
292+
self._handle_api_disconnect(client, user, index,
293+
srv_idx, con_idx)
279294
else:
280-
self._error(client, 'Method not allowed', 405)
295+
self._error(client, 'Not found', 404)
281296

282297
def _validate_port_config(self, data):
283298
"""Validate port configuration, return error string or None"""
@@ -398,6 +413,28 @@ def _handle_api_ports_delete(self, client, user, index):
398413
self._log.info("Port deleted: %d", index)
399414
client.respond({'ok': True})
400415

416+
def _handle_api_disconnect(self, client, user, port_idx,
417+
srv_idx, con_idx):
418+
"""Disconnect a specific client connection"""
419+
if not self._require_admin(client, user):
420+
return
421+
if port_idx < 0 or port_idx >= len(self._serial_proxies):
422+
self._error(client, 'Port not found', 404)
423+
return
424+
proxy = self._serial_proxies[port_idx]
425+
if srv_idx < 0 or srv_idx >= len(proxy.servers):
426+
self._error(client, 'Server not found', 404)
427+
return
428+
server = proxy.servers[srv_idx]
429+
if con_idx < 0 or con_idx >= len(server.connections):
430+
self._error(client, 'Connection not found', 404)
431+
return
432+
con = server.connections[con_idx]
433+
addr = con.address_str()
434+
server._remove_connection(con)
435+
self._log.info("Disconnected: %s", addr)
436+
client.respond({'ok': True})
437+
401438
def _handle_api_login(self, client):
402439
"""Authenticate user and return session token"""
403440
if not self._auth:

tests/test_http_server.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,75 @@ def test_no_ssl_config_for_tcp(self):
354354
self.assertNotIn('ssl', srv)
355355

356356

357+
class TestApiDisconnect(unittest.TestCase):
358+
def _make_connection(self, address='192.168.1.5:54321'):
359+
con = Mock()
360+
con.address_str.return_value = address
361+
return con
362+
363+
def _make_server_with_con(self):
364+
con = self._make_connection()
365+
server = Mock()
366+
server.protocol = 'TCP'
367+
server.config = {'address': '0.0.0.0', 'port': 21000}
368+
server.connections = [con]
369+
return server, con
370+
371+
def _make_proxy_with_con(self):
372+
server, con = self._make_server_with_con()
373+
proxy = Mock()
374+
proxy.serial_config = {'port': '/dev/ttyUSB0'}
375+
proxy.match = None
376+
proxy.name = ''
377+
proxy.is_connected = True
378+
proxy.servers = [server]
379+
return proxy, server, con
380+
381+
def test_disconnect_client(self):
382+
proxy, server, con = self._make_proxy_with_con()
383+
wrapper = make_wrapper(serial_proxies=[proxy])
384+
client = MockClient(
385+
method='DELETE',
386+
path='/api/ports/0/connections/0/0')
387+
wrapper._handle_request(client)
388+
self.assertEqual(client.respond_status, 200)
389+
server._remove_connection.assert_called_once_with(con)
390+
391+
def test_disconnect_port_not_found(self):
392+
wrapper = make_wrapper(serial_proxies=[])
393+
client = MockClient(
394+
method='DELETE',
395+
path='/api/ports/0/connections/0/0')
396+
wrapper._handle_request(client)
397+
self.assertEqual(client.respond_status, 404)
398+
399+
def test_disconnect_server_not_found(self):
400+
proxy, _, _ = self._make_proxy_with_con()
401+
wrapper = make_wrapper(serial_proxies=[proxy])
402+
client = MockClient(
403+
method='DELETE',
404+
path='/api/ports/0/connections/5/0')
405+
wrapper._handle_request(client)
406+
self.assertEqual(client.respond_status, 404)
407+
408+
def test_disconnect_connection_not_found(self):
409+
proxy, _, _ = self._make_proxy_with_con()
410+
wrapper = make_wrapper(serial_proxies=[proxy])
411+
client = MockClient(
412+
method='DELETE',
413+
path='/api/ports/0/connections/0/5')
414+
wrapper._handle_request(client)
415+
self.assertEqual(client.respond_status, 404)
416+
417+
def test_disconnect_invalid_index(self):
418+
wrapper = make_wrapper(serial_proxies=[])
419+
client = MockClient(
420+
method='DELETE',
421+
path='/api/ports/0/connections/abc/0')
422+
wrapper._handle_request(client)
423+
self.assertEqual(client.respond_status, 400)
424+
425+
357426
class TestApiDetect(unittest.TestCase):
358427
def _make_port_info(self, device='/dev/ttyUSB0', vid=None, pid=None,
359428
serial_number=None, manufacturer=None, product=None,

0 commit comments

Comments
 (0)