;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2016-2022 Ludovic Courtès ;;; Copyright © 2017, 2018 Clément Lassieur ;;; Copyright © 2017 Marius Bakke ;;; ;;; This file is part of GNU Guix. ;;; ;;; GNU Guix is free software; you can redistribute it and/or modify it ;;; under the terms of the GNU General Public License as published by ;;; the Free Software Foundation; either version 3 of the License, or (at ;;; your option) any later version. ;;; ;;; GNU Guix is distributed in the hope that it will be useful, but ;;; WITHOUT ANY WARRANTY; without even the implied warranty of ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;;; GNU General Public License for more details. ;;; ;;; You should have received a copy of the GNU General Public License ;;; along with GNU Guix. If not, see . (define-module (gnu tests ssh) #:use-module (gnu tests) #:use-module (gnu system) #:use-module (gnu system vm) #:use-module (gnu services) #:use-module (gnu services ssh) #:use-module (gnu services networking) #:use-module (gnu packages ssh) #:use-module (guix gexp) #:use-module (guix store) #:export (%test-openssh %test-dropbear)) (define* (run-ssh-test name ssh-service pid-file
aboutsummaryrefslogtreecommitdiff
# SPDX-License-Identifier: BSD-3-Clause

"""
The core for a "virtual network" proxy.
"""

# This file is part of Haketilo.
#
# Copyright (c) 2015, inaz2
# Copyright (C) 2021 jahoti <jahoti@tilde.team>
# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * 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.
#
# * Neither the name of proxy2 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.
#
#
# I, Wojtek Kosior, thereby promise not to sue for violation of this file's
# license. Although I request that you do not make use of this code in a way
# incompliant with the license, I am not going to enforce this in court.

from pathlib import Path
import socket, ssl, subprocess, sys, threading
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn

lock = threading.Lock()

class ProxyRequestHandler(BaseHTTPRequestHandler):
    """
    Handles a network request made to the proxy. Configures SSL encryption when
    needed.
    """
    def __init__(self, *args, **kwargs):
        """
        Initialize self. Uses the same arguments as
        http.server.BaseHTTPRequestHandler's constructor but also expect a
        `certdir` keyword argument with appropriate path.
        """
        self.certdir = Path(kwargs.pop('certdir')).resolve()
        super().__init__(*args, **kwargs)

    def log_error(self, *args, **kwargs):
        """
        Like log_error in http.server.BaseHTTPRequestHandler but suppresses
        "Request timed out: timeout('timed out',)".
        """
        if not isinstance(args[0], socket.timeout):
            super().log_error(*args, **kwargs)

    def get_cert(self, hostname):
        """
        If needed, generate a signed x509 certificate for `hostname`. Return
        paths to certificate's key file and to certificate itself in a tuple.
        """
        root_keyfile  = self.certdir / 'rootCA.key'
        root_certfile = self.certdir / 'rootCA.pem'
        keyfile       = self.certdir / 'site.key'
        certfile      = self.certdir / f'{hostname}.crt'

        with lock:
            requestfile = self.certdir / f'{hostname}.csr'
            if not certfile.exists():
                subprocess.run([
                    'openssl', 'req', '-new', '-key', str(keyfile),
                    '-subj', f'/CN={hostname}', '-out', str(requestfile)
                ], check=True)
                subprocess.run([
                    'openssl', 'x509', '-req', '-in', str(requestfile),
                    '-CA', str(root_certfile), '-CAkey', str(root_keyfile),
                    '-CAcreateserial', '-out', str(certfile), '-days', '1024'
                ], check=True)

        return keyfile, certfile

    def do_CONNECT(self):
        """Wrap the connection with SSL using on-demand signed certificate."""
        hostname = self.path.split(':')[0]
        sslargs = {'server_side': True}
        sslargs['keyfile'], sslargs['certfile'] = self.get_cert(hostname)

        self.send_response(200)
        self.end_headers()

        self.connection = ssl.wrap_socket(self.connection, **sslargs)
        self.rfile = self.connection.makefile('rb', self.rbufsize)
        self.wfile = self.connection.makefile('wb', self.wbufsize)

        connection_header = self.headers.get('Proxy-Connection', '').lower()
        self.close_connection = int(connection_header == 'close')

    def do_GET(self):
        content_length = int(self.headers.get('Content-Length', 0))
        req_body = self.rfile.read(content_length) if content_length else None

        if self.path[0] == '/':
            secure = 's' if isinstance(self.connection, ssl.SSLSocket) else ''
            self.path = f'http{secure}://{self.headers["Host"]}{self.path}'

        self.handle_request(req_body)

    do_OPTIONS = do_DELETE = do_PUT = do_HEAD = do_POST = do_GET

    def handle_request(self, req_body):
        """Default handler that does nothing. Please override."""
        pass


class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
    """The actual proxy server"""
    address_family, daemon_threads = socket.AF_INET6, True

    def handle_error(self, request, client_address):
        """
        Like handle_error in http.server.HTTPServer but suppresses socket/ssl
        related errors.
        """
        cls, e = sys.exc_info()[:2]
        if not (cls is socket.error or cls is ssl.SSLError):
            return super().handle_error(request, client_address)
(channel-open-session channel) (channel-request-exec channel "path-witness") (zero? (channel-get-exit-status channel)))))) (test-end))))) (gexp->derivation name test)) (define %test-openssh (system-test (name "openssh") (description "Connect to a running OpenSSH daemon.") (value (run-ssh-test name ;; Allow root logins with an empty password to ;; simplify testing. (service openssh-service-type (openssh-configuration (permit-root-login #t) (allow-empty-passwords? #t))) #f ;inetd-style, no PID file #:sftp? #t)))) (define %test-dropbear (system-test (name "dropbear") (description "Connect to a running Dropbear SSH daemon.") (value (run-ssh-test name (service dropbear-service-type (dropbear-configuration (root-login? #t) (allow-empty-passwords? #t))) "/var/run/dropbear.pid" ;; XXX: Our Dropbear is not built with PAM support. ;; Even when it is, it seems to ignore the PAM ;; 'session' requirements. #:test-getlogin? #f))))