Skip to content

Commit 6392029

Browse files
authored
Merge branch 'master' into reuse-ssh-connection
2 parents d9c604d + 05c87d8 commit 6392029

25 files changed

+521
-158
lines changed

.github/workflows/ci.yml

+11-3
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@ on:
77

88
jobs:
99
linux:
10-
11-
runs-on: ubuntu-latest
12-
1310
strategy:
1411
matrix:
1512
python-version: [3.6, 3.7, 3.8, 3.9]
13+
include:
14+
- python-version: 3.6
15+
os: ubuntu-16.04 # MySQL 5.7.32
16+
- python-version: 3.7
17+
os: ubuntu-18.04 # MySQL 5.7.32
18+
- python-version: 3.8
19+
os: ubuntu-18.04 # MySQL 5.7.32
20+
- python-version: 3.9
21+
os: ubuntu-20.04 # MySQL 8.0.22
1622

23+
runs-on: ${{ matrix.os }}
1724
steps:
1825

1926
- uses: actions/checkout@v2
@@ -42,6 +49,7 @@ jobs:
4249
- name: Pytest / behave
4350
env:
4451
PYTEST_PASSWORD: root
52+
PYTEST_HOST: 127.0.0.1
4553
run: |
4654
./setup.py test --pytest-args="--cov-report= --cov=mycli"
4755

README.md

+18-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# mycli
22

