##// END OF EJS Templates
vcs: Add custom response header 'X-RhodeCode-Backend' to indicate VCS responses and which backend is in use.
Martin Bornhold -
r608:74424d4b default
parent child Browse files
Show More
@@ -1,130 +1,136 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from urlparse import urljoin
22 22
23 23 import requests
24 24
25 25 import rhodecode
26 26 from rhodecode.lib.middleware import simplevcs
27 27 from rhodecode.lib.utils import is_valid_repo
28 28
29 29
30 30 class SimpleSvnApp(object):
31 31 IGNORED_HEADERS = [
32 32 'connection', 'keep-alive', 'content-encoding',
33 33 'transfer-encoding', 'content-length']
34 34
35 35 def __init__(self, config):
36 36 self.config = config
37 37
38 38 def __call__(self, environ, start_response):
39 39 request_headers = self._get_request_headers(environ)
40 40
41 41 data = environ['wsgi.input']
42 42 # johbo: Avoid that we end up with sending the request in chunked
43 43 # transfer encoding (mainly on Gunicorn). If we know the content
44 44 # length, then we should transfer the payload in one request.
45 45 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
46 46 data = data.read()
47 47
48 48 response = requests.request(
49 49 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
50 50 data=data, headers=request_headers)
51 51
52 52 response_headers = self._get_response_headers(response.headers)
53 53 start_response(
54 54 '{} {}'.format(response.status_code, response.reason),
55 55 response_headers)
56 56 return response.iter_content(chunk_size=1024)
57 57
58 58 def _get_url(self, path):
59 59 return urljoin(
60 60 self.config.get('subversion_http_server_url', ''), path)
61 61
62 62 def _get_request_headers(self, environ):
63 63 headers = {}
64 64
65 65 for key in environ:
66 66 if not key.startswith('HTTP_'):
67 67 continue
68 68 new_key = key.split('_')
69 69 new_key = [k.capitalize() for k in new_key[1:]]
70 70 new_key = '-'.join(new_key)
71 71 headers[new_key] = environ[key]
72 72
73 73 if 'CONTENT_TYPE' in environ:
74 74 headers['Content-Type'] = environ['CONTENT_TYPE']
75 75
76 76 if 'CONTENT_LENGTH' in environ:
77 77 headers['Content-Length'] = environ['CONTENT_LENGTH']
78 78
79 79 return headers
80 80
81 81 def _get_response_headers(self, headers):
82 return [
82 headers = [
83 83 (h, headers[h])
84 84 for h in headers
85 85 if h.lower() not in self.IGNORED_HEADERS
86 86 ]
87 87
88 # Add custom response header to indicate that this is a VCS response
89 # and which backend is used.
90 headers.append(('X-RhodeCode-Backend', 'svn'))
91
92 return headers
93
88 94
89 95 class SimpleSvn(simplevcs.SimpleVCS):
90 96
91 97 SCM = 'svn'
92 98 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
93 99
94 100 def _get_repository_name(self, environ):
95 101 """
96 102 Gets repository name out of PATH_INFO header
97 103
98 104 :param environ: environ where PATH_INFO is stored
99 105 """
100 106 path = environ['PATH_INFO'].split('!')
101 107 repo_name = path[0].strip('/')
102 108
103 109 # SVN includes the whole path in it's requests, including
104 110 # subdirectories inside the repo. Therefore we have to search for
105 111 # the repo root directory.
106 112 if not is_valid_repo(repo_name, self.basepath, self.SCM):
107 113 current_path = ''
108 114 for component in repo_name.split('/'):
109 115 current_path += component
110 116 if is_valid_repo(current_path, self.basepath, self.SCM):
111 117 return current_path
112 118 current_path += '/'
113 119
114 120 return repo_name
115 121
116 122 def _get_action(self, environ):
117 123 return (
118 124 'pull'
119 125 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
120 126 else 'push')
121 127
122 128 def _create_wsgi_app(self, repo_path, repo_name, config):
123 129 return SimpleSvnApp(config)
124 130
125 131 def _create_config(self, extras, repo_name):
126 132 server_url = rhodecode.CONFIG.get(
127 133 'rhodecode_subversion_http_server_url', '')
128 134 extras['subversion_http_server_url'] = (
129 135 server_url or 'http://localhost/')
130 136 return extras
@@ -1,135 +1,140 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Implementation of the scm_app interface using raw HTTP communication.
23 23 """
24 24
25 25 import base64
26 26 import logging
27 27 import urlparse
28 28 import wsgiref.util
29 29
30 30 import msgpack
31 31 import requests
32 32 import webob.request
33 33
34 34 import rhodecode
35 35
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 def create_git_wsgi_app(repo_path, repo_name, config):
41 41 url = _vcs_streaming_url() + 'git/'
42 return VcsHttpProxy(url, repo_path, repo_name, config)
42 return VcsHttpProxy(url, repo_path, repo_name, config, 'git')
43 43
44 44
45 45 def create_hg_wsgi_app(repo_path, repo_name, config):
46 46 url = _vcs_streaming_url() + 'hg/'
47 return VcsHttpProxy(url, repo_path, repo_name, config)
47 return VcsHttpProxy(url, repo_path, repo_name, config, 'hg')
48 48
49 49
50 50 def _vcs_streaming_url():
51 51 template = 'http://{}/stream/'
52 52 return template.format(rhodecode.CONFIG['vcs.server'])
53 53
54 54
55 55 # TODO: johbo: Avoid the global.
56 56 session = requests.Session()
57 57 # Requests speedup, avoid reading .netrc and similar
58 58 session.trust_env = False
59 59
60 60
61 61 class VcsHttpProxy(object):
62 62 """
63 63 A WSGI application which proxies vcs requests.
64 64
65 65 The goal is to shuffle the data around without touching it. The only
66 66 exception is the extra data from the config object which we send to the
67 67 server as well.
68 68 """
69 69
70 def __init__(self, url, repo_path, repo_name, config):
70 def __init__(self, url, repo_path, repo_name, config, backend):
71 71 """
72 72 :param str url: The URL of the VCSServer to call.
73 73 """
74 74 self._url = url
75 75 self._repo_name = repo_name
76 76 self._repo_path = repo_path
77 77 self._config = config
78 self._backend = backend
78 79 log.debug(
79 80 "Creating VcsHttpProxy for repo %s, url %s",
80 81 repo_name, url)
81 82
82 83 def __call__(self, environ, start_response):
83 84 status = '200 OK'
84 85
85 86 config = msgpack.packb(self._config)
86 87 request = webob.request.Request(environ)
87 88 request_headers = request.headers
88 89 request_headers.update({
89 90 # TODO: johbo: Remove this, rely on URL path only
90 91 'X-RC-Repo-Name': self._repo_name,
91 92 'X-RC-Repo-Path': self._repo_path,
92 93 'X-RC-Path-Info': environ['PATH_INFO'],
93 94 # TODO: johbo: Avoid encoding and put this into payload?
94 95 'X-RC-Repo-Config': base64.b64encode(config),
95 96 })
96 97
97 98 data = environ['wsgi.input'].read()
98 99 method = environ['REQUEST_METHOD']
99 100
100 101 # Preserve the query string
101 102 url = self._url
102 103 url = urlparse.urljoin(url, self._repo_name)
103 104 if environ.get('QUERY_STRING'):
104 105 url += '?' + environ['QUERY_STRING']
105 106
106 107 response = session.request(
107 108 method, url,
108 109 data=data,
109 110 headers=request_headers,
110 111 stream=True)
111 112
112 113 # Preserve the headers of the response, except hop_by_hop ones
113 114 response_headers = [
114 115 (h, v) for h, v in response.headers.items()
115 116 if not wsgiref.util.is_hop_by_hop(h)
116 117 ]
117 118
119 # Add custom response header to indicate that this is a VCS response
120 # and which backend is used.
121 response_headers.append(('X-RhodeCode-Backend', self._backend))
122
118 123 # TODO: johbo: Better way to get the status including text?
119 124 status = str(response.status_code)
120 125 start_response(status, response_headers)
121 126 return _maybe_stream(response)
122 127
123 128
124 129 def _maybe_stream(response):
125 130 """
126 131 Try to generate chunks from the response if it is chunked.
127 132 """
128 133 if _is_chunked(response):
129 134 return response.raw.read_chunked()
130 135 else:
131 136 return [response.content]
132 137
133 138
134 139 def _is_chunked(response):
135 140 return response.headers.get('Transfer-Encoding', '') == 'chunked'
General Comments 0
You need to be logged in to leave comments. Login now