• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tcalmant / ipopo / 17025960620

17 Aug 2025 09:16PM UTC coverage: 84.662% (+1.3%) from 83.323%
17025960620

Pull #176

github

web-flow
Merge 850c581e1 into 80da9234f
Pull Request #176: HTTP asynchronous server

653 of 761 new or added lines in 4 files covered. (85.81%)

182 existing lines in 4 files now uncovered.

14970 of 17682 relevant lines covered (84.66%)

3.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.97
/pelix/http/basic.py
1
#!/usr/bin/env python
2
# -- Content-Encoding: UTF-8 --
3
"""
1✔
4
Pelix basic HTTP service bundle.
5

6
Provides an implementation of the Pelix HTTP service based on the standard
7
Python library.
8

9
:author: Thomas Calmant
10
:copyright: Copyright 2025, Thomas Calmant
11
:license: Apache License 2.0
12
:version: 3.1.0
13

14
..
15

16
    Copyright 2025 Thomas Calmant
17

18
    Licensed under the Apache License, Version 2.0 (the "License");
19
    you may not use this file except in compliance with the License.
20
    You may obtain a copy of the License at
21

22
        https://www.apache.org/licenses/LICENSE-2.0
23

24
    Unless required by applicable law or agreed to in writing, software
25
    distributed under the License is distributed on an "AS IS" BASIS,
26
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27
    See the License for the specific language governing permissions and
28
    limitations under the License.
29
"""
30

31
import logging
4✔
32
import socket
4✔
33
import threading
4✔
34
import traceback
4✔
35
from http.server import BaseHTTPRequestHandler, HTTPServer
4✔
36
from socketserver import TCPServer, ThreadingMixIn
4✔
37
from typing import IO, TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union, cast
4✔
38

39
import pelix.http as http
4✔
40
import pelix.ipopo.constants as constants
4✔
41
import pelix.ipv6utils
4✔
42
import pelix.misc.ssl_wrap as ssl_wrap
4✔
43
import pelix.remote
4✔
44
import pelix.utilities as utilities
4✔
45
from pelix.internals.registry import ServiceReference
4✔
46
from pelix.ipopo.decorators import (
4✔
47
    BindField,
48
    ComponentFactory,
49
    HiddenProperty,
50
    Invalidate,
51
    Property,
52
    Provides,
53
    Requires,
54
    UnbindField,
55
    UpdateField,
56
    Validate,
57
)
58

59
if TYPE_CHECKING:
1✔
60
    from pelix.framework import BundleContext
61

62
# ------------------------------------------------------------------------------
63

64
# Module version
65
__version_info__ = (3, 1, 0)
4✔
66
__version__ = ".".join(str(x) for x in __version_info__)
4✔
67

68
# Documentation strings format
69
__docformat__ = "restructuredtext en"
4✔
70

71
# ------------------------------------------------------------------------------
72

73
HTTP_SERVICE_EXTRA = "http.extra"
4✔
74
""" HTTP service extra properties (dictionary) """
4✔
75

76
DEFAULT_BIND_ADDRESS = "0.0.0.0"
4✔
77
""" By default, bind to all IPv4 interfaces """
4✔
78

79
LOCALHOST_ADDRESS = "127.0.0.1"
4✔
80
"""
4✔
81
Local address, if None is given as binding address, instead of the default one
82
"""
83

84
# ------------------------------------------------------------------------------
85

86

87
class _HTTPServletRequest(http.AbstractHTTPServletRequest):
4✔
88
    """
89
    HTTP Servlet request helper
90
    """
91

92
    def __init__(self, request_handler: BaseHTTPRequestHandler, prefix: str) -> None:
4✔
93
        """
94
        Sets up the request helper
95

96
        :param request_handler: The basic request handler
97
        :param prefix: The path to the servlet root
98
        """
99
        self._handler = request_handler
4✔
100
        self._prefix = prefix
4✔
101

102
        # Compute the sub path
103
        self._sub_path = self._handler.path[len(prefix) :]
4✔
104
        if not self._sub_path.startswith("/"):
4✔
105
            self._sub_path = "/{0}".format(self._sub_path)
4✔
106

107
        while "//" in self._sub_path:
4✔
108
            self._sub_path = self._sub_path.replace("//", "/")
4✔
109

110
    def get_command(self) -> str:
4✔
111
        """
112
        Returns the HTTP verb (GET, POST, ...) used for the request
113
        """
114
        return self._handler.command
4✔
115

116
    def get_client_address(self) -> Tuple[str, int]:
4✔
117
        """
118
        Retrieves the address of the client
119

120
        :return: A (host, port) tuple
121
        """
122
        return self._handler.client_address
4✔
123

124
    def get_header(self, name: str, default: Optional[Any] = None) -> Any:
4✔
125
        """
126
        Retrieves the value of a header
127
        """
128
        return self._handler.headers.get(name, default)
