Show More
@@ -0,0 +1,160 b'' | |||||
|
1 | # RhodeCode VCSServer provides access to different vcs backends via network. | |||
|
2 | # Copyright (C) 2014-2020 RhodeCode GmbH | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or modify | |||
|
5 | # it under the terms of the GNU General Public License as published by | |||
|
6 | # the Free Software Foundation; either version 3 of the License, or | |||
|
7 | # (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
14 | # You should have received a copy of the GNU General Public License | |||
|
15 | # along with this program; if not, write to the Free Software Foundation, | |||
|
16 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | |||
|
17 | ||||
|
18 | import os | |||
|
19 | import tempfile | |||
|
20 | ||||
|
21 | from svn import client | |||
|
22 | from svn import core | |||
|
23 | from svn import ra | |||
|
24 | ||||
|
25 | from mercurial import error | |||
|
26 | ||||
|
27 | from vcsserver.utils import safe_bytes | |||
|
28 | ||||
|
29 | core.svn_config_ensure(None) | |||
|
30 | svn_config = core.svn_config_get_config(None) | |||
|
31 | ||||
|
32 | ||||
|
33 | class RaCallbacks(ra.Callbacks): | |||
|
34 | @staticmethod | |||
|
35 | def open_tmp_file(pool): # pragma: no cover | |||
|
36 | (fd, fn) = tempfile.mkstemp() | |||
|
37 | os.close(fd) | |||
|
38 | return fn | |||
|
39 | ||||
|
40 | @staticmethod | |||
|
41 | def get_client_string(pool): | |||
|
42 | return b'RhodeCode-subversion-url-checker' | |||
|
43 | ||||
|
44 | ||||
|
45 | class SubversionException(Exception): | |||
|
46 | pass | |||
|
47 | ||||
|
48 | ||||
|
49 | class SubversionConnectionException(SubversionException): | |||
|
50 | """Exception raised when a generic error occurs when connecting to a repository.""" | |||
|
51 | ||||
|
52 | ||||
|
53 | def normalize_url(url): | |||
|
54 | if not url: | |||
|
55 | return url | |||
|
56 | if url.startswith(b'svn+http://') or url.startswith(b'svn+https://'): | |||
|
57 | url = url[4:] | |||
|
58 | url = url.rstrip(b'/') | |||
|
59 | return url | |||
|
60 | ||||
|
61 | ||||
|
62 | def _create_auth_baton(pool): | |||
|
63 | """Create a Subversion authentication baton. """ | |||
|
64 | # Give the client context baton a suite of authentication | |||
|
65 | # providers.h | |||
|
66 | platform_specific = [ | |||
|
67 | 'svn_auth_get_gnome_keyring_simple_provider', | |||
|
68 | 'svn_auth_get_gnome_keyring_ssl_client_cert_pw_provider', | |||
|
69 | 'svn_auth_get_keychain_simple_provider', | |||
|
70 | 'svn_auth_get_keychain_ssl_client_cert_pw_provider', | |||
|
71 | 'svn_auth_get_kwallet_simple_provider', | |||
|
72 | 'svn_auth_get_kwallet_ssl_client_cert_pw_provider', | |||
|
73 | 'svn_auth_get_ssl_client_cert_file_provider', | |||
|
74 | 'svn_auth_get_windows_simple_provider', | |||
|
75 | 'svn_auth_get_windows_ssl_server_trust_provider', | |||
|
76 | ] | |||
|
77 | ||||
|
78 | providers = [] | |||
|
79 | ||||
|
80 | for p in platform_specific: | |||
|
81 | if getattr(core, p, None) is not None: | |||
|
82 | try: | |||
|
83 | providers.append(getattr(core, p)()) | |||
|
84 | except RuntimeError: | |||
|
85 | pass | |||
|
86 | ||||
|
87 | providers += [ | |||
|
88 | client.get_simple_provider(), | |||
|
89 | client.get_username_provider(), | |||
|
90 | client.get_ssl_client_cert_file_provider(), | |||
|
91 | client.get_ssl_client_cert_pw_file_provider(), | |||
|
92 | client.get_ssl_server_trust_file_provider(), | |||
|
93 | ] | |||
|
94 | ||||
|
95 | return core.svn_auth_open(providers, pool) | |||
|
96 | ||||
|
97 | ||||
|
98 | class SubversionRepo(object): | |||
|
99 | """Wrapper for a Subversion repository. | |||
|
100 | ||||
|
101 | It uses the SWIG Python bindings, see above for requirements. | |||
|
102 | """ | |||
|
103 | def __init__(self, svn_url: bytes = b'', username: bytes = b'', password: bytes = b''): | |||
|
104 | ||||
|
105 | self.username = username | |||
|
106 | self.password = password | |||
|
107 | self.svn_url = core.svn_path_canonicalize(svn_url) | |||
|
108 | ||||
|
109 | self.auth_baton_pool = core.Pool() | |||
|
110 | self.auth_baton = _create_auth_baton(self.auth_baton_pool) | |||
|
111 | # self.init_ra_and_client() assumes that a pool already exists | |||
|
112 | self.pool = core.Pool() | |||
|
113 | ||||
|
114 | self.ra = self.init_ra_and_client() | |||
|
115 | self.uuid = ra.get_uuid(self.ra, self.pool) | |||
|
116 | ||||
|
117 | def init_ra_and_client(self): | |||
|
118 | """Initializes the RA and client layers, because sometimes getting | |||
|
119 | unified diffs runs the remote server out of open files. | |||
|
120 | """ | |||
|
121 | ||||
|
122 | if self.username: | |||
|
123 | core.svn_auth_set_parameter(self.auth_baton, | |||
|
124 | core.SVN_AUTH_PARAM_DEFAULT_USERNAME, | |||
|
125 | self.username) | |||
|
126 | if self.password: | |||
|
127 | core.svn_auth_set_parameter(self.auth_baton, | |||
|
128 | core.SVN_AUTH_PARAM_DEFAULT_PASSWORD, | |||
|
129 | self.password) | |||
|
130 | ||||
|
131 | callbacks = RaCallbacks() | |||
|
132 | callbacks.auth_baton = self.auth_baton | |||
|
133 | ||||
|
134 | try: | |||
|
135 | return ra.open2(self.svn_url, callbacks, svn_config, self.pool) | |||
|
136 | except SubversionException as e: | |||
|
137 | # e.child contains a detailed error messages | |||
|
138 | msglist = [] | |||
|
139 | svn_exc = e | |||
|
140 | while svn_exc: | |||
|
141 | if svn_exc.args[0]: | |||
|
142 | msglist.append(svn_exc.args[0]) | |||
|
143 | svn_exc = svn_exc.child | |||
|
144 | msg = '\n'.join(msglist) | |||
|
145 | raise SubversionConnectionException(msg) | |||
|
146 | ||||
|
147 | ||||
|
148 | class svnremoterepo(object): | |||
|
149 | """ the dumb wrapper for actual Subversion repositories """ | |||
|
150 | ||||
|
151 | def __init__(self, username: bytes = b'', password: bytes = b'', svn_url: bytes = b''): | |||
|
152 | self.username = username or b'' | |||
|
153 | self.password = password or b'' | |||
|
154 | self.path = normalize_url(svn_url) | |||
|
155 | ||||
|
156 | def svn(self): | |||
|
157 | try: | |||
|
158 | return SubversionRepo(self.path, self.username, self.password) | |||
|
159 | except SubversionConnectionException as e: | |||
|
160 | raise error.Abort(safe_bytes(e)) |
@@ -7,7 +7,6 b' dogpile.cache==1.1.8' | |||||
7 |
|
7 | |||
8 | decorator==5.1.1 |
|
8 | decorator==5.1.1 | |
9 | dulwich==0.21.3 |
|
9 | dulwich==0.21.3 | |
10 | hgsubversion==1.9.3 |
|
|||
11 | hg-evolve==11.0.0 |
|
10 | hg-evolve==11.0.0 | |
12 |
|
11 | |||
13 | mercurial==6.3.2 |
|
12 | mercurial==6.3.2 |
@@ -23,7 +23,9 b' import urllib.parse' | |||||
23 | import logging |
|
23 | import logging | |
24 | import posixpath as vcspath |
|
24 | import posixpath as vcspath | |
25 | import io |
|
25 | import io | |
26 |
import urllib.request |
|
26 | import urllib.request | |
|
27 | import urllib.parse | |||
|
28 | import urllib.error | |||
27 | import traceback |
|
29 | import traceback | |
28 |
|
30 | |||
29 | import svn.client |
|
31 | import svn.client | |
@@ -38,7 +40,7 b' from vcsserver.base import RepoFactory, ' | |||||
38 | from vcsserver.exceptions import NoContentException |
|
40 | from vcsserver.exceptions import NoContentException | |
39 | from vcsserver.utils import safe_str |
|
41 | from vcsserver.utils import safe_str | |
40 | from vcsserver.vcs_base import RemoteBase |
|
42 | from vcsserver.vcs_base import RemoteBase | |
41 |
|
43 | from vcsserver.lib.svnremoterepo import svnremoterepo | ||
42 | log = logging.getLogger(__name__) |
|
44 | log = logging.getLogger(__name__) | |
43 |
|
45 | |||
44 |
|
46 | |||
@@ -50,7 +52,7 b' svn_compatible_versions_map = {' | |||||
50 | 'pre-1.9-compatible': '1.8', |
|
52 | 'pre-1.9-compatible': '1.8', | |
51 | } |
|
53 | } | |
52 |
|
54 | |||
53 |
current_compatible_version = '1.1 |
|
55 | current_compatible_version = '1.14' | |
54 |
|
56 | |||
55 |
|
57 | |||
56 | def reraise_safe_exceptions(func): |
|
58 | def reraise_safe_exceptions(func): | |
@@ -105,9 +107,6 b' class SvnRemote(RemoteBase):' | |||||
105 |
|
107 | |||
106 | def __init__(self, factory, hg_factory=None): |
|
108 | def __init__(self, factory, hg_factory=None): | |
107 | self._factory = factory |
|
109 | self._factory = factory | |
108 | # TODO: Remove once we do not use internal Mercurial objects anymore |
|
|||
109 | # for subversion |
|
|||
110 | self._hg_factory = hg_factory |
|
|||
111 |
|
110 | |||
112 | @reraise_safe_exceptions |
|
111 | @reraise_safe_exceptions | |
113 | def discover_svn_version(self): |
|
112 | def discover_svn_version(self): | |
@@ -127,15 +126,13 b' class SvnRemote(RemoteBase):' | |||||
127 | log.exception("failed to read object_store") |
|
126 | log.exception("failed to read object_store") | |
128 | return False |
|
127 | return False | |
129 |
|
128 | |||
130 |
def check_url(self, url |
|
129 | def check_url(self, url): | |
131 | # this can throw exception if not installed, but we detect this |
|
|||
132 | from hgsubversion import svnrepo |
|
|||
133 |
|
130 | |||
134 | baseui = self._hg_factory._create_config(config_items) |
|
|||
135 | # uuid function get's only valid UUID from proper repo, else |
|
131 | # uuid function get's only valid UUID from proper repo, else | |
136 | # throws exception |
|
132 | # throws exception | |
|
133 | username, password, src_url = self.get_url_and_credentials(url) | |||
137 | try: |
|
134 | try: | |
138 |
|
|
135 | svnremoterepo(username, password, src_url).svn().uuid | |
139 | except Exception: |
|
136 | except Exception: | |
140 | tb = traceback.format_exc() |
|
137 | tb = traceback.format_exc() | |
141 | log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb) |
|
138 | log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb) | |
@@ -359,6 +356,7 b' class SvnRemote(RemoteBase):' | |||||
359 |
|
356 | |||
360 | cache_on, context_uid, repo_id = self._cache_on(wire) |
|
357 | cache_on, context_uid, repo_id = self._cache_on(wire) | |
361 | region = self._region(wire) |
|
358 | region = self._region(wire) | |
|
359 | ||||
362 | @region.conditional_cache_on_arguments(condition=cache_on) |
|
360 | @region.conditional_cache_on_arguments(condition=cache_on) | |
363 | def _get_file_size(_repo_id, _path, _revision): |
|
361 | def _get_file_size(_repo_id, _path, _revision): | |
364 | repo = self._factory.repo(wire) |
|
362 | repo = self._factory.repo(wire) |
@@ -37,60 +37,57 b' def safe_int(val, default=None):' | |||||
37 | return val |
|
37 | return val | |
38 |
|
38 | |||
39 |
|
39 | |||
40 |
def safe_str( |
|
40 | def safe_str(str_, to_encoding=None) -> str: | |
41 | """ |
|
41 | """ | |
42 | safe str function. Does few trick to turn unicode_ into string |
|
42 | safe str function. Does few trick to turn unicode_ into string | |
43 |
|
43 | |||
44 |
:param |
|
44 | :param str_: str to encode | |
45 | :param to_encoding: encode to this type UTF8 default |
|
45 | :param to_encoding: encode to this type UTF8 default | |
46 | :rtype: str |
|
46 | :rtype: str | |
47 | :returns: str object |
|
47 | :returns: str object | |
48 | """ |
|
48 | """ | |
|
49 | if isinstance(str_, str): | |||
|
50 | return str_ | |||
|
51 | ||||
|
52 | # if it's bytes cast to str | |||
|
53 | if not isinstance(str_, bytes): | |||
|
54 | return str(str_) | |||
|
55 | ||||
49 | to_encoding = to_encoding or ['utf8'] |
|
56 | to_encoding = to_encoding or ['utf8'] | |
50 | # if it's not basestr cast to str |
|
|||
51 | if not isinstance(unicode_, basestring): |
|
|||
52 | return str(unicode_) |
|
|||
53 |
|
||||
54 | if isinstance(unicode_, str): |
|
|||
55 | return unicode_ |
|
|||
56 |
|
||||
57 | if not isinstance(to_encoding, (list, tuple)): |
|
57 | if not isinstance(to_encoding, (list, tuple)): | |
58 | to_encoding = [to_encoding] |
|
58 | to_encoding = [to_encoding] | |
59 |
|
59 | |||
60 | for enc in to_encoding: |
|
60 | for enc in to_encoding: | |
61 | try: |
|
61 | try: | |
62 |
return |
|
62 | return str(str_, enc) | |
63 |
except Unicode |
|
63 | except UnicodeDecodeError: | |
64 | pass |
|
64 | pass | |
65 |
|
65 | |||
66 |
return |
|
66 | return str(str_, to_encoding[0], 'replace') | |
67 |
|
67 | |||
68 |
|
68 | |||
69 |
def safe_ |
|
69 | def safe_bytes(str_, from_encoding=None) -> bytes: | |
70 | """ |
|
70 | """ | |
71 |
safe |
|
71 | safe bytes function. Does few trick to turn str_ into bytes string: | |
72 |
|
72 | |||
73 | :param str_: string to decode |
|
73 | :param str_: string to decode | |
74 | :param from_encoding: encode from this type UTF8 default |
|
74 | :param from_encoding: encode from this type UTF8 default | |
75 | :rtype: unicode |
|
75 | :rtype: unicode | |
76 | :returns: unicode object |
|
76 | :returns: unicode object | |
77 | """ |
|
77 | """ | |
78 | from_encoding = from_encoding or ['utf8'] |
|
78 | if isinstance(str_, bytes): | |
79 |
|
||||
80 | if isinstance(str_, unicode): |
|
|||
81 | return str_ |
|
79 | return str_ | |
82 |
|
80 | |||
|
81 | if not isinstance(str_, str): | |||
|
82 | raise ValueError('safe_bytes cannot convert other types than str: got: {}'.format(type(str_))) | |||
|
83 | ||||
|
84 | from_encoding = from_encoding or ['utf8'] | |||
83 | if not isinstance(from_encoding, (list, tuple)): |
|
85 | if not isinstance(from_encoding, (list, tuple)): | |
84 | from_encoding = [from_encoding] |
|
86 | from_encoding = [from_encoding] | |
85 |
|
87 | |||
86 | try: |
|
|||
87 | return unicode(str_) |
|
|||
88 | except UnicodeDecodeError: |
|
|||
89 | pass |
|
|||
90 |
|
||||
91 | for enc in from_encoding: |
|
88 | for enc in from_encoding: | |
92 | try: |
|
89 | try: | |
93 |
return |
|
90 | return str_.encode(enc) | |
94 | except UnicodeDecodeError: |
|
91 | except UnicodeDecodeError: | |
95 | pass |
|
92 | pass | |
96 |
|
93 |
General Comments 0
You need to be logged in to leave comments.
Login now