quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

caddy.py (6901B)


      1 #!/usr/bin/env python3
      2 # -*- coding: utf-8 -*-
      3 #***************************************************************************
      4 #                                  _   _ ____  _
      5 #  Project                     ___| | | |  _ \| |
      6 #                             / __| | | | |_) | |
      7 #                            | (__| |_| |  _ <| |___
      8 #                             \___|\___/|_| \_\_____|
      9 #
     10 # Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
     11 #
     12 # This software is licensed as described in the file COPYING, which
     13 # you should have received as part of this distribution. The terms
     14 # are also available at https://curl.se/docs/copyright.html.
     15 #
     16 # You may opt to use, copy, modify, merge, publish, distribute and/or sell
     17 # copies of the Software, and permit persons to whom the Software is
     18 # furnished to do so, under the terms of the COPYING file.
     19 #
     20 # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
     21 # KIND, either express or implied.
     22 #
     23 # SPDX-License-Identifier: curl
     24 #
     25 ###########################################################################
     26 #
     27 import logging
     28 import os
     29 import socket
     30 import subprocess
     31 import time
     32 from datetime import timedelta, datetime
     33 from json import JSONEncoder
     34 from typing import Dict
     35 
     36 from .curl import CurlClient
     37 from .env import Env
     38 from .ports import alloc_ports_and_do
     39 
     40 log = logging.getLogger(__name__)
     41 
     42 
     43 class Caddy:
     44 
     45     PORT_SPECS = {
     46         'caddy': socket.SOCK_STREAM,
     47         'caddys': socket.SOCK_STREAM,
     48     }
     49 
     50     def __init__(self, env: Env):
     51         self.env = env
     52         self._caddy = os.environ['CADDY'] if 'CADDY' in os.environ else env.caddy
     53         self._caddy_dir = os.path.join(env.gen_dir, 'caddy')
     54         self._docs_dir = os.path.join(self._caddy_dir, 'docs')
     55         self._conf_file = os.path.join(self._caddy_dir, 'Caddyfile')
     56         self._error_log = os.path.join(self._caddy_dir, 'caddy.log')
     57         self._tmp_dir = os.path.join(self._caddy_dir, 'tmp')
     58         self._process = None
     59         self._http_port = 0
     60         self._https_port = 0
     61         self._rmf(self._error_log)
     62 
     63     @property
     64     def docs_dir(self):
     65         return self._docs_dir
     66 
     67     @property
     68     def port(self) -> int:
     69         return self._https_port
     70 
     71     def clear_logs(self):
     72         self._rmf(self._error_log)
     73 
     74     def is_running(self):
     75         if self._process:
     76             self._process.poll()
     77             return self._process.returncode is None
     78         return False
     79 
     80     def start_if_needed(self):
     81         if not self.is_running():
     82             return self.start()
     83         return True
     84 
     85     def initial_start(self):
     86 
     87         def startup(ports: Dict[str, int]) -> bool:
     88             self._http_port = ports['caddy']
     89             self._https_port = ports['caddys']
     90             if self.start():
     91                 self.env.update_ports(ports)
     92                 return True
     93             self.stop()
     94             self._http_port = 0
     95             self._https_port = 0
     96             return False
     97 
     98         return alloc_ports_and_do(Caddy.PORT_SPECS, startup,
     99                                   self.env.gen_root, max_tries=3)
    100 
    101     def start(self, wait_live=True):
    102         assert self._http_port > 0 and self._https_port > 0
    103         self._mkpath(self._tmp_dir)
    104         if self._process:
    105             self.stop()
    106         self._write_config()
    107         args = [
    108             self._caddy, 'run'
    109         ]
    110         caddyerr = open(self._error_log, 'a')
    111         self._process = subprocess.Popen(args=args, cwd=self._caddy_dir, stderr=caddyerr)
    112         if self._process.returncode is not None:
    113             return False
    114         return not wait_live or self.wait_live(timeout=timedelta(seconds=Env.SERVER_TIMEOUT))
    115 
    116     def stop(self, wait_dead=True):
    117         self._mkpath(self._tmp_dir)
    118         if self._process:
    119             self._process.terminate()
    120             self._process.wait(timeout=2)
    121             self._process = None
    122             return not wait_dead or self.wait_dead(timeout=timedelta(seconds=5))
    123         return True
    124 
    125     def restart(self):
    126         self.stop()
    127         return self.start()
    128 
    129     def wait_dead(self, timeout: timedelta):
    130         curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
    131         try_until = datetime.now() + timeout
    132         while datetime.now() < try_until:
    133             check_url = f'https://{self.env.domain1}:{self.port}/'
    134             r = curl.http_get(url=check_url)
    135             if r.exit_code != 0:
    136                 return True
    137             log.debug(f'waiting for caddy to stop responding: {r}')
    138             time.sleep(.1)
    139         log.debug(f"Server still responding after {timeout}")
    140         return False
    141 
    142     def wait_live(self, timeout: timedelta):
    143         curl = CurlClient(env=self.env, run_dir=self._tmp_dir)
    144         try_until = datetime.now() + timeout
    145         while datetime.now() < try_until:
    146             check_url = f'https://{self.env.domain1}:{self.port}/'
    147             r = curl.http_get(url=check_url)
    148             if r.exit_code == 0:
    149                 return True
    150             time.sleep(.1)
    151         log.error(f"Caddy still not responding after {timeout}")
    152         return False
    153 
    154     def _rmf(self, path):
    155         if os.path.exists(path):
    156             return os.remove(path)
    157 
    158     def _mkpath(self, path):
    159         if not os.path.exists(path):
    160             return os.makedirs(path)
    161 
    162     def _write_config(self):
    163         domain1 = self.env.domain1
    164         creds1 = self.env.get_credentials(domain1)
    165         assert creds1  # convince pytype this isn't None
    166         domain2 = self.env.domain2
    167         creds2 = self.env.get_credentials(domain2)
    168         assert creds2  # convince pytype this isn't None
    169         self._mkpath(self._docs_dir)
    170         self._mkpath(self._tmp_dir)
    171         with open(os.path.join(self._docs_dir, 'data.json'), 'w') as fd:
    172             data = {
    173                 'server': f'{domain1}',
    174             }
    175             fd.write(JSONEncoder().encode(data))
    176         with open(self._conf_file, 'w') as fd:
    177             conf = [   # base server config
    178                 '{',
    179                 f'  http_port {self._http_port}',
    180                 f'  https_port {self._https_port}',
    181                 '  log default {',
    182                 '     level ERROR',
    183                 '}',
    184                 f'  servers :{self._https_port} {{',
    185                 '    protocols h3 h2 h1',
    186                 '  }',
    187                 '}',
    188                 f'{domain1}:{self._https_port} {{',
    189                 '  file_server * {',
    190                 f'    root {self._docs_dir}',
    191                 '  }',
    192                 f'  tls {creds1.cert_file} {creds1.pkey_file}',
    193                 '}',
    194             ]
    195             if self.env.http_port > 0:
    196                 conf.extend([
    197                     f'{domain2} {{',
    198                     f'  reverse_proxy /* http://localhost:{self.env.http_port} {{',
    199                     '  }',
    200                     f'  tls {creds2.cert_file} {creds2.pkey_file}',
    201                     '}',
    202                 ])
    203             fd.write("\n".join(conf))