4✔
129

130
    def get_headers(self) -> Dict[str, Any]:
4✔
131
        """
132
        Retrieves all headers
133
        """
134
        return cast(Dict[str, Any], self._handler.headers)
4✔
135

136
    def get_path(self) -> str:
4✔
137
        """
138
        Retrieves the request full path
139
        """
140
        return self._handler.path
4✔
141

142
    def get_prefix_path(self) -> str:
4✔
143
        """
144
        Returns the path to the servlet root
145

146
        :return: A request path (string)
147
        """
148
        return self._prefix
4✔
149

150
    def get_sub_path(self) -> str:
4✔
151
        """
152
        Returns the servlet-relative path, i.e. after the prefix
153

154
        :return: A request path (string)
155
        """
156
        return self._sub_path
4✔
157

158
    def get_rfile(self) -> IO[bytes]:
4✔
159
        """
160
        Retrieves the input as a file stream
161
        """
162
        return self._handler.rfile
4✔
163

164

165
class _HTTPServletResponse(http.AbstractHTTPServletResponse):
4✔
166
    """
167
    HTTP Servlet response helper
168
    """
169

170
    def __init__(self, request_handler: BaseHTTPRequestHandler) -> None:
4✔
171
        """
172
        Sets up the response helper
173

174
        :param request_handler: The basic request handler
175
        """
176
        self._handler = request_handler
4✔
177
        self._headers: Dict[str, Any] = {}
4✔
178

179
    def set_response(self, code: int, message: Optional[str] = None) -> None:
4✔
180
        """
181
        Sets the response line.
182
        This method should be the first called when sending an answer.
183

184
        :param code: HTTP result code
185
        :param message: Associated message
186
        """
187
        self._handler.send_response(code, message)
4✔
188

189
    def set_header(self, name: str, value: Any) -> None:
4✔
190
        """
191
        Sets the value of a header.
192
        This method should not be called after ``end_headers()``.
193

194
        :param name: Header name
195
        :param value: Header value
196
        """
197
        self._headers[name.lower()] = value
4✔
198

199
    def is_header_set(self, name: str) -> bool:
4✔
200
        """
201
        Checks if the given header has already been set
202

203
        :param name: Header name
204
        :return: True if it has already been set
205
        """
206
        return name.lower() in self._headers
4✔
207

208
    def end_headers(self) -> None:
4✔
209
        """
210
        Ends the headers part
211
        """
212
        # Send them all at once
213
        for name, value in self._headers.items():
4✔
214
            self._handler.send_header(name, value)
4✔
215

216
        self._handler.end_headers()
4✔
217

218
    def get_wfile(self) -> IO[bytes]:
4✔
219
        """
220
        Retrieves the output as a file stream.
221
        ``end_headers()`` should have been called before, except if you want
222
        to write your own headers.
223

224
        :return: The output file-like object
225
        """
UNCOV
226
        return self._handler.wfile
×
227

228
    def write(self, data: bytes) -> None:
4✔
229
        """
230
        Writes the given data.
231
        ``end_headers()`` should have been called before, except if you want
232
        to write your own headers.
233

234
        :param data: Data to be written
235
        """
236
        self._handler.wfile.write(data)
4✔
237

238

239
# ------------------------------------------------------------------------------
240

241

242
class _RequestHandler(BaseHTTPRequestHandler):
4✔
243
    """
244
    Basic HTTP server request handler
245
    """
246

247
    # Override the default HTTP version
248
    default_request_version = "HTTP/1.0"
4✔
249

250
    def __init__(self, http_svc: http.HTTPService, *args: Any, **kwargs: Any) -> None:
4✔
251
        """
252
        Sets up the request handler (called for each request)
253

254
        :param http_svc: The associated HTTP service
255
        """
256
        self._service = http_svc
4✔
257

258
        # This calls the do_* methods
259
        BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
4✔
260

261
    def __getattr__(self, name: str) -> Any:
4✔
262
        """
263
        Retrieves the do_* in the servlet corresponding to the request path.
264
        If the name is not a "do_*", returns the normal result of __getattr__.
265

266
        :param name: Name of the attribute
267
        :return: The found attribute
268
        :raise AttributeError: Attribute not found
269
        """
270
        if not name.startswith("do_"):
4✔
271
            # Not a request handling
272
            return object.__getattribute__(self, name)
4✔
273

274
        # Remove the query part and double-slashes in the request path
275
        parsed_path = self.path.split("?", 1)[0].replace("//", "/")
4✔
276

277
        # Get the corresponding servlet
278
        found_servlet = self._service.get_servlet(parsed_path)
4✔
279
        if found_servlet is not None:
4✔
280
            servlet, _, prefix, _ = found_servlet
4✔
281
            if hasattr(servlet, name):
4✔
282
                # Prepare the helpers
283
                request = _HTTPServletRequest(self, prefix)
