15 Commits

10 changed files with 139 additions and 23 deletions

View File

@ -1,3 +1,6 @@
**0.19.1**
- Added ability to authenticate receivers with listing sites using "receiver id" tokens
**0.19.0**
- Fix direwolf connection setup by implementing a retry loop
- Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector

View File

@ -59,6 +59,19 @@ Antenna: Receiver Antenna<br />
Website: <a href="http://localhost" target="_blank">http://localhost</a>
"""
# ==== Public receiver listings ====
# You can publish your receiver on online receiver directories, like https://www.receiverbook.de
# You will receive a receiver key from the directory that will authenticate you as the operator of this receiver.
# Please note that you not share your receiver keys publicly since anyone that obtains your receiver key can take over
# your public listing.
# Your receiver keys should be placed into this array:
receiver_keys = []
# If you list your receiver on multiple sites, you can place all your keys into the array above, or you can append
# keys to the arraylike this:
# receiver_keys += ["my-receiver-key"]
# If you're not sure, simply copy & paste the code you received from your listing site below this line:
# ==== DSP/RX settings ====
fft_fps = 9
fft_size = 4096 # Should be power of 2

7
debian/changelog vendored
View File

@ -1,3 +1,10 @@
openwebrx (0.19.1) buster focal; urgency=low
* Added ability to authenticate receivers with listing sites using
"receiver id" tokens
-- Jakob Ketterl <jakob.ketterl@gmx.de> Sat, 13 Jun 2020 16:46:00 +0000
openwebrx (0.19.0) buster focal; urgency=low
* Fix direwolf connection setup by implementing a retry loop
* Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector

View File

@ -138,7 +138,6 @@ class OpenWebRxReceiverClient(OpenWebRxClient):
def handleTextMessage(self, conn, message):
try:
message = json.loads(message)
logger.debug(message)
if "type" in message:
if message["type"] == "dspcontrol":
if "action" in message and message["action"] == "start":

View File

@ -7,14 +7,18 @@ class Controller(object):
self.request = request
self.options = options
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None):
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None):
self.handler.send_response(code)
if headers is None:
headers = {}
if content_type is not None:
self.handler.send_header("Content-Type", content_type)
headers["Content-Type"] = content_type
if last_modified is not None:
self.handler.send_header("Last-Modified", last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT"))
headers["Last-Modified"] = last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT")
if max_age is not None:
self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age))
headers["Cache-Control"] = "max-age: {0}".format(max_age)
for key, value in headers.items():
self.handler.send_header(key, value)
self.handler.end_headers()
if type(content) == str:
content = content.encode()

View File

@ -1,11 +1,13 @@
from . import Controller
from owrx.client import ClientRegistry
from owrx.version import openwebrx_version
from owrx.sdr import SdrService
from owrx.config import Config
import os
from owrx.receiverid import ReceiverId, KeyException
import json
import pkg_resources
import logging
logger = logging.getLogger(__name__)
class StatusController(Controller):
@ -27,7 +29,12 @@ class StatusController(Controller):
def indexAction(self):
pm = Config.get()
headers = None
if "Authorization" in self.request.headers:
try:
headers = ReceiverId.getResponseHeader(self.request.headers["Authorization"])
except KeyException:
logger.exception("error processing authorization header")
status = {
"receiver": {
"name": pm["receiver_name"],
@ -40,4 +47,4 @@ class StatusController(Controller):
"version": openwebrx_version,
"sdrs": [self.getReceiverStats(r) for r in SdrService.getSources().values()]
}
self.send_response(json.dumps(status), content_type="application/json")
self.send_response(json.dumps(status), content_type="application/json", headers=headers)

View File

@ -35,19 +35,26 @@ class RequestHandler(BaseHTTPRequestHandler):
logger.debug("%s - - [%s] %s", self.address_string(), self.log_date_time_string(), format % args)
def do_GET(self):
self.router.route(self, "GET")
self.router.route(self, self.get_request("GET"))
def do_POST(self):
self.router.route(self, "POST")
self.router.route(self, self.get_request("POST"))
def get_request(self, method):
url = urlparse(self.path)
return Request(url, method, self.headers)
class Request(object):
def __init__(self, url, method, cookies):
def __init__(self, url, method, headers):
self.path = url.path
self.query = parse_qs(url.query)
self.matches = None
self.method = method
self.cookies = cookies
self.headers = headers
self.cookies = SimpleCookie()
if "Cookie" in headers:
self.cookies.load(headers["Cookie"])
def setMatches(self, matches):
self.matches = matches
@ -114,12 +121,7 @@ class Router(object):
if r.matches(request):
return r
def route(self, handler, method):
url = urlparse(handler.path)
cookies = SimpleCookie()
if "Cookie" in handler.headers:
cookies.load(handler.headers["Cookie"])
request = Request(url, method, cookies)
def route(self, handler, request):
route = self.find_route(request)
if route is not None:
controller = route.controller

81
owrx/receiverid.py Normal file
View File

@ -0,0 +1,81 @@
import re
import logging
import hashlib
import hmac
from datetime import datetime
from owrx.config import Config
logger = logging.getLogger(__name__)
keyRegex = re.compile("^([a-zA-Z]+)-([0-9a-f]{32})-([0-9a-f]{64})$")
keyChallengeRegex = re.compile("^([a-zA-Z]+)-([0-9a-f]{32})-([0-9a-f]{32})$")
headerRegex = re.compile("^ReceiverId (.*)$")
class KeyException(Exception):
pass
class Key(object):
def __init__(self, keyString):
matches = keyRegex.match(keyString)
if not matches:
raise KeyException("invalid key format")
self.source = matches.group(1)
self.id = matches.group(2)
self.secret = matches.group(3)
class KeyChallenge(object):
def __init__(self, challengeString):
matches = keyChallengeRegex.match(challengeString)
if not matches:
raise KeyException("invalid key challenge format")
self.source = matches.group(1)
self.id = matches.group(2)
self.challenge = matches.group(3)
class KeyResponse(object):
def __str__(self):
return "TODO"
class ReceiverId(object):
@staticmethod
def getResponseHeader(requestHeader):
matches = headerRegex.match(requestHeader)
if not matches:
raise KeyException("invalid authorization header")
challenge = KeyChallenge(matches.group(1))
key = ReceiverId.findKey(challenge)
if key is None:
return {}
time, signature = ReceiverId.signChallenge(challenge, key)
return {
"Signature": signature,
"Time": time,
}
@staticmethod
def findKey(challenge):
def parseKey(keyString):
try:
return Key(keyString)
except KeyException as e:
logger.error(e)
keys = [parseKey(keyString) for keyString in Config.get()['receiver_keys']]
keys = [key for key in keys if key is not None]
matching_keys = [key for key in keys if key.source == challenge.source and key.id == challenge.id]
if matching_keys:
return matching_keys[0]
return None
@staticmethod
def signChallenge(challenge, key):
now = datetime.utcnow().isoformat()
m = hmac.new(bytes.fromhex(key.secret), digestmod=hashlib.sha256)
m.update(bytes.fromhex(challenge.challenge))
m.update(now.encode('utf8'))
return now, m.hexdigest()

View File

@ -17,10 +17,10 @@ class FifiSdrSource(DirectSource):
return super().getEventNames() + ["device"]
def getFormatConversion(self):
return ["csdr convert_s16_f", "csdr gain_ff 30"]
return ["csdr convert_s16_f", "csdr gain_ff 5"]
def sendRockProgFrequency(self, frequency):
process = Popen(["rockprog", "--vco", "-w", "--", "freq={}".format(frequency / 1E6)])
process = Popen(["rockprog", "--vco", "-w", "--freq={}".format(frequency / 1E6)])
process.communicate()
rc = process.wait()
if rc != 0:

View File

@ -1,5 +1,5 @@
from distutils.version import StrictVersion
_versionstring = "0.19.0"
_versionstring = "0.19.1"
strictversion = StrictVersion(_versionstring)
openwebrx_version = "v{0}".format(strictversion)