mirror of
https://github.com/aljazceru/plugins.git
synced 2025-12-20 06:34:20 +01:00
backup: Add support for IPv6 addresses in socket backend
Support the bracketed `socket:[::]:1234` syntax for IPv6 addresses. Also, add factor out the socket URI parsing to a function and add tests for it. As a forward compatibility measure, reject query strings (`?a=b`) because I intend to use these for parameters such as SOCKS5 proxy (for Tor) in a future patch.
This commit is contained in:
committed by
Christian Decker
parent
ca75d7e132
commit
cdfcd5a2fe
@@ -15,7 +15,8 @@ The remote backup system consists of two parts:
|
|||||||
- A server daemon that receives changes from the backup backend and communicates with a local backup backend
|
- A server daemon that receives changes from the backup backend and communicates with a local backup backend
|
||||||
to store them. The server side does not need to be running c-lightning, nor have it installed.
|
to store them. The server side does not need to be running c-lightning, nor have it installed.
|
||||||
|
|
||||||
The backend URL format is `socket:<host>:<port>`. For example `socket:127.0.0.1:1234`.
|
The backend URL format is `socket:<host>:<port>`. For example `socket:127.0.0.1:1234`. To supply a IPv6
|
||||||
|
address use the bracketed syntax `socket:[::1]:1234`.
|
||||||
|
|
||||||
To run the server against a local backend use `backup-cli server file://.../ 127.0.0.1:1234`.
|
To run the server against a local backend use `backup-cli server file://.../ 127.0.0.1:1234`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,71 @@
|
|||||||
import json
|
from collections import namedtuple
|
||||||
import logging, socket, struct
|
import json, logging, socket, re, struct
|
||||||
from typing import Tuple, Iterator
|
from typing import Tuple, Iterator
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|
||||||
from backend import Backend, Change
|
from backend import Backend, Change
|
||||||
from protocol import PacketType, recvall, PKT_CHANGE_TYPES, change_from_packet, packet_from_change, send_packet, recv_packet
|
from protocol import PacketType, recvall, PKT_CHANGE_TYPES, change_from_packet, packet_from_change, send_packet, recv_packet
|
||||||
|
|
||||||
|
SocketURLInfo = namedtuple('SocketURLInfo', ['host', 'port', 'addrtype'])
|
||||||
|
|
||||||
|
class AddrType:
|
||||||
|
IPv4 = 0
|
||||||
|
IPv6 = 1
|
||||||
|
NAME = 2
|
||||||
|
|
||||||
|
def parse_socket_url(destination: str) -> SocketURLInfo:
|
||||||
|
'''Parse a socket: URL to extract the information contained in it.'''
|
||||||
|
url = urlparse(destination)
|
||||||
|
if url.scheme != 'socket':
|
||||||
|
raise ValueError('Scheme for socket backend must be socket:...')
|
||||||
|
|
||||||
|
if url.path.startswith('['): # bracketed IPv6 address
|
||||||
|
eidx = url.path.find(']')
|
||||||
|
if eidx == -1:
|
||||||
|
raise ValueError('Unterminated bracketed host address.')
|
||||||
|
host = url.path[1:eidx]
|
||||||
|
addrtype = AddrType.IPv6
|
||||||
|
eidx += 1
|
||||||
|
if eidx >= len(url.path) or url.path[eidx] != ':':
|
||||||
|
raise ValueError('Port number missing.')
|
||||||
|
eidx += 1
|
||||||
|
else:
|
||||||
|
eidx = url.path.find(':')
|
||||||
|
if eidx == -1:
|
||||||
|
raise ValueError('Port number missing.')
|
||||||
|
host = url.path[0:eidx]
|
||||||
|
if re.match('\d+\.\d+\.\d+\.\d+$', host): # matches IPv4 address format
|
||||||
|
addrtype = AddrType.IPv4
|
||||||
|
else:
|
||||||
|
addrtype = AddrType.NAME
|
||||||
|
eidx += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
port = int(url.path[eidx:])
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError('Invalid port number')
|
||||||
|
|
||||||
|
# parse query parameters
|
||||||
|
# reject unknown parameters (currently all of them)
|
||||||
|
qs = parse_qs(url.query)
|
||||||
|
if len(qs):
|
||||||
|
raise ValueError('Invalid query string')
|
||||||
|
|
||||||
|
return SocketURLInfo(host=host, port=port, addrtype=addrtype)
|
||||||
|
|
||||||
class SocketBackend(Backend):
|
class SocketBackend(Backend):
|
||||||
def __init__(self, destination: str, create: bool):
|
def __init__(self, destination: str, create: bool):
|
||||||
self.version = None
|
self.version = None
|
||||||
self.prev_version = None
|
self.prev_version = None
|
||||||
self.destination = destination
|
self.destination = destination
|
||||||
self.url = urlparse(self.destination)
|
self.url = parse_socket_url(destination)
|
||||||
|
if self.url.addrtype == AddrType.IPv6:
|
||||||
|
self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||||
|
else: # TODO NAME is assumed to be IPv4 for now
|
||||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
(host, port) = self.url.path.split(':')
|
logging.info('Initialized socket backend, connecting to {}:{} (addrtype {})...'.format(
|
||||||
logging.info('Initialized socket backend')
|
self.url.host, self.url.port, self.url.addrtype))
|
||||||
self.sock.connect((host, int(port)))
|
self.sock.connect((self.url.host, self.url.port))
|
||||||
logging.info('Connected to {}'.format(destination))
|
logging.info('Connected to {}'.format(destination))
|
||||||
|
|
||||||
def _send_packet(self, typ: int, payload: bytes) -> None:
|
def _send_packet(self, typ: int, payload: bytes) -> None:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from backend import Backend
|
from backend import Backend
|
||||||
from filebackend import FileBackend
|
from filebackend import FileBackend
|
||||||
|
import socketbackend
|
||||||
from flaky import flaky
|
from flaky import flaky
|
||||||
from pyln.testing.fixtures import * # noqa: F401,F403
|
from pyln.testing.fixtures import * # noqa: F401,F403
|
||||||
from pyln.testing.utils import sync_blockheight
|
from pyln.testing.utils import sync_blockheight
|
||||||
@@ -279,3 +280,38 @@ def test_compact(bitcoind, directory, node_factory):
|
|||||||
l1.rpc.backup_compact()
|
l1.rpc.backup_compact()
|
||||||
tmp = tempfile.TemporaryDirectory()
|
tmp = tempfile.TemporaryDirectory()
|
||||||
subprocess.check_call([cli_path, "restore", bdest, tmp.name])
|
subprocess.check_call([cli_path, "restore", bdest, tmp.name])
|
||||||
|
|
||||||
|
def test_parse_socket_url():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
# fail: invalid url scheme
|
||||||
|
socketbackend.parse_socket_url('none')
|
||||||
|
# fail: no port number
|
||||||
|
socketbackend.parse_socket_url('socket:127.0.0.1')
|
||||||
|
socketbackend.parse_socket_url('socket:127.0.0.1:')
|
||||||
|
# fail: unbracketed IPv6
|
||||||
|
socketbackend.parse_socket_url('socket:::1:1234')
|
||||||
|
# fail: no port number IPv6
|
||||||
|
socketbackend.parse_socket_url('socket:[::1]')
|
||||||
|
socketbackend.parse_socket_url('socket:[::1]:')
|
||||||
|
# fail: invalid port number
|
||||||
|
socketbackend.parse_socket_url('socket:127.0.0.1:12bla')
|
||||||
|
# fail: unrecognized query string key
|
||||||
|
socketbackend.parse_socket_url('socket:127.0.0.1:1234?dummy=value')
|
||||||
|
|
||||||
|
# IPv4
|
||||||
|
s = socketbackend.parse_socket_url('socket:127.0.0.1:1234')
|
||||||
|
assert(s.host == '127.0.0.1')
|
||||||
|
assert(s.port == 1234)
|
||||||
|
assert(s.addrtype == socketbackend.AddrType.IPv4)
|
||||||
|
|
||||||
|
# IPv6
|
||||||
|
s = socketbackend.parse_socket_url('socket:[::1]:1235')
|
||||||
|
assert(s.host == '::1')
|
||||||
|
assert(s.port == 1235)
|
||||||
|
assert(s.addrtype == socketbackend.AddrType.IPv6)
|
||||||
|
|
||||||
|
# Hostname
|
||||||
|
s = socketbackend.parse_socket_url('socket:backup.local:1236')
|
||||||
|
assert(s.host == 'backup.local')
|
||||||
|
assert(s.port == 1236)
|
||||||
|
assert(s.addrtype == socketbackend.AddrType.NAME)
|
||||||
|
|||||||
Reference in New Issue
Block a user