4✔
284
                response = _HTTPServletResponse(self)
4✔
285

286
                # Create a wrapper to pass the handler to the servlet
287
                def wrapper() -> None:
4✔
288
                    """
289
                    Wrapped servlet call
290
                    """
291
                    try:
4✔
292
                        # Handle the request
293
                        getattr(servlet, name)(request, response)
4✔
UNCOV
294
                    except:
×
295
                        # Send a 500 error page on error
UNCOV
296
                        self.send_exception(response)
×
297

298
                # Return it
299
                return wrapper
4✔
300

301
        # Return the super implementation if needed
302
        return self.send_no_servlet_response
4✔
303

304
    def log_error(self, format: str, *args: Any, **kwargs: Any) -> None:
4✔
305
        # pylint: disable=W0221
306
        """
307
        Log server error
308
        """
UNCOV
309
        self._service.log(logging.ERROR, format, *args, **kwargs)
×
310

311
    def log_request(self, code: Union[str, int] = "-", size: Union[str, int] = "-") -> None:
4✔
312
        """
313
        Logs a request to the server
314
        """
315
        self._service.log(logging.DEBUG, '"%s" %s', self.requestline, code)
4✔
316

317
    def send_no_servlet_response(self) -> None:
4✔
318
        """
319
        Default response sent when no servlet is found for the requested path
320
        """
321
        # Use the helper to send the error page
322
        response = _HTTPServletResponse(self)
4✔
323
        response.send_content(404, self._service.make_not_found_page(self.path))
4✔
324

325
    def send_exception(self, response: http.AbstractHTTPServletResponse) -> None:
4✔
326
        """
327
        Sends an exception page with a 500 error code.
328
        Must be called from inside the exception handling block.
329

330
        :param response: The response handler
331
        """
332
        # Get a formatted stack trace
UNCOV
333
        stack = traceback.format_exc()
×
334

335
        # Log the error
UNCOV
336
        self.log_error("Error handling request upon: %s\n%s\n", self.path, stack)
×
337

338
        # Send the page
UNCOV
339
        response.send_content(500, self._service.make_exception_page(self.path, stack))
×
340

341

342
# ------------------------------------------------------------------------------
343

344

345
class _HttpServerFamily(ThreadingMixIn, HTTPServer):
4✔
346
    """
347
    A small modification to have a threaded HTTP Server with a custom address
348
    family
349

350
    Inspired from:
351
    http://www.arcfn.com/2011/02/ipv6-web-serving-with-arc-or-python.html
352
    """
353

354
    def __init__(
4✔
355
        self,
356
        server_address: Tuple[str, int],
357
        request_handler_class: Type[BaseHTTPRequestHandler],
358
        request_queue_size: int = 5,
359
        logger: Optional[logging.Logger] = None,
360
    ):
361
        """
362
        Proxy constructor
363

364
        :param server_address: The server address
365
        :param request_handler_class: The request handler class
366
        :param request_queue_size: The size of the request queue (clients waiting for treatment)
367
        :param logger: An optional logger, in case of ignored error
368
        """
369
        # Determine the address family
370
        addr_info = socket.getaddrinfo(server_address[0], server_address[1], 0, 0, socket.SOL_TCP)
4✔
371

372
        # Change the address family before the socket is created
373
        # Get the family of the first possibility
374
        self.address_family = addr_info[0][0]
4✔
375

376
        # Set the queue size
377
        self.request_queue_size = request_queue_size
4✔
378

379
        # Set up the server, socket, ... but do not bind immediately
380
        HTTPServer.__init__(self, server_address, request_handler_class, False)
4✔
381
        self.server_name = server_address[0]
4✔
382
        self.server_port = server_address[1]
4✔
383

384
        if self.address_family == socket.AF_INET6:
4✔
385
            # Explicitly ask to be accessible both by IPv4 and IPv6
386
            try:
4✔
387
                pelix.ipv6utils.set_double_stack(self.socket)
4✔
UNCOV
388
            except AttributeError as ex:
×
UNCOV
389
                if logger is not None:
×
UNCOV
390
                    logger.exception("System misses IPv6 constant: %s", ex)
×
UNCOV
391
            except socket.error as ex:
×
UNCOV
392
                if logger is not None:
×
UNCOV
393
                    logger.exception("Error setting up IPv6 double stack: %s", ex)
×
394

395
        # Bind & accept
396
        self.server_bind()
4✔
397
        self.server_activate()
4✔
398

399
    def server_bind(self) -> None:
4✔
400
        """
401
        Override server_bind to store the server name, even in IronPython.
402

403
        See https://ironpython.codeplex.com/workitem/29477
404
        """
405
        TCPServer.server_bind(self)
4✔
406
        host, port = self.socket.getsockname()[:2]
4✔
407
        self.server_port = port
4✔
408
        try:
