В этот раз мы решили подготовить уникальный для русского сегмента Сети материал про малоизвестный, новый и интересный протокол spartan. На описание протокола мы наткнулись в gemini и описание, увы, выполнено на английском, а в русскоязычной части Интернета поиск описания затруднен интерференцией наименования протокола с одноименными околокриптовалютными технологиями или FPGA от фирмы Xilinx. Поэтому, мы решили выполнить перевод официальной спецификации spartan, и хотя в ней ничего нет про язык программирования D, но мы вам обещаем, что эта статья только начало…
Далее мы приведем полноценный текст спецификации и некоторых других, связанных с ней, материалов с небольшими примечаниями и вставками от нашей команды.
1. Обзор протокола
spartan:// — это клиент-серверный протокол, разработанный для любителей. Spartan опирается на идеи gemini, gopher и http, чтобы создать что-то новое, но при этом знакомое. Протокол стремится быть простым, веселым и вдохновляющим.
Spartan отправляет незашифрованные текстовые запросы в кодировке ASCII по протоколу TCP. Произвольный текст и двоичные файлы поддерживаются как для загрузки, так и для скачивания. Как и в gemini,типом «гипертекстового документа» по умолчанию в spartan является text/gemini. Для запроса ввода используется специальный тип строки (=:). В протоколе есть четыре кода состояния: «успех» (success), «перенаправление» (redirect), «ошибка сервера» (server-error) и «ошибка клиента» (client-error).
Примечание переводчика. Как пишет сам автор протокола, spartan стремится быть простым, знакомым и веселым. Сам протокол не накладывает никаких ограничений на использование, а также не ставит своей целью что-либо заменить. Также автор говорит о том, что минимализм, следование философии Unix, а также приватность и эффективность, не были целями при создании протокола и потому решаются в каждой отдельной реализации по своему.
2. Запросы
Запрос в spartan представляет собой одну строку запроса в кодировке ASCII, за которой следует необязательный блок данных.
запрос = строка_запроса [блок_данных] строка_запроса = хост SP абсолютный_путь SP длина_блока_данных CRLF
Компонент хост указывает хост сервера, на который отправляется запрос. Номер порта не должен быть включен в указание хоста. Хосты, содержащие не-ASCII-символы (IDN), должны быть преобразованы в punycode.
Компонент абсолютный_путь указывает запрашиваемый ресурс. Он должен быть абсолютным (absolute path) и начинаться с символа /.
Компонент блок_данных может использоваться клиентом для загрузки произвольных данных на сервер. Компонент длина_блока_данных определяет длину блока данных в байтах. Длина содержимого 0 означает, что к запросу не будут прилагаться дополнительные данные.
Формат загружаемых данных определяется сервером на основе окружающего контекста. Он может содержать обычный текст, двоичные данные или смешанную кодировку.
2.1 Примеры запросов spartan
# Загрузка текстового файла example.com /files/about.txt 0 # Отправить сообщение в гостевую книгу example.com /guestbook/submit 12 Hello world! # Загрузка аудио-файла example.com /upload/africa.mp3 3145728 <binary data stream...>
3. Ответы
Ответ в spartan представляет собой одну строку состояния в кодировке ASCII, за которой следует необязательное тело ответа.
В протоколе используются однозначные (т.е состоящие всего из одной цифры) коды состояния для обозначения успеха или неудачи:
ответ = успех / перенаправление / ошибка_клиента / ошибка_сервера успех = '2' SP тип_mime CRLF тело_ответа перенаправление = '3' SP абсолютный_путь CRLF ошибка_клиента = '4' SP сообщение_об_ошибке CRLF ошибка_сервера = '5' SP сообщение_об_ошибке CRLF
«2»: Успех (Success)
Статус «2» означает, что ресурс был успешно получен, понят и принят. Компонент тип_mime должен содержать MIME документа ответа. Он также может включать параметр charset для указания кодировки документа. Кодировкой по умолчанию для документов text/* следует считать UTF-8.
«3»: Перенаправление (Redirect)
Статус «3» указывает на то, что ресурс находится в другом месте. Клиент должен сделать новый запрос по указанному абсолютному пути. Перенаправления могут быть указаны только на тот же хост, что и исходный запрос.
«4»: Ошибка клиента (Client Error)
Статус «4» указывает на то, что запрос содержит неправильный синтаксис или не может быть выполнен. Компонент сообщение_об_ошибке должен содержать удобочитаемое описание ошибки.
«5»: Ошибка сервера (Server error)
Статус «5» указывает, что сервер не может выполнить, в принципе, допустимый запрос. Компонент сообщение_об_ошибке должен содержать удобочитаемое описание ошибки.
3.1 Примеры ответов в spartan
# Успех 2 text/plain; charset=utf-8 In a hole in the ground there lived a hobbit. Not a nasty, dirty, wet hole, filled with the ends of worms and an oozy smell, nor yet a dry, bare... # Перенаправление 3 /new/path/ # Ошибка клиента 4 File "/files/meaning_of_life.txt" not found # Ошибка сервера 5 Server is experiencing heavy load
4. Формат документа
В Spartan определен один дополнительный нестандартный тип строки для поддержки загрузки данных:
=:[<пробел>]<URL>[<пробел><человекочитаемый текст ссылки>]
Строку подсказки следует рассматривать так же, как строку ссылки => в gemini, за исключением того, что клиент должен предложить пользователю определить данные для включения в качестве секции блок_данных перед отправкой запроса. Пользовательский интерфейс для ввода данных не является обязательным и строго заданным, но некоторые возможные варианты форм могут включать в себя виджет текстового поля или виджет выбора файлов.
Это устраняет необходимость в коде статуса ответа gemini 10 INPUT и обеспечивает большую гибкость для авторов документов, предоставляя эквивалент тега <form> в HTML или полнотекстового поиска с типом 7 в gopher.
5. URL
Адреса URL в spartan имеют ту же структуру что и URL в HTTP:
scheme://пользователь@хост:порт/путь;параметры?запрос#фрагмент
По умолчанию, порт для spartan имеет номер 300, что является отсылкой к битве при Фермопилах, когда 300 спартанских солдат выступили против вторгшейся персидской армии.
Компонент пользователь разрешен, но не имеет особого значения. Компоненты хост и порт должны использоваться для установления соединения, но в строку запроса должен быть включен только компонент хост.
Предполагается, что компонент пути использует иерархическую форму с проходимыми (иерархическими) сегментами пути. Как и в HTTP, путь / эквивалентен пустому пути. Элементы компонента путь с символами Unicode или небезопасными символами должны быть представлены в формате %-кодированного запроса. Компонент фрагмент разрешен, но не имеет специального значения.
Компонент запрос должен быть %-кодирован и использоваться в качестве блока данных для запроса. Элементы компонента параметры (пары «ключ=значение») особого значения не имеют.
5.1 Привязка URL к запросам spartan
URL Запрос --- ------- spartan://example.com "example.com / 0" spartan://example.com/ "example.com / 0" spartan://example.com:3000/ "example.com / 0" spartan://example.com/#about "example.com / 0" spartan://anon@example.com/ "example.com / 0" spartan://127.0.0.1/ "127.0.0.1 / 0" spartan://[::1]/ "[::1] / 0" spartan://examplé.com/ "xn--exampl-dma.com / 0" spartan://example.com/my%20file.txt "example.com /my%20file.txt 0" spartan://example.com/café.txt "example.com /caf%C3%A9.txt 0" spartan://example.com?a=1&b=2 "example.com / 7 a=1&b=2" spartan://example.com?hello%20world "example.com / 11 hello world"
Приложение А. Полная BNF-грамматика spartan
запрос = строка_запроса [блок_данных] строка_запроса = хост SP путь SP длина_блока_данных CRLF ответ = успех / перенаправление / ошибка_клиента / ошибка_сервера успех = '2' SP тип_mime CRLF тело_ответа перенаправление = '3' SP абсолютный_путь CRLF ошибка_клиента = '4' SP сообщение_об_ошибке CRLF ошибка_сервера = '5' SP сообщение_об_ошибке CRLF длина_блока_данных = 1*DIGIT блок_данных = *OCTET тип_mime = тип '/' подтип *(';' параметр) тело_ответа = *OCTET сообщение_об_ошибке = 1*(WSP / VCHAR) ; хост из RFC 3986 ; абсолютный_путь из RFC 3986, за исключением пустой строки "" ; тип из RFC 2045 ; подтип из RFC 2045 ; параметр из RFC 2045 ; CRLF из RFC 5234 ; DIGIT из RFC 5234 ; OCTET из RFC 5234 ; SP из RFC 5234 ; WSP из RFC 5234 ; VCHAR из RFC 5234
Приложение B. О происхождении наименования протокола.
Название является отсылкой к альма-матер создателя — Мичиганскому Государственному Университету и его талисману — сражающимся спартанцам. Это и дань уважения протоколу gopher://, который также был назван в честь талисмана — суслика Университета Миннесоты из Большой Десятки Университетов.
Примечание переводчика. Судя по всему, здесь автор протокола Майкл Лазар (Michael Lazar) упоминает свой родной университет.
В греческой мифологии созвездие Близнецов связано с близнецами Кастором и Полидевком (Поллуксом). Полидевк был сыном Зевса, а Кастор — сыном Тиндарея, царя Спарты. Когда Кастор умер (а он был рожден смертным, в отличие от своего брата), Полидевк умолял своего отца Зевса даровать Кастору бессмертие, что он и сделал, объединив их вместе на небесах.
Примечание переводчика. Вообще тут довольно все любопытно. В плане созвездия Близнецов здесь идет достаточно толстый намек на протокол gemini (так как, gemini — это буквально близнецы, по английски) и некоторые утилиты для gemini используют в своих именованиях легенду о Касторе и Полидевке (к примеру, есть сервер gemini на Rust с наименованием pollux — латинизированной версией имени одного из близнецов). Но самое занимательное, что эти два персонажа были родом из Спарты, на что автор протокола хитро намекает во второй версии.
Выбирайте из этих двух версий, любой вариант.
Слово spartan также имеет следующее значение в общеупотребительном английском языке:
Из словаря:
spartan
прилаг.: отмеченный строгой самодисциплиной или самоотречением.
прилаг.: отмеченный простотой, бережливостью или избеганием роскоши и комфорта.
Примечание переводчика. Вспоминаем, значение прилагательного «спартанский» в русском языке…
Но эти определения не имеют никакого отношения к протоколу. Просто совпадение.
Официальный логотип эмодзи (emoji) для протокола spartan — согнутые бицепсы (💪).
Приложение C. История изменений в документе.
2021-03-24 Публикация начальной версии. ### 2021-07-11 Переписана секция обзор протокола. ### 2022-07-19 Выполнен перевод данной спецификации ### 2022-09-04 Исправлены мелкие ошибки в переводе и примечаниях
На этом спецификация заканчивается, но… в дополнение к этому на странице протокола spartan://spartan.mozz.us/ есть ряд материалов, не входящих в спецификацию, и при этом неплохо ее дополняющих. Мы решили добавить их далее, поскольку они фактически важны для создания собственных реализаций клиентов/серверов spartan.
Эталонная реализация сервера spartan
Выполнена на python 3 и выглядит следующим образом:
#!/usr/bin/env python3 """ A reference spartan:// protocol server. Copyright (c) Michael Lazar Blue Oak Model License 1.0.0 """ import argparse from datetime import datetime import mimetypes import os import pathlib import shutil from socketserver import ThreadingTCPServer, StreamRequestHandler from urllib.parse import unquote class SpartanRequestHandler(StreamRequestHandler): def handle(self): try: self._handle() except ValueError as e: self.write_status(4, e) except Exception: self.write_status(5, "An unexpected error has occurred") raise def _handle(self): request = self.rfile.readline(4096) request = request.decode("ascii").strip("\r\n") print(f'{datetime.now().isoformat()} "{request}"') hostname, path, content_length = request.split(" ") if not path: raise ValueError("Not Found") path = unquote(path) # Guard against breaking out of the directory safe_path = os.path.normpath(path.strip("/")) if safe_path.startswith(("..", "/")): raise ValueError("Not Found") filepath = root / safe_path if filepath.is_file(): self.write_file(filepath) elif filepath.is_dir(): if not path.endswith("/"): # Redirect to canonical path with trailing slash self.write_status(3, f"{path}/") elif (filepath / "index.gmi").is_file(): self.write_file(filepath / "index.gmi") else: self.write_status(2, "text/gemini") self.write_line("=>..") for child in filepath.iterdir(): if child.is_dir(): self.write_line(f"=>{child.name}/") else: self.write_line(f"=>{child.name}") else: raise ValueError("Not Found") def write_file(self, filepath): mimetype, encoding = mimetypes.guess_type(filepath, strict=False) mimetype = mimetype or "application/octet-stream" with filepath.open("rb") as fp: self.write_status(2, mimetype) shutil.copyfileobj(fp, self.wfile) def write_line(self, text): self.wfile.write(f"{text}\n".encode("utf-8")) def write_status(self, code, meta): self.wfile.write(f"{code} {meta}\r\n".encode("ascii")) mimetypes.add_type("text/gemini", ".gmi") parser = argparse.ArgumentParser(description="A spartan static file server") parser.add_argument("dir", default=".", nargs="?", type=pathlib.Path) parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", default=3000, type=int) args = parser.parse_args() root = args.dir.resolve(strict=True) print(f"Root Directory {root}") server = ThreadingTCPServer((args.host, args.port), SpartanRequestHandler) print(f"Listening on {server.server_address}") try: server.serve_forever() except KeyboardInterrupt: pass
Эталонная реализация клиента spartan
Также выполнена на python 3:
#!/usr/bin/env python3 """ A reference spartan:// protocol client. Copyright (c) Michael Lazar Blue Oak Model License 1.0.0 """ import argparse import shutil import socket import sys from urllib.parse import urlparse, unquote_to_bytes, quote_from_bytes def fetch_url(url, infile=None): url_parts = urlparse(url) if url_parts.scheme != "spartan": raise ValueError("Unrecognized URL scheme") host = url_parts.hostname port = url_parts.port or 300 path = url_parts.path or "/" query = url_parts.query redirect_url = None with socket.create_connection((host, port)) as sock: if infile: data = infile.read() elif query: data = unquote_to_bytes(query) else: data = b"" encoded_host = host.encode("idna") encoded_path = quote_from_bytes(unquote_to_bytes(path)).encode("ascii") sock.send(b"%s %s %d\r\n" % (encoded_host, encoded_path, len(data))) sock.send(data) fp = sock.makefile("rb") response = fp.readline(4096).decode("ascii").strip("\r\n") print(response, file=sys.stderr, flush=True) parts = response.split(" ", maxsplit=1) code, meta = int(parts[0]), parts[1] if code == 2: shutil.copyfileobj(fp, sys.stdout.buffer) elif code == 3: redirect_url = url_parts._replace(path=meta).geturl() if redirect_url: fetch_url(redirect_url) parser = argparse.ArgumentParser(description="A spartan client") parser.add_argument("url") parser.add_argument("--infile", type=argparse.FileType("rb")) args = parser.parse_args() try: fetch_url(args.url, args.infile) except ValueError as e: print(f"Error: {e}", file=sys.stderr) except KeyboardInterrupt: pass
P.S: А где взять нормальный клиент ? Внезапно, упоминавшийся нами в статье про gemini браузер Lagrange (есть кстати и на мобильные платформы) успешно и давно работает со spartan, смотрите сами:
Ссылки
UPD: Еще ссылки, на статьи которые после выхода этой публикации
/>_________S_P_A_R_T_A_N_://_________ [########[]___________________________________> \> Established 650 BC.