3-
[![Build Status](https://travis-ci.org/dbcli/mycli.svg?branch=master)](https://travis-ci.org/dbcli/mycli)
3+
[![Build Status](https://github.com/dbcli/mycli/workflows/mycli/badge.svg)](https://github.com/dbcli/mycli/actions?query=workflow%3Amycli)
44
[![PyPI](https://img.shields.io/pypi/v/mycli.svg?style=plastic)](https://pypi.python.org/pypi/mycli)
5-
[![Join the chat at https://gitter.im/dbcli/mycli](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dbcli/mycli?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
65

76
A command line client for MySQL that can do auto-completion and syntax highlighting.
87

@@ -53,6 +52,7 @@ $ sudo apt-get install mycli # Only on debian or ubuntu
5352
-h, --host TEXT Host address of the database.
5453
-P, --port INTEGER Port number to use for connection. Honors
5554
$MYSQL_TCP_PORT.
55+
5656
-u, --user TEXT User name to connect to the database.
5757
-S, --socket TEXT The socket file to use for connection.
5858
-p, --password TEXT Password to connect to the database.
@@ -63,8 +63,11 @@ $ sudo apt-get install mycli # Only on debian or ubuntu
6363
--ssh-password TEXT Password to connect to ssh server.
6464
--ssh-key-filename TEXT Private key filename (identify file) for the
6565
ssh connection.
66+
6667
--ssh-config-path TEXT Path to ssh configuration.
67-
--ssh-config-host TEXT Host for ssh server in ssh configurations (requires paramiko).
68+
--ssh-config-host TEXT Host to connect to ssh server reading from ssh
69+
configuration.
70+
6871
--ssl-ca PATH CA file in PEM format.
6972
--ssl-capath TEXT CA directory.
7073
--ssl-cert PATH X509 cert in PEM format.
@@ -73,33 +76,43 @@ $ sudo apt-get install mycli # Only on debian or ubuntu
7376
--ssl-verify-server-cert Verify server's "Common Name" in its cert
7477
against hostname used when connecting. This
7578
option is disabled by default.
79+
7680
-V, --version Output mycli's version.
7781
-v, --verbose Verbose output.
7882
-D, --database TEXT Database to use.
7983
-d, --dsn TEXT Use DSN configured into the [alias_dsn]
8084
section of myclirc file.
85+
8186
--list-dsn list of DSN configured into the [alias_dsn]
8287
section of myclirc file.
83-
--list-ssh-config list ssh configurations in the ssh config (requires paramiko).
88+
89+
--list-ssh-config list ssh configurations in the ssh config
90+
(requires paramiko).
91+
8492
-R, --prompt TEXT Prompt format (Default: "\t \u@\h:\d> ").
8593
-l, --logfile FILENAME Log every query and its results to a file.
8694
--defaults-group-suffix TEXT Read MySQL config groups with the specified
8795
suffix.
96+
8897
--defaults-file PATH Only read MySQL options from the given file.
8998
--myclirc PATH Location of myclirc file.
9099
--auto-vertical-output Automatically switch to vertical output mode
91100
if the result is wider than the terminal
92101
width.
102+
93103
-t, --table Display batch output in table format.
94104
--csv Display batch output in CSV format.
95105
--warn / --no-warn Warn before running a destructive query.
96106
--local-infile BOOLEAN Enable/disable LOAD DATA LOCAL INFILE.
97-
--login-path TEXT Read this path from the login file.
107+
-g, --login-path TEXT Read this path from the login file.
98108
-e, --execute TEXT Execute command and quit.
99109
--init-command TEXT SQL statement to execute after connecting.
100110
--charset TEXT Character set for MySQL session.
111+
--password-file PATH File or FIFO path containing the password
112+
to connect to the db if not specified otherwise
101113
--help Show this message and exit.
102114

115+
103116
Features
104117
--------
105118

changelog.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
TBD
2-
=======
2+
===
33

44
Bug Fixes:
55
----------
66
* Allow `FileNotFound` exception for SSH config files.
7+
* Fix startup error on MySQL < 5.0.22
8+
* Check error code rather than message for Access Denied error
9+
* Fix login with ~/.my.cnf files
710

811
Features:
912
---------
1013
* Add `-g` shortcut to option `--login-path`.
14+
* Alt-Enter dispatches the command in multi-line mode.
15+
* Allow to pass a file or FIFO path with --password-file when password is not specified or is failing (as suggested in this best-practice https://www.netmeister.org/blog/passing-passwords.html)
1116
* Reuse the same SSH connection for both main thread and completion thread.
1217

18+
Internal:
19+
---------
20+
* Remove unused function is_open_quote()
21+
* Use importlib, instead of file links, to locate resources
22+
* Test various host-port combinations in command line arguments
23+
* Switched from Cryptography to pyaes for decrypting mylogin.cnf
24+
1325

1426
1.23.2
1527
===

mycli/AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ Contributors:
8484
* xeron
8585
* 0xflotus
8686
* Seamile
87+
* Jerome Provensal
8788

8889
Creator:
8990
--------

mycli/clibuffer.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from prompt_toolkit.enums import DEFAULT_BUFFER
22
from prompt_toolkit.filters import Condition
33
from prompt_toolkit.application import get_app
4-
from .packages.parseutils import is_open_quote
54
from .packages import special
65

76

mycli/config.py

+80-22
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
import io
2-
import shutil
31
from copy import copy
42
from io import BytesIO, TextIOWrapper
53
import logging
64
import os
75
from os.path import exists
86
import struct
97
import sys
10-
from typing import Union
8+
from typing import Union, IO
119

1210
from configobj import ConfigObj, ConfigObjError
13-
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
14-
from cryptography.hazmat.backends import default_backend
11+
import pyaes
12+
13+
try:
14+
import importlib.resources as resources
15+
except ImportError:
16+
# Python < 3.7
17+
import importlib_resources as resources
1518

1619
try:
1720
basestring
@@ -49,9 +52,9 @@ def read_config_file(f, list_values=True):
4952
config = ConfigObj(f, interpolation=False, encoding='utf8',
5053
list_values=list_values)
5154
except ConfigObjError as e:
52-
log(logger, logging.ERROR, "Unable to parse line {0} of config file "
55+
log(logger, logging.WARNING, "Unable to parse line {0} of config file "
5356
"'{1}'.".format(e.line_number, f))
54-
log(logger, logging.ERROR, "Using successfully parsed config values.")
57+
log(logger, logging.WARNING, "Using successfully parsed config values.")
5558
return e.config
5659
except (IOError, OSError) as e:
5760
log(logger, logging.WARNING, "You don't have permission to read "
@@ -61,7 +64,7 @@ def read_config_file(f, list_values=True):
6164
return config
6265

6366

64-
def get_included_configs(config_file: Union[str, io.TextIOWrapper]) -> list:
67+
def get_included_configs(config_file: Union[str, TextIOWrapper]) -> list:
6568
"""Get a list of configuration files that are included into config_path
6669
with !includedir directive.
6770
@@ -95,7 +98,7 @@ def get_included_configs(config_file: Union[str, io.TextIOWrapper]) -> list:
9598
def read_config_files(files, list_values=True):
9699
"""Read and merge a list of config files."""
97100

98-
config = ConfigObj(list_values=list_values)
101+
config = create_default_config(list_values=list_values)
99102
_files = copy(files)
100103
while _files:
101104
_file = _files.pop(0)
@@ -112,12 +115,21 @@ def read_config_files(files, list_values=True):
112115
return config
113116

114117

115-
def write_default_config(source, destination, overwrite=False):
118+
def create_default_config(list_values=True):
119+
import mycli
120+
default_config_file = resources.open_text(mycli, 'myclirc')
121+
return read_config_file(default_config_file, list_values=list_values)
122+
123+
124+
def write_default_config(destination, overwrite=False):
125+
import mycli
126+
default_config = resources.read_text(mycli, 'myclirc')
116127
destination = os.path.expanduser(destination)
117128
if not overwrite and exists(destination):
118129
return
119130

120-
shutil.copyfile(source, destination)
131+
with open(destination, 'w') as f:
132+
f.write(default_config)
121133

122134

123135
def get_mylogin_cnf_path():
@@ -160,6 +172,58 @@ def open_mylogin_cnf(name):
160172
return TextIOWrapper(plaintext)
161173

162174

175+
# TODO reuse code between encryption an decryption
176+
def encrypt_mylogin_cnf(plaintext: IO[str]):
177+
"""Encryption of .mylogin.cnf file, analogous to calling
178+
mysql_config_editor.
179+
180+
Code is based on the python implementation by Kristian Koehntopp
181+
https://github.com/isotopp/mysql-config-coder
182+
183+
"""
184+
def realkey(key):
185+
"""Create the AES key from the login key."""
186+
rkey = bytearray(16)
187+
for i in range(len(key)):
188+
rkey[i % 16] ^= key[i]
189+
return bytes(rkey)
190+
191+
def encode_line(plaintext, real_key, buf_len):
192+
aes = pyaes.AESModeOfOperationECB(real_key)
193+
text_len = len(plaintext)
194+
pad_len = buf_len - text_len
195+
pad_chr = bytes(chr(pad_len), "utf8")
196+
plaintext = plaintext.encode() + pad_chr * pad_len
197+
encrypted_text = b''.join(
198+
[aes.encrypt(plaintext[i: i + 16])
199+
for i in range(0, len(plaintext), 16)]
200+
)
201+
return encrypted_text
202+
203+
LOGIN_KEY_LENGTH = 20
204+
key = os.urandom(LOGIN_KEY_LENGTH)
205+
real_key = realkey(key)
206+
207+
outfile = BytesIO()
208+
209+
outfile.write(struct.pack("i", 0))
210+
outfile.write(key)
211+
212+
while True:
213+
line = plaintext.readline()
214+
if not line:
215+
break
216+
real_len = len(line)
217+
pad_len = (int(real_len / 16) + 1) * 16
218+
219+
outfile.write(struct.pack("i", pad_len))
220+
x = encode_line(line, real_key, pad_len)
221+
outfile.write(x)
222+
223+
outfile.seek(0)
224+
return outfile
225+
226+
163227
def read_and_decrypt_mylogin_cnf(f):
164228
"""Read and decrypt the contents of .mylogin.cnf.
165229
@@ -201,11 +265,9 @@ def read_and_decrypt_mylogin_cnf(f):
201265
return None
202266
rkey = struct.pack('16B', *rkey)
203267

204-
# Create a decryptor object using the key.
205-
decryptor = _get_decryptor(rkey)
206-
207268
# Create a bytes buffer to hold the plaintext.
208269
plaintext = BytesIO()
270+
aes = pyaes.AESModeOfOperationECB(rkey)
209271

210272
while True:
211273
# Read the length of the ciphertext.
@@ -216,7 +278,10 @@ def read_and_decrypt_mylogin_cnf(f):
216278

217279
# Read cipher_len bytes from the file and decrypt.
218280
cipher = f.read(cipher_len)
219-
plain = _remove_pad(decryptor.update(cipher))
281+
plain = _remove_pad(
282+
b''.join([aes.decrypt(cipher[i: i + 16])
283+
for i in range(0, cipher_len, 16)])
284+
)
220285
if plain is False:
221286
continue
222287
plaintext.write(plain)
@@ -260,15 +325,8 @@ def strip_matching_quotes(s):
260325
return s
261326

262327

263-
def _get_decryptor(key):
264-
"""Get the AES decryptor."""
265-
c = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())
266-
return c.decryptor()
267-
268-
269328
def _remove_pad(line):
270329
"""Remove the pad from the *line*."""
271-
pad_length = ord(line[-1:])
272330
try:
273331
# Determine pad length.
274332
pad_length = ord(line[-1:])

mycli/key_bindings.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,12 @@ def _(event):
7878

7979
@kb.add('escape', 'enter')
8080
def _(event):
81-
"""Introduces a line break regardless of multi-line mode or not."""
81+
"""Introduces a line break in multi-line mode, or dispatches the
82+
command in single-line mode."""
8283
_logger.debug('Detected alt-enter key.')
83-
event.app.current_buffer.insert_text('\n')
84+
if mycli.multi_line:
85+
event.app.current_buffer.validate_and_handle()
86+
else:
87+
event.app.current_buffer.insert_text('\n')
8488

8589
return kb

0 commit comments

Comments
 (0)