4✔
409
            self.server_name = socket.getfqdn(host)
4✔
UNCOV
410
        except ValueError:
×
411
            # Use the local host name in case of error, like CPython does
UNCOV
412
            self.server_name = socket.gethostname()
×
413

414
    def process_request(
4✔
415
        self, request: socket.socket | Tuple[bytes, socket.socket], client_address: Any
416
    ) -> None:
417
        """
418
        Starts a new thread to process the request, adding the client address
419
        in its name.
420
        """
421
        thread = threading.Thread(
4✔
422
            name=f"HttpService-{self.server_port}-Client-{client_address}",
423
            target=self.process_request_thread,
424
            args=(request, client_address),
425
        )
426
        thread.daemon = self.daemon_threads
4✔
427
        thread.start()
4✔
428

429

430
# ------------------------------------------------------------------------------
431

432

433
@ComponentFactory(http.FACTORY_HTTP_BASIC)
4✔
434
@Provides(http.HTTP_SERVICE)
4✔
435
@Requires("_servlets_services", http.Servlet, True, True)
4✔
436
@Requires("_error_handler", http.ErrorHandler, optional=True)
4✔
437
@Property("_address", http.HTTP_SERVICE_ADDRESS, DEFAULT_BIND_ADDRESS)
4✔
438
@Property("_port", http.HTTP_SERVICE_PORT, 8080)
4✔
439
@Property("_uses_ssl", http.HTTP_USES_SSL, False)
4✔
440
@Property("_cert_file", http.HTTPS_CERT_FILE, None)
4✔
441
@Property("_key_file", http.HTTPS_KEY_FILE, None)
4✔
442
@HiddenProperty("_key_password", http.HTTPS_KEY_PASSWORD, None)
4✔
443
@Property("_extra", HTTP_SERVICE_EXTRA, None)
4✔
444
@Property("_instance_name", constants.IPOPO_INSTANCE_NAME)
4✔
445
@Property("_logger_name", "pelix.http.logger.name", "")
4✔
446
@Property("_logger_level", "pelix.http.logger.level", None)
4✔
447
@Property("_request_queue_size", "pelix.http.request_queue_size", 100)
4✔
448
class HttpServiceImpl(http.HTTPService):
4✔
449
    """
450
    Basic HTTP service component
451
    """
452

453
    def __init__(self) -> None:
4✔
454
        # Properties
455
        self._address = "0.0.0.0"
4✔
456
        self._port = 8080
4✔
457
        self._uses_ssl = False
4✔
458
        self._extra: Optional[Dict[str, Any]] = None
4✔
459
        self._instance_name: Optional[str] = None
4✔
460
        self._logger_name: Optional[str] = None
4✔
461
        self._logger_level: Optional[int] = None
4✔
462
        self._request_queue_size = 5
4✔
463

464
        # SSL Parameters
465
        self._cert_file: Optional[str] = None
4✔
466
        self._key_file: Optional[str] = None
4✔
467
        self._key_password: Optional[str] = None
4✔
468

469
        # Validation flag
470
        self._validated = False
4✔
471

472
        # The logger
473
        self._logger: Optional[logging.Logger] = None
4✔
474

475
        # Servlets registry lock
476
        self._lock = threading.RLock()
4✔
477

478
        # Path -> (servlet, parameters)
479
        self._servlets: Dict[str, Tuple[http.Servlet, Dict[str, Any]]] = {}
4✔
480

481
        # Fields injected by iPOPO
482
        self._servlets_services: List[http.Servlet] = []
4✔
483
        self._error_handler: Optional[http.ErrorHandler] = None
4✔
484

485
        # Servlet -> ServiceReference
486
        self._servlets_refs: Dict[http.Servlet, ServiceReference[http.Servlet]] = {}
4✔
487
        self._binding_lock = threading.RLock()
4✔
488

489
        # Server control
490
        self._server: Optional[HTTPServer] = None
4✔
491
        self._thread: Optional[threading.Thread] = None
4✔
492

493
    def __str__(self) -> str:
4✔
494
        """
495
        String representation of the instance
496
        """
UNCOV
497
        return f"BasicHttpService({self._address}, {self._port})"
×
498

499
    def __safe_callback(self, instance: http.Servlet, method: str, *args: Any, **kwargs: Any) -> Any:
4✔
500
        """
501
        Safely calls the given method in the given instance.
502
        Returns True on method absence.
503
        Returns False on error.
504
        Returns the method result if found.
505

506
        :param instance: The instance to call
507
        :param method: The method to call in the instance
508
        :return: The method result or True on method absence or False on error
509
        """
510
        # Call back the method
511
        if instance is None:
4✔
512
            # Consider invalidity as a failure
UNCOV
513
            return False
×
514

515
        try:
4✔
516
            callback = getattr(instance, method)
4✔
517
        except AttributeError:
4✔
518
            # Consider absence as a success
