Coverage for creepo/composerproxy.py: 68%

56 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-03 18:52 -0500

1""" 

2 

3Composer - proxy https://packagist.org for use by php developers 

4 

5""" 

6import io 

7import json 

8import urllib.parse 

9from urllib.parse import urlparse 

10import cherrypy 

11 

12from httpproxy import HttpProxy 

13 

14 

15class ComposerProxy: 

16 """ 

17 Default configuration: { 'registry' : 'https://packagist.org' } 

18 

19 Exposed at endpoint /p2 

20 

21 When no_cache is not True then we will host selected 'source' and 'dist' packages 

22 

23 Configure a project to use Creepo by adding this to composer.json: 

24 

25.. code-block:: json 

26 

27 { 

28 "repositories": [ 

29 { 

30 "type": "composer", 

31 "url": "https://${HOST_IP}:4443/p2/" 

32 } 

33 ], 

34 } 

35 """ 

36 

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'} 

44 

45 self._proxy = HttpProxy(self.config, self.key) 

46 self.logger.debug('ComposerProxy instantiated with %s', 

47 self.config[self.key]) 

48 

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. 

53 

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 

56 

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)) 

61 

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']) 

74 

75 request['response'] = bytes(json.dumps(data), 'utf-8') 

76 

77 @cherrypy.expose 

78 def proxy(self, environ, start_response): 

79 """ 

80 Proxy a composer request. 

81 

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}") 

87 

88 self.logger.debug('%s %s composer(%s)', __name__, 

89 cherrypy.request.method, path) 

90 

91 newpath = path 

92 if cherrypy.request.query_string != '': 

93 self.logger.debug('%s QUERY_STRING: %s', __name__, 

94 cherrypy.request.query_string) 

95 

96 newrequest = {} 

97 newrequest['method'] = cherrypy.request.method 

98 newrequest['headers'] = {} 

99 newrequest['actual_request'] = cherrypy.request 

100 newrequest['logger'] = self.logger 

101 

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) 

114 

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) 

118 

119 newrequest['content_type'] = dynamic_proxy.mimetype( 

120 newpath.split('?')[0], 'text/html') 

121 return dynamic_proxy.rest_proxy(newrequest, start_response) 

122 

123 newrequest['content_type'] = self._proxy.mimetype( 

124 path, 'application/octet-stream') 

125 

126 newrequest['path'] = newpath 

127 newrequest['storage'] = self.key 

128 newrequest['callback'] = self.callback 

129 return self._proxy.rest_proxy(newrequest, start_response)