Работа с secure shell; Tkinter: текстовый редактор
Git и ssh-ключи
Старый, но актуальный материал из ПСПО про ssh
Secure shell:
- терминальное подключение
не обязательно shell!
- шифрование трафика
- аутентификация
Асимметричное шифрование:
Два ключа
Шифрование происходит одним любым ключом, дешифрование — другим
- ⇒ Делаем один ключ секретным, другой — публичным
Раздаём публичный ключ всем
Не даём секретный ключ никому (случаев, когда требуется передача/копирование секретного ключа, нет)
- Проблема первоначального обмена ключами: удостовериться, что то, что ты получил и есть то, что тебе послали (aka «man in the middle attack»)
Аутентичность: кто-то шифрует своим секретным ключом
расшифровать публичным ключом отправителя могут все
- вероятность, что зашифрованное сообщение создал кто-то другой, очень низкая (фактически, только утечка ключа)
Шифрование: кто-то шифрует публичным ключом получателя
расшифровать своим секретным ключом может только получатель
- вероятность расшифровать со стороны — утечка ключа либо взлом самого криптоалгоритма
Сгенерить пару ключей
1 $ ssh-keygen -t ed25519 -f tmpkey
2 Generating public/private ed25519 key pair.
3 Enter passphrase (empty for no passphrase):
4 Enter same passphrase again:
5 Your identification has been saved in tmpkey.
6 Your public key has been saved in tmpkey.pub.
7 The key fingerprint is:
8 SHA256:2ylIy4YEDwM7Xwg8UkspUJ8lYaa10PFd3otVHedJR0k george@inspiron
9 The key's randomart image is:
10 +--[ED25519 256]--+
11 |*o=oBo. . oE*|
12 |oB.X.* . o . ..o=|
13 |+.O = . . . o ..|
14 | o * o . |
15 | . o . S . . |
16 | . + o o . |
17 | . = o o |
18 | . . |
19 | |
20 +----[SHA256]-----+
21 $ ls tmpkey*
22 tmpkey tmpkey.pub
23 $ ssh-keygen -v -l -f tmpkey.pub
24 256 SHA256:2ylIy4YEDwM7Xwg8UkspUJ8lYaa10PFd3otVHedJR0k george@inspiron (ED25519)
25 +--[ED25519 256]--+
26 |*o=oBo. . oE*|
27 |oB.X.* . o . ..o=|
28 |+.O = . . . o ..|
29 | o * o . |
30 | . o . S . . |
31 | . + o o . |
32 | . = o o |
33 | . . |
34 | |
35 +----[SHA256]-----+
36
За красивую картинку отвечает настройка «VisualHostKey yes» в файле $HOME/.ssh/config. Это визуализация отпечатка ключа (сам ключ может быть достаточно большим), а отпечаток фиксированного размера, его можно продиктовать или переслать фото в мессенджере, если всерьёз опасаешься MitM-атаки.
Запуск и пополнение агента
Агент — это программа, которая хранит в памяти распароленные закрытые ключи и предлагает удалённой машине использовать соответствующий открытый ключ для авторизации доступа.
Запуск:
1 $ ssh-add -l
2 Error connecting to agent: No such file or directory
3 $ ssh-agent
4 SSH_AUTH_SOCK=/tmp/.private/george/ssh-MAVKXDo58vzi/agent.2667630; export SSH_AUTH_SOCK;
5 SSH_AGENT_PID=2667631; export SSH_AGENT_PID;
6 echo Agent pid 2667631;
7 $ SSH_AUTH_SOCK=/tmp/.private/george/ssh-MAVKXDo58vzi/agent.2667630; export SSH_AUTH_SOCK;
8 $ ssh-add -l
9 The agent has no identities.
10
- Первая команда показывает, что агент недоступен
Третья команда — это просто копипаста выдачи агента
⇒ весь запуск можно сократить до eval $(ssh-agent)
В большинстве систем ssh-agent или его аналог уже запущены
Добавление ключа:
Настройка доступа по ключу
Если ключ не зарегистрирован, доступ будет только парольным (когда разрешён)
1 george@inspiron ~$ ssh user@host-241.local
2 user@host-241.local's password:
3 Last login: Thu Mar 18 15:01:54 2021 from 192.168.122.1
4 user@host-241$ mkdir repo
5 user@host-241 ~$ cd repo
6 user@host-241 repo$ git-init
7 Initialized empty Git repository in /home/user/repo/.git/
8 user@host-241 repo$ cal > calend
9 user@host-241 repo$ git add .
10 user@host-241 repo$ git commit -a -m initial
11 [master (root-commit) ab3b91d] initial
12 1 file changed, 8 insertions(+)
13 create mode 100644 calend
14 user@host-241 repo$ выход
15 Connection to host-241.local closed.
16 george@inspiron ~$ git clone user@host-241.local:repo
17 Cloning into 'repo'...
18 user@host-241.local's password:
19 remote: Enumerating objects: 3, done.
20 remote: Counting objects: 100% (3/3), done.
21 remote: Compressing objects: 100% (2/2), done.
22 remote: Total 3 (delta 0), reused 0 (delta 0)
23 Receiving objects: 100% (3/3), done.
24 george@inspiron ~$ cd repo
25 george@inspiron repo$ git pull
26 user@host-241.local's password:
27 Already up to date.
28
Регистрация ключа совсем вручную:
1 george@inspiron ~$ ssh-add tmpkey.pub
2 Enter passphrase for tmpkey.pub:
3 Identity added: tmpkey.pub (george@inspiron)
4 george@inspiron ~$ ssh-add -L
5 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJI/ZphlcwJOg23Q6U0b1pXxZC3LFcjYuE9ggIfz9bUs george@inspiron
6 george@inspiron: ~$ ssh user@host-241.local
7 user@host-241.local's password:
8 Last login: Thu Mar 18 15:40:34 2021 from 192.168.122.1
9 user@host-241 ~$ echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJI/ZphlcwJOg23Q6U0b1pXxZC3LFcjYuE9ggIfz9bUs george@inspiron" >> .ssh/authorized_keys
10 user@host-241 ~$ выход
11 Connection to host-241.local closed.
12 george@inspiron ~$ ssh user@host-241.local
13 Last login: Thu Mar 18 15:41:44 2021 from 192.168.122.1
14 user@host-241 ~$
- Доступ теперь беспарольный!
- На самом деле мы же вводили пароль к самому ключу
Имеется скрипт ssh-copy-id <удалённая_машина>, который делает то же самое, но сам)
Создание репозитория
Работа с репозиторием тоже беспарольная:
1 george@inspiron ~$ rm -rf repo
2 george@inspiron ~$ git clone user@host-241.local:repo
3 Cloning into 'repo'...
4 remote: Enumerating objects: 3, done.
5 remote: Counting objects: 100% (3/3), done.
6 remote: Compressing objects: 100% (2/2), done.
7 remote: Total 3 (delta 0), reused 0 (delta 0)
8 Receiving objects: 100% (3/3), done.
9 george@inspiron ~$ cd repo
10 george@inspiron ~/repo$ cat .git/config
11 [core]
12 repositoryformatversion = 0
13 filemode = true
14 bare = false
15 logallrefupdates = true
16 [remote "origin"]
17 url = user@host-241.local:repo
18 fetch = +refs/heads/*:refs/remotes/origin/*
19 [branch "master"]
20 remote = origin
21 merge = refs/heads/master
22 george@inspiron ~/repo$ git pull
23 Already up to date.
24
Однако поскольку удалённый репозиторий имеет свою собственную рабочую копию, возможны конфликты если обе копии изменятся. Что делать с рабочей копией удалённого репозитория при push (ответ «ничего» — наихудший, догадайтесь, почему)?
Надо было заводить удалённый репозиторий с помощью git init --bare
Тогда на удалённой машине непосредственно в репозитории работать нельзя нельзя (но clone/push/pull делать можно)
- Именно так работают разные git-движки
Заставить git перебивать содержимое удалённой рабочей копии актуальным содержимым ветки, на которой она развёрнута
- Да, если при этом там кто-то работает (другая копия вас, ага), у него будут проблемы
1 george@inspiron ~/repo$ date >> calend 2 george@inspiron ~/repo$ git commit -a -m "Next commit" 3 [master c790bf1] Next commit 4 1 file changed, 1 insertion(+) 5 george@inspiron ~/repo$ git push 6 Enumerating objects: 5, done. 7 Counting objects: 100% (5/5), done. 8 Delta compression using up to 12 threads 9 Compressing objects: 100% (2/2), done. 10 Writing objects: 100% (3/3), 321 bytes | 321.00 KiB/s, done. 11 Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 12 remote: error: refusing to update checked out branch: refs/heads/master 13 remote: error: By default, updating the current branch in a non-bare repository 14 remote: is denied, because it will make the index and work tree inconsistent 15 remote: with what you pushed, and will require 'git reset --hard' to match 16 remote: the work tree to HEAD. 17 remote: 18 remote: You can set the 'receive.denyCurrentBranch' configuration variable 19 remote: to 'ignore' or 'warn' in the remote repository to allow pushing into 20 remote: its current branch; however, this is not recommended unless you 21 remote: arranged to update its work tree to match what you pushed in some 22 remote: other way. 23 remote: 24 remote: To squelch this message and still keep the default behaviour, set 25 remote: 'receive.denyCurrentBranch' configuration variable to 'refuse'. 26 To host-241.local:repo 27 ! [remote rejected] master -> master (branch is currently checked out) 28 error: failed to push some refs to 'host-241.local:repo' 29 george@inspiron ~/repo$ ssh user@host-241.local 30 Last login: Thu Mar 18 15:42:00 2021 from 192.168.122.1 31 user@host-241 ~$ cd repo/ 32 user@host-241 repo$ git config receive.denyCurrentBranch ignore 33 user@host-241 repo$ cat .git/config 34 [core] 35 repositoryformatversion = 0 36 filemode = true 37 bare = false 38 logallrefupdates = true 39 [receive] 40 denyCurrentBranch = ignore 41 user@host-241 repo$ выход 42 Connection to host-241.local closed. 43 george@inspiron ~/repo$ git push 44 Enumerating objects: 5, done. 45 Counting objects: 100% (5/5), done. 46 Delta compression using up to 12 threads 47 Compressing objects: 100% (2/2), done. 48 Writing objects: 100% (3/3), 321 bytes | 321.00 KiB/s, done. 49 Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 50 To host-241.local:repo 51 ab3b91d..c790bf1 master -> master 52 george@inspiron ~/repo$
- Да, если при этом там кто-то работает (другая копия вас, ага), у него будут проблемы
Ключи и git-порталы
Именно такие ключи требуют от вас порталы типа SourceHut, GitHub, GitLab и многие другие
Никогда и никуда не копируйте закрытые ключи! Кроме вас они нужны только злоумышленникам!
Tkinter: Text
Text — довольно развесистый текстовый редактор, упрятанный в один Tk-виджет
Координаты (индексы, «indices») — способ указать определённый символ в тексте по его местоположению
Строки вида "строка.столбец" (начиная с 1.0)
"строка.end" — конец строки
"@x,y" — экранные координаты в пикселях, пересчитанные в «строка.столбец»
"end" — конец всего текста
дополнительные модификаторы вида "+ 1 chars", "- 2 lines" и т. п.
- … (см по ссылке ещё примеры)
.get(), .index(),
.insert(), .delete()
- …
Метки (маркеры, «marks») — отметка в тексте, имеющая единственную координату
Именованные ( методы .mark_set(), .mark_unset(), .mark_names() и т. п.
- Специальные
"insert" — координаты текстового курсора
"current" — координаты указателя мыши (пересчитанные в «строка.столбец»)
Границы выделенного текста "sel.first" / 'sel.last'
- …
.mark_gravity() задаёт, что случается с меткой, когда текст изменяется
- …
Теги («tags») — текст меджу двумя маркерами
Именованные (см группу методов ыида .tag_*())
Специальные (tk.SEL — тег выделенного текста)
- Несколько областей текста могут быть помечены одним и тем же тегом
- В том числе тегом выделения, который SEL
- Теги могут вкладываться и пересекаться (у Tk есть понятие глубины тега)
Для областей, выделенных тегами можно задавать собственный свойства (см. метод .tag_config())
…и даже собственные обработчики событий (.tag_bind())
- Поддерживается скроллинг (но соответствующий виджет надо добавлять)
- (ещё прямо внутри текста могут быть изображения и окна)
- …
Пример
1 import tkinter as tk
2 import inspect
3
4 class Application(tk.Frame):
5 '''Sample tkinter application class'''
6
7 def __init__(self, master=None, title="<application>", **kwargs):
8 '''Create root window with frame, tune weight and resize'''
9 super().__init__(master, **kwargs)
10 self.master.title(title)
11 self.master.columnconfigure(0, weight=1)
12 self.master.rowconfigure(0, weight=1)
13 self.grid(sticky="NEWS")
14 self.create_widgets()
15 for column in range(self.grid_size()[0]):
16 self.columnconfigure(column, weight=1)
17 for row in range(self.grid_size()[1]):
18 self.rowconfigure(row, weight=1)
19
20 def create_widgets(self):
21 '''Create all the widgets'''
22
23 class App(Application):
24 def create_widgets(self):
25 self.T = tk.Text(self, undo=True, wrap=tk.WORD, font="fixed",
26 inactiveselectbackground="MidnightBlue")
27 self.mytext()
28 self.T.grid(row=0, column=0, sticky="NEWS")
29 self.B = tk.LabelFrame(self, text="Controls")
30 self.B.grid(row=1, column=0, sticky="NEWS")
31 self.LS = tk.StringVar()
32 self.IS = tk.StringVar()
33 self.I = tk.Entry(self.B, textvariable=self.IS)
34 self.S = tk.Button(self.B, text="Go", command=self.setI)
35 self.MF = tk.Button(self.B, text="From",
36 command=lambda: self.T.mark_set("From", tk.INSERT))
37 self.MT = tk.Button(self.B, text="To",
38 command=lambda: self.T.mark_set("To", tk.INSERT))
39 self.G = tk.Button(self.B, text="Generate", command=self.mytext)
40 self.Q = tk.Button(self.B, text="Quit", command=self.master.quit)
41 self.L = tk.Label(self.B, textvariable=self.LS)
42 for O in self.I, self.S, self.MF, self.MT, self.G, self.Q, self.L:
43 O.grid(row=0, column=self.B.grid_size()[0])
44
45 self.T.bind("<Motion>", self.showmarks)
46 self.T.tag_config("keyword", foreground="limegreen")
47 self.T.tag_bind("keyword", "<Motion>", print)
48
49 def showmarks(self, event):
50 self.LS.set(f"[{self.T.index('From')}:{self.T.index('To')}] {self.T.index('current')}")
51
52 def setI(self):
53 beg, *end = self.IS.get().split(":")
54 try:
55 self.T.mark_set(tk.INSERT, beg)
56 if end:
57 self.T.tag_add(tk.SEL, beg, end[0])
58 self.T.focus_force()
59 except Exception as E:
60 print(E)
61
62 def mytext(self):
63 self.T.delete(1.0, tk.END)
64 mytext = open(inspect.getsourcefile(type(self))).read()
65 self.T.insert("1.0", mytext)
66 self.T.mark_set("From", "1.0")
67 self.T.mark_set("To", "end")
68 pos, res = 0.0, []
69 while pos := self.T.search("self", f"{pos} + 1 chars", stopindex=tk.END):
70 res.append(pos)
71 for i0 in res:
72 self.T.tag_add("keyword", i0, f"{i0} + 4 chars")
73
74 app = App(title="Sample application")
75 app.mainloop()
Пояснения к примеру:
- Изначально загружен текст самой программы)
Если слева внизу в окне ввода возле «Go» ввести индекс и нажать «Go» , курсор переместится на указанную позицию
Если вместо индекса ввести индекс:индекс, курсор переместится на первую позицию и текст выделится от ней до второй позиции
В качестве индекса можно вводить любые имеющиеся маркеры (например, "end"), а также два заданных маркера — From и To
Например, если ввести From:end, текст выделится от маркера From до конца текста
При нажатии кнопок From и To соответствующий маркер переставляется в позицию, на которую указывается текстовый курсор (автоматический маркер insert)
В правом нижнем углу выводится текущее состояние этих маркеров, к также позиция курсора мыши (автоматический маркер current)
При считывании файла (кнопка «Generate») происходит поиск всех подстрок «"self"» в тексте и каждая такая подстрока помечается тегом keyword)
Третий параметр метода T.search() — stopindex — задаёт место, после которого поиск останавливается (иначе цикл оказался бы вечным: поиск после конца продолжается сначала)
Изначально текстовое окно настроено так, что области, помеченные тегом keyword
- Выводятся светло-зелёным шрифтом
Обрабатывают событие <Motion>
- (цвета адаптированы для моих (зелёно-коричневых) настроек, возможно, для стандартной темы нужно выбрать другие ☺)
Tkinter: Canvas и turtle
Не успели! Ну и ладно, можно и самому разобраться.
Д/З
- Прочитать в справочнике
Про text
Про canvas (самостоятельное изучение)
См. план семинара по этой теме с указанием того, что именно нужно посмотреть в Canvas
- (если ещё не) Сгенерировать и аккуратно сохранить пару открытый/закрытый ключ и научиться пользоваться ей для доступа к отчётному репозиторию Git
- Написать вырожденный графический редактор. Минимальные требования
- Имеет два окна — текстовое и графическое
- В графическом окне:
- Нажатие мышью в пустое место и движение с нажатой кнопкой создаёт овал и меняет его размер
- Нажатие на существующий овал и движение с нажатой кнопкой перемещает этот овал
- В текстовом окне параллельно формируется описание картинки на некотором языке. Синтаксис языка придумайте сами, можно взять из семинара.
- Обязаны присутствовать координаты, ширина границы, цвет границы и цвет заливки
При изменении текста
- Если в нём нет ошибок, рисуются все фигуры
- Если ошибки есть, ошибочные строчки раскрашиваются в красный цвет
- (подумайте, как сделать так, чтобы индикация не мешала редактированию)
- Допустимо для обновления одного окна из другого использовать две кнопки
- Остальное — необязательно.
В зарегистрированном вами репозитории создать подкаталог 05_SshAndSmartWidgents (совпадает с финальной частью URL данной лекции), всё, что вы написали, положить туда