519
            return True
4✔
520

521
        try:
4✔
522
            result = callback(*args, **kwargs)
4✔
523
            if result is None:
4✔
524
                # Special case: consider None as success
525
                return True
4✔
526

527
            return result
4✔
528

529
        except Exception as ex:
4✔
530
            self.log_exception("Error calling back an instance: %s", ex)
4✔
531

532
        return False
4✔
533

534
    def __register_servlet_service(
4✔
535
        self, service: http.Servlet, service_reference: ServiceReference[http.Servlet]
536
    ) -> None:
537
        """
538
        Registers a servlet according to its service properties
539

540
        :param service: A servlet service
541
        :param service_reference: The associated ServiceReference
542
        """
543
        # Servlet bound
544
        paths = service_reference.get_property(http.HTTP_SERVLET_PATH)
4✔
545
        if utilities.is_string(paths):
4✔
546
            # Register the servlet to a single path
547
            self.register_servlet(paths, service)
4✔
548
        elif isinstance(paths, (list, tuple)):
4✔
549
            # Register the servlet to multiple paths
550
            for path in paths:
4✔
551
                self.register_servlet(path, service)
4✔
552

553
    @BindField("_servlets_services")
4✔
554
    def _bind(self, _: str, service: http.Servlet, service_reference: ServiceReference[http.Servlet]) -> None:
4✔
555
        """
556
        Called by iPOPO when a service is bound
557
        """
558
        # Ignore imported services
559
        if self.__is_imported(service_reference):
4✔
UNCOV
560
            return
×
561
        with self._binding_lock:
4✔
562
            self._servlets_refs[service] = service_reference
4✔
563

564
            if self._validated:
4✔
565
                # We've been validated, register the service
566
                self.__register_servlet_service(service, service_reference)
4✔
567

568
    @UpdateField("_servlets_services")
4✔
569
    def _update(
4✔
570
        self,
571
        _: str,
572
        service: http.Servlet,
573
        service_reference: ServiceReference[http.Servlet],
574
        old_properties: Dict[str, Any],
575
    ) -> None:
576
        """
577
        Called by iPOPO when the properties of a service have been updated
578
        """
579
        # Ignore imported services
580
        if self.__is_imported(service_reference):
4✔
UNCOV
581
            return
×
582
        # Check if the property concerns the registration
583
        old_path = old_properties.get(http.HTTP_SERVLET_PATH)
4✔
584
        new_path = service_reference.get_property(http.HTTP_SERVLET_PATH)
4✔
585
        if old_path == new_path:
4✔
586
            # Nothing to do
UNCOV
587
            return
×
588

589
        with self._binding_lock:
4✔
590
            # Unregister the previous paths
591
            self.unregister(None, service)
4✔
592

593
            if self._validated:
4✔
594
                # Register the service with its new properties
595
                self.__register_servlet_service(service, service_reference)
4✔
596

597
    @UnbindField("_servlets_services")
4✔
598
    def _unbind(
4✔
599
        self, _: str, service: http.Servlet, service_reference: ServiceReference[http.Servlet]
600
    ) -> None:
601
        """
602
        Called by iPOPO when a service is gone
603
        """
604
        # Ignore imported services
605
        if self.__is_imported(service_reference):
4✔
UNCOV
606
            return
×
607
        with self._binding_lock:
4✔
608
            # Servlet gone: unregister all paths associated to this servlet
609
            self.unregister(None, service)
4✔
610

611
            # Remove the service reference
612
            try:
4✔
613
                del self._servlets_refs[service]
4✔
614
            except KeyError:
4✔
615
                self.log(logging.DEBUG, "Tried to remove an unknown servlet: %s", service)
4✔
616

617
    def get_access(self) -> Tuple[str, int]:
4✔
618
        """
619
        Retrieves the (address, port) tuple to access the server
620
        """
621
        assert self._server is not None
4✔
622
        sock_info = self._server.socket.getsockname()
4✔
623

624
        # Only keep the address and the port information
625
        return sock_info[0], sock_info[1]
4✔
626

627
    @staticmethod
4✔
628
    def get_hostname() -> str:
4✔
629
        """
630
        Retrieves the server host name
631

632
        :return: The server host name
633
        """
634
        return socket.gethostname()
4✔
635

636
    def is_https(self) -> bool:
4✔
637
        """
638
        Returns True if this is an HTTPS server
639

640
        :return: True if this server uses SSL
641
        """
642
        return self._uses_ssl
4✔
643

644
    def get_registered_paths(self) -> List[str]:
4✔
645
        """
646
        Returns the paths registered by servlets
647

648
        :return: The paths registered by servlets (sorted list)
649
        """
650
        return sorted(self._servlets)
4✔
651

652
    def get_servlet(self, path: Optional[str]) -> Optional[Tuple[http.Servlet, Dict[str, Any], str, http.ServletType]]:
