Coverage for creepo/composerproxy.py: 68%
56 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-03 18:52 -0500
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-03 18:52 -0500
1"""
3Composer - proxy https://packagist.org for use by php developers
5"""
6import io
7import json
8import urllib.parse
9from urllib.parse import urlparse
10import cherrypy
12from httpproxy import HttpProxy
15class ComposerProxy:
16 """
17 Default configuration: { 'registry' : 'https://packagist.org' }
19 Exposed at endpoint /p2
21 When no_cache is not True then we will host selected 'source' and 'dist' packages
23 Configure a project to use Creepo by adding this to composer.json:
25.. code-block:: json
27 {
28 "repositories": [
29 {
30 "type": "composer",
31 "url": "https://${HOST_IP}:4443/p2/"
32 }
33 ],
34 }
35 """
37 def __init__(self, config):
38 self.logger = config['logger']
39 self.config = config
40 self.key = 'p2'
41 if self.key not in self.config:
42 self.config[self.key] = {
43 'registry': 'https://packagist.org', 'self': 'https://localhost:4443/p2'}
45 self._proxy = HttpProxy(self.config, self.key)
46 self.logger.debug('ComposerProxy instantiated with %s',
47 self.config[self.key])
49 def callback(self, _input_bytes, request):
50 """
51 When ComposerProxy acts as a registry it will retrieve meta-data from the configured
52 upstream proxy.
54 If no_cache is not True then the source and dist urls of the meta-data are re-written
55 to self-relative urls
57 """
58 self.logger.debug('%s received type(%s) as %s ',
59 __name__, type(_input_bytes), _input_bytes)
60 data = json.load(io.BytesIO(_input_bytes))
62 for package in data['packages']:
63 self.logger.debug('%s package is %s', __name__, package)
64 for version in data['packages'][package]:
65 if version.get('source') is not None:
66 version['source']['url'] = self.config[self.key]['self'] + \
67 '/source?q=' + \
68 urllib.parse.quote_plus(version['source']['url'])
69 if version.get('dist') is not None:
70 version['dist'][
71 'url'] = self.config[self.key]['self'] + \
72 '/dist?q=' + \
73 urllib.parse.quote_plus(version['dist']['url'])
75 request['response'] = bytes(json.dumps(data), 'utf-8')
77 @cherrypy.expose
78 def proxy(self, environ, start_response):
79 """
80 Proxy a composer request.
82 Creepo exposes a WSGI-compliant server at /p2
83 """
84 path = environ["REQUEST_URI"]
85 if path == f"/{self.key}/packages.json":
86 path = environ["REQUEST_URI"].removeprefix(f"/{self.key}")
88 self.logger.debug('%s %s composer(%s)', __name__,
89 cherrypy.request.method, path)
91 newpath = path
92 if cherrypy.request.query_string != '':
93 self.logger.debug('%s QUERY_STRING: %s', __name__,
94 cherrypy.request.query_string)
96 newrequest = {}
97 newrequest['method'] = cherrypy.request.method
98 newrequest['headers'] = {}
99 newrequest['actual_request'] = cherrypy.request
100 newrequest['logger'] = self.logger
102 if len(newpath.split('?q=')) > 1:
103 new_remote = urlparse(
104 urllib.parse.unquote_plus(newpath.split('?q=')[1]))
105 # Remove the leading / from the remaining path to get the storage bucket
106 newrequest['storage'] = f"{newpath.split('?q=')[0][1:]}"
107 newpath = new_remote.path
108 if '?' not in newpath and '&' in newpath:
109 newrequest['path'] = newpath.replace('&', '?', 1)
110 else:
111 newrequest['path'] = newpath
112 dynamic_proxy = HttpProxy(self._proxy.dynamic_config(
113 f"{new_remote.scheme}://{new_remote.netloc}"), self.key)
115 self.logger.info(
116 '%s Create new proxy with host %s and path %s', __name__,
117 f"{new_remote.scheme}://{new_remote.netloc}", new_remote.path)
119 newrequest['content_type'] = dynamic_proxy.mimetype(
120 newpath.split('?')[0], 'text/html')
121 return dynamic_proxy.rest_proxy(newrequest, start_response)
123 newrequest['content_type'] = self._proxy.mimetype(
124 path, 'application/octet-stream')
126 newrequest['path'] = newpath
127 newrequest['storage'] = self.key
128 newrequest['callback'] = self.callback
129 return self._proxy.rest_proxy(newrequest, start_response)