http-relay-2.1.3/0000755000175000017500000000000014500271115013524 5ustar jriverojriverohttp-relay-2.1.3/src/0000755000175000017500000000000014500271115014313 5ustar jriverojriverohttp-relay-2.1.3/src/http_relay/0000755000175000017500000000000014500271115016466 5ustar jriverojriverohttp-relay-2.1.3/src/http_relay/__init__.py0000644000175000017500000000024514500271102020574 0ustar jriverojrivero# SPDX-License-Identifier: BSD-3-Clause # SPDX-FileCopyrightText: Czech Technical University in Prague from .relay import HttpRelay __all__ = [HttpRelay.__name__] http-relay-2.1.3/src/http_relay/__main__.py0000644000175000017500000000722114500271102020556 0ustar jriverojrivero# SPDX-License-Identifier: BSD-3-Clause # SPDX-FileCopyrightText: Czech Technical University in Prague from __future__ import print_function import argparse import logging import sys import threading from http_relay import HttpRelay def get_parser(): parser = argparse.ArgumentParser( description="Relay HTTP requests from localhost to a remote host (act as reverse HTTP proxy).") parser.add_argument("host", type=str, help='The remote host to connect to (e.g. "cvut.cz")') parser.add_argument("port", type=int, default=80, nargs='?', help='The remote host\'s port to connect to (e.g. 80).') parser.add_argument("local_port", type=int, default=0, nargs='?', help='The local port to be used for the relay. If left out, will be the same as remote port.') parser.add_argument("local_addr", type=str, default='0.0.0.0', nargs='?', help='Local interface (IP or hostname) to run the relay on. By default, it runs on all IPv4 ' 'interfaces (0.0.0.0).') parser.add_argument("-n", "--num-threads", type=int, default=8, help='Number of threads servicing the incoming requests.') parser.add_argument("-b", "--buffer-size", type=int, default=1, help='Size of the buffer used for reading responses. Generally, a larger buffer should be more ' 'efficient, but if it is too large, the local clients may time out before they ' 'receive any data.') parser.add_argument("-t", "--sigkill-timeout", type=int, required=False, help='If specified, the relay will be sigkilled in this number of seconds.') parser.add_argument("-s", "--sigkill-on-stream-stop", action="store_true", help='If True, --sigkill-timeout will not be counted when no requests are active, and ' 'during requests, each successful data transmission will reset the timeout. This can be ' 'used to detect stale streams if you expect an application to be constantly receiving ' 'data.') return parser def main(cli_args=None): parser = get_parser() args = parser.parse_args(cli_args) host = args.host.lstrip("[").rstrip("]") # strip [] from IPv6 addresses port = args.port local_port = args.local_port if args.local_port != 0 else port local_addr = args.local_addr.lstrip("[").rstrip("]") # strip [] from IPv6 addresses num_threads = args.num_threads buffer_size = args.buffer_size sigkill_timeout = args.sigkill_timeout sigkill_on_stream_stop = args.sigkill_on_stream_stop logging.info("Relaying HTTP requests from %s:%i to %s:%i using %i threads" % ( local_addr if ":" not in local_addr else ("[" + local_addr + "]"), local_port, # wrap IPv6 in [] host if ":" not in host else ("[" + host + "]"), port, # wrap IPv6 in [] num_threads)) relay = HttpRelay(local_addr, local_port, host, port, buffer_size) if sigkill_timeout is not None: logging.info("HTTP relay has sigkill timeout set to %i seconds. After that time%s, the node will be killed." % ( sigkill_timeout, " with a stale stream" if sigkill_on_stream_stop else "")) t = threading.Thread(target=relay.sigkill_after, args=(sigkill_timeout, sigkill_on_stream_stop)) t.daemon = True t.start() try: relay.run(num_threads) except Exception as e: print(str(e), file=sys.stderr) sys.exit(1) if __name__ == '__main__': logging.basicConfig(level=logging.INFO) main() http-relay-2.1.3/src/http_relay/relay.py0000644000175000017500000003405014500271102020152 0ustar jriverojrivero# SPDX-License-Identifier: BSD-3-Clause # SPDX-FileCopyrightText: Czech Technical University in Prague """ Relay HTTP get requests from localhost to a remote host (act as reverse HTTP proxy). """ import errno import logging import os import signal import socket import sys import threading import time from functools import partial try: from http.server import HTTPServer, BaseHTTPRequestHandler from http.client import * except ImportError: from SimpleHTTPServer import HTTPServer, BaseHTTPRequestHandler from httplib import HTTPConnection __all__ = ['HttpRelay'] # Some servers (e.g. NTRIP, Shoutcast...) do respond with nonstandard status lines like "ICY 200 OK" instead of # the standard "HTTP/1.0 200 OK". The Python 3 client raises an exception when it finds these status lines. # Python 2 client works fine, but overrides the status line to HTTP/1.0. What we want instead is to tell the rest # of the codebase to look at such response as HTTP/1.0 response, but save the original status line so that it can # be relayed exactly as it was received. # Code borrowed and modified from Python 3 source code available under the Python Software Foundation License. class NonstandardHttpResponse(HTTPResponse): def _read_status(self): _MAXLINE = 65536 if sys.version_info.major > 2: line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1") else: line = self.fp.readline(_MAXLINE + 1) if len(line) > _MAXLINE: raise LineTooLong("status line") if self.debuglevel > 0: print("reply:", repr(line)) if not line: # Presumably, the server closed the connection before # sending a valid response. if sys.version_info.major > 2: raise RemoteDisconnected("Remote end closed connection without" " response") else: raise BadStatusLine("No status line received - the server " "has closed the connection") self._status_line = line try: version, status, reason = line.split(None, 2) except ValueError: try: version, status = line.split(None, 1) reason = "" except ValueError: # empty version will cause next test to fail. version = "" if not version.startswith("HTTP/"): # Here is the difference to the original method: we treat invalid version as HTTP 1.0 version = "HTTP/1.0" # The status code is a three-digit number try: status = int(status) if status < 100 or status > 999: raise BadStatusLine(line) except ValueError: raise BadStatusLine(line) return version, status, reason class HTTP10Connection(HTTPConnection): _http_vsn_str = "HTTP/1.0" _http_vsn = 10 response_class = NonstandardHttpResponse class HTTP11Connection(HTTPConnection): _http_vsn_str = "HTTP/1.1" _http_vsn = 11 response_class = NonstandardHttpResponse def is_server_running(server): try: return server.running except: return not server._BaseServer__shutdown_request class Handler(BaseHTTPRequestHandler): """ The main logic of the relay - forward the HTTP request to the remote server with changed Host: header and pass back whatever it returns. """ def __init__(self, relay, request, client_address, server): self._req_num = relay.request_num relay.request_num += 1 self.relay = relay self.host = relay.host self.port = relay.port self.relay_port = relay.relay_port self.read_buffer_size = relay.read_buffer_size try: BaseHTTPRequestHandler.__init__(self, request, client_address, server) except socket.error as e: self.log_socket_error(e) # Do not log requests using the BaseHTTPRequestHandler logging mechanism, we have our own. def log_request(self, code='-', size='-'): pass def log_error(self, format, *args): """ Log an error message. :param str format: Format string. :param List[Any] args: % parameters of the format string. """ if not self.relay.shutting_down and is_server_running(self.server): logging.error(("Request [%i] error: " + format) % ((self._req_num,) + args)) def log_socket_error(self, e): """ Log an error raised by socket operation. :param socket.error e: The error. """ # Ignore EPIPE and ECONNRESET as that is generated when the other end stops being interested in our data if isinstance(e, tuple) and e[0] in (errno.EPIPE, errno.ECONNRESET): logging.info("Response [%i]: finished" % (self._req_num,)) elif ("Errno %i" % (errno.EPIPE,)) in str(e) or ("Errno %i" % (errno.ECONNRESET,)) in str(e): logging.info("Response [%i]: finished" % (self._req_num,)) else: if not self.relay.shutting_down and is_server_running(self.server): self.log_error("%s", str(e)) def log_message(self, format, *args): """ Log an info message. :param str format: Format string. :param List[Any] args: % parameters of the format string. """ if is_server_running(self.server): logging.info(("Request [%i]: " + format) % ((self._req_num,) + args)) def log_response(self, format, *args): """ Log an info message related to the response. :param str format: Format string. :param List[Any] args: % parameters of the format string. """ if is_server_running(self.server): logging.info(("Response [%i]: " + format) % ((self._req_num,) + args)) def send_status_line(self, response): self.log_request(response.status) if self.request_version != 'HTTP/0.9': if sys.version_info[0] > 2: # self.send_response_only(code, message) if not hasattr(self, '_headers_buffer'): self._headers_buffer = [] self._headers_buffer.append(response._status_line.encode('latin-1', 'strict')) else: self.wfile.write(response._status_line) def do_GET(self): """ Do the relaying work. """ with self.relay.lock: self.relay.num_open_requests += 1 try: # Choose the right HTTP version connection_class = HTTP11Connection if self.protocol_version == "HTTP/1.1" else HTTP10Connection conn = connection_class(self.host, self.port) # Forward the request with the same headers headers = dict(zip(self.headers.keys(), self.headers.values())) # Replace host in Host header orig_host = None host = self.host if ":" not in self.host else ("[" + self.host + "]") host_port = host for header in headers: if header.lower() == "host": orig_host = headers[header] # append port if it is non-default or if it differs from the relay port if self.port != 80 and (self.port != self.relay_port or ":" in orig_host): # : is also valid in IPv6 addresses; a port is specified in an IPv6 only if the last : is after # the last ] if not (":" in orig_host and "]" in orig_host) or (orig_host.rfind(':') > orig_host.rfind(']')): host_port += ":" + str(self.port) headers[header] = host_port break self.log_message("GET http://%s%s", host_port, self.path) conn.request("GET", self.path, headers=headers) # Obtain the response resp = conn.getresponse() self.send_status_line(resp) self.log_response("%i %s", resp.status, resp.reason) # Forward back the response headers for header, value in resp.getheaders(): # Replace host in Location header if orig_host is not None and header.lower() == "location": value = value.replace(host, orig_host, 1) self.send_header(header, value) self.end_headers() # Forward back the response body num_bytes = 0 while True: chunk = resp.read(self.read_buffer_size) if not chunk: self.log_response("finished") break self.wfile.write(chunk) num_bytes += self.read_buffer_size self.relay.total_bytes += self.read_buffer_size if num_bytes > 10 * self.read_buffer_size: logging.debug("Response body [%i]: Sent %i bytes." % (self._req_num, num_bytes)) except socket.error as e: self.log_socket_error(e) except KeyboardInterrupt: pass #except Exception as e: # self.log_error("%s", str(e)) finally: with self.relay.lock: self.relay.num_open_requests -= 1 class Thread(threading.Thread): """ The HTTP server servicing thread. """ def __init__(self, server, relay): """ Create and run the servicing thread. :param HTTPServer server: The server to work with. :param HttpRelay relay: The relay. """ threading.Thread.__init__(self) self.server = server self.relay = relay self.daemon = True self.start() def run(self): """ Process the server requests. """ try: self.server.serve_forever() except Exception as e: if not self.relay.shutting_down and is_server_running(self.server): logging.error("Error in processing thread: " + str(e)) class HttpRelay(object): """ Relay HTTP get requests from localhost to a remote host (act as reverse HTTP proxy). """ def __init__(self, relay_addr, relay_port, remote_host, remote_port, buffer_size): """ Run the multithreaded relay server. :param str relay_addr: IP address or hostname specifying the local interface(s) to run the relay on (pass 0.0.0.0 or :: to run it on all interfaces (IPv4 or IPv6)). :param int relay_port: The local port. :param str remote_host: The remote host name. :param int remote_port: The remote host port. :param int buffer_size: Size of the buffer used for reading responses. If too large, the forwarding can be too slow. """ self.server_address = (relay_addr.lstrip("[").rstrip("]"), relay_port) self.host = remote_host.lstrip("[").rstrip("]") self.port = remote_port self.relay_port = relay_port self.read_buffer_size = buffer_size self.request_num = 0 self.total_bytes = 0 self.num_open_requests = 0 self.shutting_down = False self.lock = threading.Lock() # Create a standalone socket shared by all servers socket_type = socket.AF_INET if ":" not in relay_addr else socket.AF_INET6 self.socket = socket.socket(socket_type, socket.SOCK_STREAM) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) def run(self, num_threads=1): """ Run the multithreaded relay server. :param int num_threads: Number of servicing threads. """ try: self.socket.bind(self.server_address) self.socket.listen(5) # Create the servers and run their servicing threads servers = [] threads = [] for i in range(num_threads): handler = partial(Handler, self) httpd = HTTPServer(self.server_address, handler, False) httpd.socket = self.socket httpd.server_bind = httpd.server_close = lambda self: None servers.append(httpd) threads.append(Thread(httpd, self)) # Wait for node exit try: while not self.shutting_down: time.sleep(0.1) except KeyboardInterrupt: pass logging.info("Stopping HTTP relay.") self.shutting_down = True # First, shut down the socket, which should convince server.shutdown() to finish. self.socket.shutdown(socket.SHUT_RDWR) self.socket.close() # Shut down the servers (their service threads are daemons, so we don't need to join them) for server in servers: if server is not None: server.shutdown() except socket.gaierror as e: logging.error(str(e)) self.shutting_down = True raise except socket.error as e: logging.error(str(e)) self.shutting_down = True raise except: self.shutting_down = True raise def shutdown(self): self.shutting_down = True def sigkill_after(self, timeout, check_streaming=False): remaining = timeout last_total_bytes = -1 while not self.shutting_down: if not check_streaming or (self.num_open_requests > 0 and self.total_bytes == last_total_bytes): remaining -= 1 if remaining <= 0: logging.error("Stopping HTTP relay!") self.shutting_down = True time.sleep(0.01) os.kill(os.getpid(), signal.SIGKILL) return else: remaining = timeout if check_streaming and remaining == timeout // 2: logging.warning( "Relayed HTTP stream stopped. The relay will be stopped in %i sec if the stream does not " "resume." % (timeout // 2,)) last_total_bytes = self.total_bytes time.sleep(1) http-relay-2.1.3/src/http_relay.egg-info/0000755000175000017500000000000014500271115020160 5ustar jriverojriverohttp-relay-2.1.3/src/http_relay.egg-info/SOURCES.txt0000644000175000017500000000056014500271115022045 0ustar jriverojriveroLICENSE README.md pyproject.toml setup.py src/http_relay/__init__.py src/http_relay/__main__.py src/http_relay/relay.py src/http_relay.egg-info/PKG-INFO src/http_relay.egg-info/SOURCES.txt src/http_relay.egg-info/dependency_links.txt src/http_relay.egg-info/entry_points.txt src/http_relay.egg-info/requires.txt src/http_relay.egg-info/top_level.txt test/test_relay.pyhttp-relay-2.1.3/src/http_relay.egg-info/PKG-INFO0000644000175000017500000001305214500271115021256 0ustar jriverojriveroMetadata-Version: 2.1 Name: http-relay Version: 2.1.3 Summary: Relay for HTTP messages (reverse proxy) Home-page: https://github.com/ctu-vras/http_relay Author: Martin Pecka Author-email: Martin Pecka Maintainer: Martin Pecka Maintainer-email: Martin Pecka License: BSD 3-Clause License Copyright (c) 2023, Czech Technical University in Prague Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Project-URL: Homepage, https://github.com/ctu-vras/http_relay Keywords: http,proxy Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: Science/Research Classifier: Operating System :: OS Independent Classifier: Topic :: Communications Classifier: Topic :: Internet Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: System :: Networking Classifier: Topic :: Utilities Requires-Python: >=2.7 Description-Content-Type: text/markdown License-File: LICENSE Provides-Extra: test Requires-Dist: pytest; extra == "test" # http-relay Relay HTTP requests from localhost to a remote host (act as reverse HTTP proxy). This HTTP relay properly processes also the nonstandard HTTP responses like `ICY 200 OK` produced by Shoutcast or NTRIP streaming servers. The relay works properly with hostnames, IPv4 and IPv6 addresses. IPv6 addresses can be specified with or without `[]`. ## Usage ``` usage: http-relay [-h] [-n NUM_THREADS] [-b BUFFER_SIZE] [-t SIGKILL_TIMEOUT] [-s] host [port] [local_port] [local_addr] positional arguments: host The remote host to connect to (e.g. "cvut.cz") port The remote host's port to connect to (e.g. 80). local_port The local port to be used for the relay. If left out, will be the same as remote port. local_addr Local interface (IP or hostname) to run the relay on. By default, it runs on all IPv4 interfaces (0.0.0.0). optional arguments: -h, --help show this help message and exit -n NUM_THREADS, --num-threads NUM_THREADS Number of threads servicing the incoming requests. -b BUFFER_SIZE, --buffer-size BUFFER_SIZE Size of the buffer used for reading responses. Generally, a larger buffer should be more efficient, but if it is too large, the local clients may time out before they receive any data. -t SIGKILL_TIMEOUT, --sigkill-timeout SIGKILL_TIMEOUT If specified, the relay will be sigkilled in this number of seconds. -s, --sigkill-on-stream-stop If True, --sigkill-timeout will not be counted when no requests are active, and during requests, each successful data transmission will reset the timeout. This can be used to detect stale streams if you expect an application to be constantly receiving data. ``` ## Python module This package also provides Python module `http_relay`. You can start the relay as a part of your application like this: ```python from http_relay import run # ... run("0.0.0.0", 80, "cvut.cz", 80) ``` ## Examples ```bash http-relay cvut.cz 80 8080 # redirects local port 8080 to cvut.cz:80 http-relay cvut.cz 2101 # redirects local port 2101 to cvut.cz:2101 http-relay cvut.cz 80 8080 localhost # redirects localhost:8080 to cvut.cz:80 (i.e. no external access to the 8080 port) http-relay stream.cz 2101 -t 10 -s # redirects local port 2101 to stream.cz and kills the relay after 10 secs if a stream is being downloaded and becomes stale ``` http-relay-2.1.3/src/http_relay.egg-info/top_level.txt0000644000175000017500000000001314500271115022704 0ustar jriverojriverohttp_relay http-relay-2.1.3/src/http_relay.egg-info/entry_points.txt0000644000175000017500000000007014500271115023453 0ustar jriverojrivero[console_scripts] http-relay = http_relay.__main__:main http-relay-2.1.3/src/http_relay.egg-info/dependency_links.txt0000644000175000017500000000000114500271115024226 0ustar jriverojrivero http-relay-2.1.3/src/http_relay.egg-info/requires.txt0000644000175000017500000000001714500271115022556 0ustar jriverojrivero [test] pytest http-relay-2.1.3/setup.py0000644000175000017500000000270714500271102015240 0ustar jriverojrivero# SPDX-License-Identifier: BSD-3-Clause # SPDX-FileCopyrightText: Czech Technical University in Prague from setuptools import setup setup( name="http-relay", version="2.1.3", description="Relay for HTTP messages (reverse proxy)", author="Martin Pecka", author_email="peci1@seznam.cz", maintainer="Martin Pecka", maintainer_email="peci1@seznam.cz", url="https://github.com/ctu-vras/http_relay", license="BSD 3-Clause", python_requires='>=2.7', packages=["http_relay"], package_dir={"": "src"}, extras_require={"test": ['pytest']}, entry_points={"console_scripts": ["http-relay = http_relay.__main__:main"]}, classifiers=[ "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Topic :: Communications", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development :: Libraries", "Topic :: System :: Networking", "Topic :: Utilities", ], keywords=["http", "proxy"], ) http-relay-2.1.3/test/0000755000175000017500000000000014500271115014503 5ustar jriverojriverohttp-relay-2.1.3/test/test_relay.py0000644000175000017500000001367314500271102017236 0ustar jriverojrivero# SPDX-License-Identifier: BSD-3-Clause # SPDX-FileCopyrightText: Czech Technical University in Prague """ Integration test testing the relay functionality. """ import http_relay import socket import threading import time import unittest from http_relay.__main__ import main try: from http.server import HTTPServer, BaseHTTPRequestHandler from http.client import * except ImportError: from SimpleHTTPServer import HTTPServer, BaseHTTPRequestHandler from httplib import HTTPConnection class TestHandler(BaseHTTPRequestHandler): __test__ = False def do_GET(self): self.send_response(200) self.end_headers() self.wfile.write(b"Test") class NTRIPHandler(BaseHTTPRequestHandler): """This handler simulates NTRIP server responses which use a slight modification of the HTTP protocol.""" def do_GET(self): self.wfile.write(b"ICY 200 OK\r\n") if not hasattr(self, '_headers_buffer'): self._headers_buffer = [] self.end_headers() self.wfile.write(b"Test") class TestServer(threading.Thread): __test__ = False def __init__(self, host, port, handler=TestHandler): """ Create and run the test server thread. :param str host: The host to listen on. :param int port: The port to listen on. """ threading.Thread.__init__(self) HTTPServer.address_family = \ socket.AF_INET if ":" not in host else socket.AF_INET6 self.server = HTTPServer((host, port), handler) self.daemon = True self.running = False self.start() def run(self): """ Process the server requests. """ try: self.running = True self.server.serve_forever() except Exception: pass class RelayThread(threading.Thread): def __init__(self, host, server_port, relay_port, use_main=False): """ Create and run the test relay. All args are passed to `run()`. """ threading.Thread.__init__(self) self.host = host self.server_port = server_port self.relay_port = relay_port self.use_main = use_main self.relay = None self.daemon = True self.running = False self.start() def run(self): """ Process the server requests. """ self.running = True if self.use_main: cli_args = list(map(str, [ self.host, self.server_port, self.relay_port, self.host, "--num-threads", 1 ])) main(cli_args) else: self.relay = http_relay.HttpRelay(self.host, self.relay_port, self.host, self.server_port, 1) self.relay.run(num_threads=1) def stop(self): if self.relay is not None: self.relay.shutdown() def get_response_body(resp): resp_body = b"" while True: chunk = resp.read(1) if not chunk: break resp_body += chunk return resp_body class TestRelay(unittest.TestCase): def test_local_relay_hostname(self): self.do_test('localhost', 8080, 8081) def test_local_relay_ipv4(self): self.do_test('127.0.0.1', 8040, 8041) def test_local_relay_ipv6(self): self.do_test('::1', 8060, 8061) def test_local_relay_ipv6_brackets(self): self.do_test('[::1]', 8050, 8051, "::1") def test_local_relay_ntrip(self): self.do_test('localhost', 2101, 2102, handler=NTRIPHandler) def test_main(self): self.do_test('localhost', 8090, 8091, use_main=True) def do_test(self, host, server_port, relay_port, server_host=None, use_main=False, handler=TestHandler): if server_host is None: server_host = host server_thread = TestServer(server_host, server_port, handler=handler) relay_thread = RelayThread( host, server_port, relay_port, use_main=use_main) while not server_thread.running or not relay_thread.running: time.sleep(0.01) time.sleep(1.0) if not issubclass(handler, TestHandler): HTTPConnection.response_class = \ http_relay.relay.NonstandardHttpResponse conn = HTTPConnection(server_host, relay_port, timeout=1.0) conn.request("GET", "test") resp = conn.getresponse() relay_thread.stop() self.assertEqual(200, resp.status) resp_body = get_response_body(resp) self.assertEqual(b"Test", resp_body) def test_concurrent(self): server_host = "localhost" host = "localhost" server_port = 8100 server_thread = TestServer(server_host, server_port, handler=TestHandler) relay_thread1 = RelayThread(host, server_port, 8101) relay_thread2 = RelayThread(host, server_port, 8102) while not server_thread.running or not relay_thread1.running or not relay_thread2.running: time.sleep(0.01) time.sleep(1.0) conn1 = HTTPConnection(server_host, 8101, timeout=1.0) conn1.request("GET", "test") resp1 = conn1.getresponse() conn2 = HTTPConnection(server_host, 8102, timeout=1.0) conn2.request("GET", "test") resp2 = conn2.getresponse() relay_thread1.stop() self.assertEqual(200, resp1.status) self.assertEqual(200, resp2.status) resp1_body = get_response_body(resp1) resp2_body = get_response_body(resp2) self.assertEqual(b"Test", resp1_body) self.assertEqual(b"Test", resp2_body) time.sleep(1.0) conn3 = HTTPConnection(server_host, 8102, timeout=1.0) conn3.request("GET", "test") resp3 = conn3.getresponse() relay_thread2.stop() self.assertEqual(200, resp3.status) resp3_body = get_response_body(resp3) self.assertEqual(b"Test", resp3_body) if __name__ == '__main__': unittest.main() http-relay-2.1.3/README.md0000644000175000017500000000507614500271102015007 0ustar jriverojrivero # http-relay Relay HTTP requests from localhost to a remote host (act as reverse HTTP proxy). This HTTP relay properly processes also the nonstandard HTTP responses like `ICY 200 OK` produced by Shoutcast or NTRIP streaming servers. The relay works properly with hostnames, IPv4 and IPv6 addresses. IPv6 addresses can be specified with or without `[]`. ## Usage ``` usage: http-relay [-h] [-n NUM_THREADS] [-b BUFFER_SIZE] [-t SIGKILL_TIMEOUT] [-s] host [port] [local_port] [local_addr] positional arguments: host The remote host to connect to (e.g. "cvut.cz") port The remote host's port to connect to (e.g. 80). local_port The local port to be used for the relay. If left out, will be the same as remote port. local_addr Local interface (IP or hostname) to run the relay on. By default, it runs on all IPv4 interfaces (0.0.0.0). optional arguments: -h, --help show this help message and exit -n NUM_THREADS, --num-threads NUM_THREADS Number of threads servicing the incoming requests. -b BUFFER_SIZE, --buffer-size BUFFER_SIZE Size of the buffer used for reading responses. Generally, a larger buffer should be more efficient, but if it is too large, the local clients may time out before they receive any data. -t SIGKILL_TIMEOUT, --sigkill-timeout SIGKILL_TIMEOUT If specified, the relay will be sigkilled in this number of seconds. -s, --sigkill-on-stream-stop If True, --sigkill-timeout will not be counted when no requests are active, and during requests, each successful data transmission will reset the timeout. This can be used to detect stale streams if you expect an application to be constantly receiving data. ``` ## Python module This package also provides Python module `http_relay`. You can start the relay as a part of your application like this: ```python from http_relay import run # ... run("0.0.0.0", 80, "cvut.cz", 80) ``` ## Examples ```bash http-relay cvut.cz 80 8080 # redirects local port 8080 to cvut.cz:80 http-relay cvut.cz 2101 # redirects local port 2101 to cvut.cz:2101 http-relay cvut.cz 80 8080 localhost # redirects localhost:8080 to cvut.cz:80 (i.e. no external access to the 8080 port) http-relay stream.cz 2101 -t 10 -s # redirects local port 2101 to stream.cz and kills the relay after 10 secs if a stream is being downloaded and becomes stale ```http-relay-2.1.3/LICENSE0000644000175000017500000000276314500271102014535 0ustar jriverojriveroBSD 3-Clause License Copyright (c) 2023, Czech Technical University in Prague Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. http-relay-2.1.3/setup.cfg0000644000175000017500000000004614500271115015345 0ustar jriverojrivero[egg_info] tag_build = tag_date = 0 http-relay-2.1.3/PKG-INFO0000644000175000017500000001305214500271115014622 0ustar jriverojriveroMetadata-Version: 2.1 Name: http-relay Version: 2.1.3 Summary: Relay for HTTP messages (reverse proxy) Home-page: https://github.com/ctu-vras/http_relay Author: Martin Pecka Author-email: Martin Pecka Maintainer: Martin Pecka Maintainer-email: Martin Pecka License: BSD 3-Clause License Copyright (c) 2023, Czech Technical University in Prague Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Project-URL: Homepage, https://github.com/ctu-vras/http_relay Keywords: http,proxy Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: Science/Research Classifier: Operating System :: OS Independent Classifier: Topic :: Communications Classifier: Topic :: Internet Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: System :: Networking Classifier: Topic :: Utilities Requires-Python: >=2.7 Description-Content-Type: text/markdown License-File: LICENSE Provides-Extra: test Requires-Dist: pytest; extra == "test" # http-relay Relay HTTP requests from localhost to a remote host (act as reverse HTTP proxy). This HTTP relay properly processes also the nonstandard HTTP responses like `ICY 200 OK` produced by Shoutcast or NTRIP streaming servers. The relay works properly with hostnames, IPv4 and IPv6 addresses. IPv6 addresses can be specified with or without `[]`. ## Usage ``` usage: http-relay [-h] [-n NUM_THREADS] [-b BUFFER_SIZE] [-t SIGKILL_TIMEOUT] [-s] host [port] [local_port] [local_addr] positional arguments: host The remote host to connect to (e.g. "cvut.cz") port The remote host's port to connect to (e.g. 80). local_port The local port to be used for the relay. If left out, will be the same as remote port. local_addr Local interface (IP or hostname) to run the relay on. By default, it runs on all IPv4 interfaces (0.0.0.0). optional arguments: -h, --help show this help message and exit -n NUM_THREADS, --num-threads NUM_THREADS Number of threads servicing the incoming requests. -b BUFFER_SIZE, --buffer-size BUFFER_SIZE Size of the buffer used for reading responses. Generally, a larger buffer should be more efficient, but if it is too large, the local clients may time out before they receive any data. -t SIGKILL_TIMEOUT, --sigkill-timeout SIGKILL_TIMEOUT If specified, the relay will be sigkilled in this number of seconds. -s, --sigkill-on-stream-stop If True, --sigkill-timeout will not be counted when no requests are active, and during requests, each successful data transmission will reset the timeout. This can be used to detect stale streams if you expect an application to be constantly receiving data. ``` ## Python module This package also provides Python module `http_relay`. You can start the relay as a part of your application like this: ```python from http_relay import run # ... run("0.0.0.0", 80, "cvut.cz", 80) ``` ## Examples ```bash http-relay cvut.cz 80 8080 # redirects local port 8080 to cvut.cz:80 http-relay cvut.cz 2101 # redirects local port 2101 to cvut.cz:2101 http-relay cvut.cz 80 8080 localhost # redirects localhost:8080 to cvut.cz:80 (i.e. no external access to the 8080 port) http-relay stream.cz 2101 -t 10 -s # redirects local port 2101 to stream.cz and kills the relay after 10 secs if a stream is being downloaded and becomes stale ``` http-relay-2.1.3/pyproject.toml0000644000175000017500000000265114500271102016440 0ustar jriverojrivero# SPDX-License-Identifier: BSD-3-Clause # SPDX-FileCopyrightText: Czech Technical University in Prague [build-system] requires = ["setuptools>=39.0.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "http-relay" version = "2.1.3" description = "Relay for HTTP messages (reverse proxy)" readme = "README.md" authors = [{ name = "Martin Pecka", email = "peci1@seznam.cz" }] maintainers = [{ name = "Martin Pecka", email = "peci1@seznam.cz" }] license = { file = "LICENSE" } classifiers = [ "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Topic :: Communications", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development :: Libraries", "Topic :: System :: Networking", "Topic :: Utilities", ] keywords = ["http", "proxy"] dependencies = [] requires-python = ">=2.7" [project.urls] Homepage = "https://github.com/ctu-vras/http_relay" [project.scripts] http-relay = "http_relay.__main__:main" [project.optional-dependencies] test = ["pytest"]