4✔
653
        """
654
        Retrieves the servlet matching the given path and its parameters.
655
        Returns None if no servlet matches the given path.
656

657
        :param path: A request URI
658
        :return: A tuple (servlet, parameters, prefix) or None
659
        """
660
        if not path or path[0] != "/":
4✔
661
            # No path, nothing to return
662
            return None
4✔
663

664
        # Use lower case for comparison
665
        path = path.lower()
4✔
666

667
        if path[-1] != "/":
4✔
668
            # Add a trailing slash
669
            path += "/"
4✔
670

671
        with self._lock:
4✔
672
            longest_match = ""
4✔
673
            longest_match_len = 0
4✔
674
            for servlet_path in self._servlets:
4✔
675
                tested_path = servlet_path
4✔
676
                if tested_path[-1] != "/":
4✔
677
                    # Add a trailing slash
678
                    tested_path += "/"
4✔
679

680
                if path.startswith(tested_path) and len(servlet_path) > longest_match_len:
4✔
681
                    # Found a corresponding servlet
682
                    # which is deeper than the previous one
683
                    longest_match = servlet_path
4✔
684
                    longest_match_len = len(servlet_path)
4✔
685

686
            # Return the found servlet
687
            if not longest_match:
4✔
688
                # No match found
689
                return None
4✔
690

691
            # Retrieve the stored information
692
            servlet, params = self._servlets[longest_match]
4✔
693
            return servlet, params, longest_match, http.ServletType.SYNC
4✔
694

695
    def make_not_found_page(self, path: str) -> str:
4✔
696
        """
697
        Prepares a "page not found" page for a 404 error
698

699
        :param path: Request path
700
        :return: A HTML page
701
        """
702
        page = None
4✔
703
        if self._error_handler is not None:
4✔
UNCOV
704
            page = self._error_handler.make_not_found_page(path)
×
705

706
        if not page:
4✔
707
            page = f"""<html>
4✔
708
<head>
709
<title>404 - Page not found</title>
710
</head>
711
<body>
712
<h1>Page not found</h1>
713
<p>No servlet is associated to this path:</p>
714
<pre>{path}</pre>
715
<h2>Registered paths:</h2>
716
{http.make_html_list(self.get_registered_paths())}
717
</body>
718
</html>"""
719
        return page
4✔
720

721
    def make_exception_page(self, path: str, stack: str) -> str:
4✔
722
        """
723
        Prepares a page printing an exception stack trace in a 500 error
724

725
        :param path: Request path
726
        :param stack: Exception stack trace
727
        :return: A HTML page
728
        """
UNCOV
729
        page = None
×
UNCOV
730
        if self._error_handler is not None:
×
UNCOV
731
            page = self._error_handler.make_exception_page(path, stack)
×
732

UNCOV
733
        if not page:
×
UNCOV
734
            page = f"""<html>
×
735
<head>
736
<title>500 - Internal Server Error</title>
737
</head>
738
<body>
739
<h1>Internal Server Error</h1>
740
<p>Error handling request upon: {path}</p>
741
<pre>
742
{stack}
743
</pre>
744
</body>
745
</html>"""
746
        return page
×
747

748
    def register_servlet(
4✔
749
        self,
750
        path: str,
751
        servlet: http.Servlet,
752
        parameters: Optional[Dict[str, Any]] = None,
753
        servlet_type: http.ServletType = http.ServletType.SYNC,
754
    ) -> bool:
755
        """
756
        Registers a servlet
757

758
        :param path: Path handled by this servlet
759
        :param servlet: The servlet instance
760
        :param parameters: The parameters associated to this path
761
        :param servlet_type: The type of servlet (sync, async, websocket, ...)
762
        :return: True if the servlet has been registered, False if it refused the binding.
763
        :raise ValueError: Invalid path or handler
764
        """
765
        if servlet_type != http.ServletType.SYNC:
4✔
NEW
UNCOV
766
            raise ValueError("Asynchronous mode is not supported")
×
767

768
        if servlet is None:
4✔
769
            raise ValueError("Invalid servlet instance")
4✔
770

771
        if not path or path[0] != "/":
4✔
772
            raise ValueError("Invalid path given to register the servlet: {0}".format(path))
4✔
773

774
        # Use lower-case paths
775
        path = path.lower()
4✔
776

777
        # Prepare the parameters
778
        if parameters is None:
4✔
779
            parameters = {}
4✔
780

781
        with self._lock:
4✔
782
            if path in self._servlets:
4✔
783
                # Already registered path
784
                if self._servlets[path][0] is servlet:
4✔
785
                    # Double-registration: Nothing to do
786
                    return True
4✔
787
                else:
788
                    # Path is already taken by another servlet
789
                    already_taken = True
4✔
790
            else:
791
                # Path is available
792
                already_taken = False
4✔
793

