Любительский протокол spartan. Спецификация [перевод]

В этот раз мы решили подготовить уникальный для русского сегмента Сети материал про малоизвестный, новый и интересный протокол 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, смотрите сами:

Официальная страница spartan, работающая по протоколу spartan

Ссылки

UPD: Еще ссылки, на статьи которые после выхода этой публикации

          />_________S_P_A_R_T_A_N_://_________
 [########[]___________________________________>
          \>        Established 650 BC.

aquaratixc

Программист-самоучка и программист-любитель

Добавить комментарий