794
            # Add server information in parameters
795
            parameters[http.PARAM_ADDRESS] = self._address
4✔
796
            parameters[http.PARAM_PORT] = self._port
4✔
797
            parameters[http.PARAM_HTTPS] = self._uses_ssl
4✔
798
            parameters[http.PARAM_NAME] = self._instance_name
4✔
799
            parameters[http.PARAM_EXTRA] = self._extra.copy() if self._extra else None
4✔
800

801
            # The servlet might refuse to be bound to this server
802
            if not self.__safe_callback(servlet, "accept_binding", path, parameters):
4✔
803
                # Server refused: stop right there
804
                # => No need to raise the "already taken path" exception
805
                return False
4✔
806

807
            if already_taken:
4✔
808
                # The path is already taken by another servlet
809
                raise ValueError("A servlet is already registered on {0}".format(path))
4✔
810

811
            # Tell the servlet it can be bound to the path
812
            if self.__safe_callback(servlet, "bound_to", path, parameters):
4✔
813
                # Store the servlet
814
                self._servlets[path] = (servlet, parameters)
4✔
815
                return True
4✔
816

817
            # The servlet refused the binding
818
            return False
4✔
819

820
    def unregister(self, path: Optional[str], servlet: Optional[http.Servlet] = None) -> bool:
4✔
821
        """
822
        Unregisters the servlet for the given path
823

824
        :param path: The path to a servlet
825
        :param servlet: If given, unregisters all the paths handled by this servlet
826
        :return: True if at least one path as been unregistered, else False
827
        """
828
        if servlet is not None:
4✔
829
            with self._lock:
4✔
830
                # Unregister all paths for this servlet
831
                paths = [
4✔
832
                    servlet_path
833
                    for (servlet_path, servlet_info) in self._servlets.items()
834
                    if servlet_info[0] == servlet
835
                ]
836

837
            result = False
4✔
838
            for servlet_path in paths:
4✔
839
                result |= self.unregister(servlet_path)
4✔
840

841
            return result
4✔
842
        else:
843
            if not path:
4✔
844
                # Invalid path
845
                return False
4✔
846

847
            # Always use lower case to compare paths
848
            path = path.lower()
4✔
849

850
            with self._lock:
4✔
851
                # Notify the servlet
852
                servlet_info = self._servlets.get(path)
4✔
853
                if servlet_info is None:
4✔
854
                    # Unknown path
855
                    return False
4✔
856

857
                self.__safe_callback(servlet_info[0], "unbound_from", path, servlet_info[1])
4✔
858

859
                # Remove the servlet
860
                try:
4✔
861
                    del self._servlets[path]
4✔
862
                except KeyError:
4✔
863
                    self.log(logging.DEBUG, "Tried to remove an unknown servlet path: %s", path)
4✔
864
                return True
4✔
865

866
    def log(self, level: int, message: str, *args: Any, **kwargs: Any) -> None:
4✔
867
        """
868
        Logs the given message
869

870
        :param level: Log entry level
871
        :param message: Log message (Python logging format)
872
        """
873
        if self._logger is not None:
4✔
874
            # Log the message
875
            self._logger.log(level, message, *args, **kwargs)
4✔
876

877
    def log_exception(self, message: str, *args: Any, **kwargs: Any) -> None:
4✔
878
        """
879
        Logs an exception
880

881
        :param message: Log message (Python logging format)
882
        """
883
        if self._logger is not None:
4✔
884
            # Log the exception
885
            self._logger.exception(message, *args, **kwargs)
4✔
886

887
    @Validate
4✔
888
    def validate(self, _: "BundleContext") -> None:
4✔
889
        """
890
        Component validation
891
        """
892
        # Check if we'll use an SSL connection
893
        self._uses_ssl = self._cert_file is not None
4✔
894

895
        if not self._address:
4✔
896
            # No address given, use the localhost address
897
            self._address = LOCALHOST_ADDRESS
4✔
898

899
        if self._port is None:
4✔
900
            # Random port
901
            self._port = 0
4✔
902
        else:
903
            # Ensure we have an integer
904
            self._port = int(self._port)
4✔
905
            if self._port < 0:
4✔
906
                # Random port
UNCOV
907
                self._port = 0
×
908

909
        # Normalize the request queue size
910
        try:
4✔
911
            self._request_queue_size = int(self._request_queue_size)
4✔
UNCOV
912
        except (ValueError, TypeError):
×
UNCOV
913
            self._request_queue_size = 5
×
914

915
        if self._request_queue_size <= 0:
4✔
UNCOV
916
            self._request_queue_size = 5
×
917

918
        # Normalize the extra properties
919
        if not isinstance(self._extra, dict):
4✔
920
            self._extra = {}
4✔
921

922
        # Set up the logger
923
        if not self._logger_name:
4✔
924
            # Empty name, use the instance name
925
            self._logger_name = self._instance_name
4✔
926

927
        self._logger = logging.getLogger(self._logger_name)
4✔
928

929
        level: int | None = None
4✔
930
        if self._logger_level is None:
4✔
931
            self._logger.level = logging.INFO
4✔
932
        elif isinstance(self._logger_level, int):
4✔
NEW
UNCOV
933
            level = self._logger_level
×
934
        else:
935
            level = utilities.get_log_level(self._logger_level)
4✔
936
            if level is None:
4✔
NEW
937
                try:
×
NEW
UNCOV
938
                    level = int(self._logger_level)
×
NEW
939
                except ValueError:
×
940
                    # Invalid level
NEW
941
                    level = None
×
942

943
        self._logger.level = level if level is not None else logging.INFO
4✔
944

945
        self.log(
4✔
946
            logging.INFO,
947
            "Starting HTTP%s server: [%s]:%d ...",
948
            "S" if self._uses_ssl else "",
949
            self._address,
950
            self._port,
951
        )
952

953
        parent = self
4✔
954

955
        class LocalRequestHandler(_RequestHandler):
4✔
956
            def __init__(self, *args: Any, **kwargs: Any) -> None:
4✔
957
                super().__init__(parent, *args, **kwargs)
4✔
958

959
        # Create the server
960
        self._server = _HttpServerFamily(
4✔
961
            (self._address, self._port),
962
            LocalRequestHandler,
963
            self._request_queue_size,
964
            self._logger,
965
        )
966

967
        if self._uses_ssl:
4✔
968
            if not self._cert_file or not self._key_file:
4✔
UNCOV
969
                raise ValueError("No certificate given to setup HTTPS")
×
970

971
            # Activate HTTPS if required
972
            self._server.socket = ssl_wrap.wrap_socket(
4✔
973
                self._server.socket,
974
                self._cert_file,
975
                self._key_file,
976
                self._key_password,
977
            )
978

979
        # Property update (if port was 0)
980
        self._port = self._server.server_port
4✔
981

982
        # Run it in a separate thread
983
        self._thread = threading.Thread(
4✔
984
            target=self._server.serve_forever,
985
            name=f"HttpService-{self._port}-Server",
986
        )
987
        self._thread.daemon = True
4✔
988
        self._thread.start()
4✔
989

990
        with self._binding_lock:
4✔
991
            # Set the validation flag up, once the server is ready
992
            self._validated = True
4✔
993

994
            # Register bound servlets
995
            for service, svc_ref in self._servlets_refs.items():
4✔
996
                self.__register_servlet_service(service, svc_ref)
4✔
997

998
        self.log(
4✔
999
            logging.INFO,
1000
            "HTTP%s server started: [%s]:%d",
1001
            "S" if self._uses_ssl else "",
1002
            self._address,
1003
            self._port,
1004
        )
1005

1006
    @Invalidate
4✔
1007
    def invalidate(self, _: "BundleContext") -> None:
4✔
1008
        """
1009
        Component invalidation
1010
        """
1011
        with self._binding_lock:
4✔
1012
            # Refuse new registrations
1013
            self._validated = False
4✔
1014

1015
            # Unregister servlets (to call unbound_from...)
1016
            for service in self._servlets_refs:
4✔
UNCOV
1017
                self.unregister(None, service)
×
1018

1019
        self.log(
4✔
1020
            logging.INFO,
1021
            "Shutting down HTTP server: [%s]:%d ...",
1022
            self._address,
1023
            self._port,
1024
        )
1025

1026
        # Shutdown server (if active)
1027
        if self._server is not None:
4✔
1028
            self._server.shutdown()
4✔
1029

1030
            # Wait for the thread to stop...
1031
            self.log(
4✔
1032
                logging.INFO,
1033
                "Waiting HTTP server ([%s]:%d) thread to stop...",
1034
                self._address,
1035
                self._port,
1036
            )
1037

1038
            if self._thread is not None:
4✔
1039
                self._thread.join(2)
4✔
1040

1041
            # Close the server
1042
            self._server.server_close()
4✔
1043

1044
        self.log(
4✔
1045
            logging.INFO,
1046
            "HTTP server down: [%s]:%d ...",
1047
            self._address,
1048
            self._port,
1049
        )
1050

1051
        # Clean up
1052
        self._servlets.clear()
4✔
1053
        self._thread = None
4✔
1054
        self._server = None
4✔
1055
        self._logger = None
4✔
1056

1057
    @staticmethod
4✔
1058
    def __is_imported(service_reference: ServiceReference[Any]) -> bool:
4✔
1059
        """
1060
        Tests if the given service has been imported by Remote Services
1061

1062
        :param service_reference: The reference of the service to check
1063
        :return: True if the service is flagged as imported
1064
        """
1065
        return cast(bool, service_reference.get_property(pelix.remote.PROP_IMPORTED))
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc