The Guppy Protocol Specification v0.4.1

(I don't know if anyone is interested in this, but let's give it a try. It started as a thought experiment, when I looked for 'lighter' protocols I can implement on the Pico W. I see Spartan mentioned here and there, and I wonder if there's any interest in going even more ... spartan. If you find this interesting, useful or fun, and have ideas how to improve this protocol, I'd love to hear from you at my-first-name@dimakrasner.com!)

Overview

Guppy is a simple unencrypted client-to-server protocol, for download of text and text-based interfaces that require upload of short input. It uses UDP and inspired by TFTP, DNS and Spartan. The goal is to design a simple, text-based protocol without Gopher's limitations that is easy to implement and can be used to host a "guplog" even on a microcontroller (like a Raspberry Pi Pico W, ESP32 or ESP8266) and serve multiple requests using a single UDP socket.

Requests are always sent as a single packet, while responses can be chunked and each chunk must be acknowledged by the client. The protocol is designed for short-lived sessions that transfer small text files, therefore it doesn't allow failed downloads to be resumed, and doesn't allow upload of big chunks of data.

Implementers can choose their preferred complexity vs. speed ratio. Out-of-order transmission of chunked responses should allow extremely fast transfer of small textual documents, especially if the network is reliable. However, this requires extra code complexity, memory and bandwidth in both clients and servers. Simple implementations can achieve slow but reliable TFTP-like transfers with minimal amounts of code. Out-of-order transmission doesn't matter much if the server is a blog containing small posts (that fit in one or two chunks) and the client is smart enough to display the beginning of the response while receiving the next chunks.

Changelog

v0.4.1:

v0.4:

v0.3.2:

(Response to warning by conman)

v0.3.1:

(Response to feedback from tjp)

v0.3:

v0.2:

(Response to feedback from slondr)

v0.1:

Examples

Success - Single Packet

If the URL is guppy://localhost/a and the response is "# Title 1\n":

	> guppy://localhost/a\r\n (request)
	< 566837578 text/gemini\r\n# Title 1\n (response)
	> 566837578\r\n (acknowledgment)
	< 566837579\r\n (end-of-file)
	> 566837579\r\n (acknowledgment)

Success - Single Packet with User Input

If the URL is guppy://localhost/a and input is "b c":

	> guppy://localhost/a?b%20c\r\n
	< 566837578 text/gemini\r\n# Title 1\n
	> 566837578\r\n
	< 566837579\r\n
	> 566837579\r\n

Success - Multiple Packets

If the URL is guppy://localhost/a and the response is "# Title 1\nParagraph 1\n":

	> guppy://localhost/a\r\n
	< 566837578 text/gemini\r\n# Title 1\n
	> 566837578\r\n
	< 566837579\r\nParagraph 1
	> 566837579\r\n
	< 566837580\r\n\n
	> 566837580\r\n
	< 566837581\r\n
	> 566837581\r\n

Success - Multiple Packets With Out of Order Packets

If the URL is guppy://localhost/a and the response is "# Title 1\nParagraph 1\n":

	> guppy://localhost/a\r\n
	< 566837578 text/gemini\r\n# Title 1\n
	< 566837579\r\nParagraph 1
	> 566837578\r\n
	< 566837579\r\nParagraph 1
	> 566837579\r\n
	< 566837579\r\nParagraph 1
	< 566837580\r\n\n
	> 566837580\r\n
	< 566837581\r\n
	> 566837581\r\n

Success - Simple Client, Sophisticated Server

If the URL is guppy://localhost/a and the response is "# Title 1\nParagraph 1\n":

	> guppy://localhost/a\r\n
	< 566837578 text/gemini\r\n# Title 1\n
	< 566837579\r\nParagraph 1 (server sends packet 566837579 without waiting for the client to acknowledge 566837578)
	> 566837578\r\n
	< 566837579\r\nParagraph 1 (server sends packet 566837579 again because the client didn't acknowledge it)
	< 566837580\r\n\n (server sends packet 566837580 without waiting for the client to acknowledge 566837579)
	> 566837579\r\n
	< 566837580\r\n\n (server sends packet 566837580 again because the client didn't acknowledge it)
	< 566837581\r\n (server sends packet 566837581 without waiting for the client to acknowledge 566837580)
	> 566837580\r\n
	< 566837581\r\n (server sends packet 566837581 again because the client didn't acknowledge it)
	> 566837581\r\n

Success - Multiple Packets With Unreliable Network

If the URL is guppy://localhost/a and the response is "# Title 1\nParagraph 1\n":

	> guppy://localhost/a\r\n
	< 566837578 text/gemini\r\n# Title 1\n
	> 566837578\r\n
	< 566837578 text/gemini\r\n# Title 1\n (acknowledgement arrived after the server re-transmitted the success packet)
	< 566837579\r\nParagraph 1
	< 566837579\r\nParagraph 1 (first continuation packet was lost)
	> 566837579\r\n
	< 566837580\r\n\n
	> 566837580\r\n
	> 566837580\r\n (first acknowledgement packet was lost and the client re-transmitted it while waiting for a continuation or EOF packet)
	< 566837581\r\n (server sends EOF after receiving the re-transmitted acknowledgement packet)
	< 566837581\r\n (first EOF packet was lost while server waits for client to acknowledge EOF)
	> 566837581\r\n

Input prompt

	> guppy://localhost/greet\r\n
	< 1 Your name\r\n
	> guppy://localhost/greet?Guppy\r\n
	< 566837578 text/gemini\r\nHello Guppy\n
	> 566837578\r\n
	< 566837579\r\n
	> 566837579\r\n

Redirect - Absolute URL

	> guppy://localhost/a\r\n
	< 3 guppy://localhost/b\r\n

Redirect - Relative URL

	> guppy://localhost/a\r\n
	< 3 /b\r\n

Error

	> guppy://localhost/search\r\n
	< 4 No search keywords specified\r\n

Sample Implementation

Python client with support for out-of-order packets:

	#!/usr/bin/python3

	import socket
	import sys
	from urllib.parse import urlparse
	import select

	s = socket.socket(type=socket.SOCK_DGRAM)
	url = urlparse(sys.argv[1])
	s.connect((url.hostname, 6775))

	request = (sys.argv[1] + "\r\n").encode('utf-8')

	sys.stderr.write(f"Sending request for {sys.argv[1]}\n")
	s.send(request)

	buffered = b''
	mime_type = None
	tries = 0
	last_buffered = 0
	chunks = {}
	while True:
		ready, _, _ = select.select([s.fileno()], [], [], 2)

		# if we still haven't received anything from the server, retry the request
		if len(chunks) == 0 and not ready:
			if tries > 5:
				raise Exception("All 5 tries have failed")

			sys.stderr.write(f"Retrying request for {sys.argv[1]}\n")
			s.send(request)
			tries += 1
			continue

		# if we're waiting for packet n+1, retry ack packet n
		if not ready and last_buffered > 0:
			sys.stderr.write(f"Retrying ack for packet {last_buffered}\n")
			s.send(f"{last_buffered}\r\n".encode('utf-8'))
			continue

		# receive and parse the next packet
		pkt = s.recv(4096)
		crlf = pkt.index(b'\r\n')
		header = pkt[:crlf]

		try:
			# parse the success packet header
			space = header.index(b' ')
			seq = int(header[:space])
			mime_type = header[space + 1:]

			if seq == 4:
				raise Exception(f"Error: {mime_type.decode('utf-8')}")

			if seq == 3:
				raise Exception(f"Redirected to {mime_type.decode('utf-8')}")

			if seq == 1:
				raise Exception(f"Input required: {mime_type.decode('utf-8')}")

			if seq < 6:
				raise Exception(f"Invalid status code: {seq}")
		except ValueError as e:
			# parse the continuation or EOF packet header
			seq = int(header)

		if seq in chunks:
			sys.stderr.write(f"Ignoring duplicate packet {seq} and resending ack\n")
			s.send(f"{seq}\r\n".encode('utf-8'))
			continue

		if last_buffered == 0 and mime_type is not None:
			sys.stderr.write(f"Response is of type {mime_type.decode('utf-8')}\n")

		sys.stderr.write(f"Sending ack for packet {seq}\n")
		s.send(f"{seq}\r\n".encode('utf-8'))

		data = pkt[crlf + 2:]
		if last_buffered == 0 or seq == last_buffered + 1:
			sys.stderr.write(f"Received packet {seq} with {len(data)} bytes of data\n")
		else:
			sys.stderr.write(f"Received out-of-order packet {seq} with {len(data)} bytes of data\n")

		chunks[seq] = data

		# concatenate the consequentive response chunks we have
		while (last_buffered == 0 and mime_type is not None) or seq == last_buffered + 1:
			data = chunks[seq]
			sys.stderr.write(f"Queueing packet {seq} for display\n")
			buffered += data
			last_buffered = seq

		# print the buffered text if we can
		try:
			print(buffered.decode('utf-8'))
			last_buffered = seq
			sys.stderr.write("Flushed the buffer to screen\n")
			buffered = b''
		except UnicodeDecodeError:
			sys.stderr.write("Cannot print buffered text until valid UTF-8\n")
			continue

		# stop once we printed everything until the end-of-file packet
		if not chunks[last_buffered]:
			sys.stderr.write("Reached end of document\n")
			break

(Slightly) simpler Python client that ignores out-of-order packets:

	#!/usr/bin/python3

	import socket
	import sys
	from urllib.parse import urlparse
	import select

	s = socket.socket(type=socket.SOCK_DGRAM)
	url = urlparse(sys.argv[1])
	s.connect((url.hostname, 6775))

	request = (sys.argv[1] + "\r\n").encode('utf-8')

	sys.stderr.write(f"Sending request for {sys.argv[1]}\n")
	s.send(request)

	buffered = b''
	mime_type = None
	tries = 0
	last_buffered = 0
	while True:
		ready, _, _ = select.select([s.fileno()], [], [], 2)

		# if we still haven't received anything from the server, retry the request
		if last_buffered == 0 and not ready:
			if tries > 5:
				raise Exception("All 5 tries have failed")

			sys.stderr.write(f"Retrying request for {sys.argv[1]}\n")
			s.send(request)
			tries += 1
			continue

		# if we're waiting for packet n+1, retry ack packet n
		if not ready and last_buffered > 0:
			sys.stderr.write(f"Retrying ack for packet {last_buffered}\n")
			s.send(f"{last_buffered}\r\n".encode('utf-8'))
			continue

		# receive and parse the next packet
		pkt = s.recv(4096)
		crlf = pkt.index(b'\r\n')
		header = pkt[:crlf]

		try:
			# parse the success packet header
			space = header.index(b' ')
			seq = int(header[:space])
			mime_type = header[space + 1:]

			if seq == 4:
				raise Exception(f"Error: {mime_type.decode('utf-8')}")

			if seq == 3:
				raise Exception(f"Redirected to {mime_type.decode('utf-8')}")

			if seq == 1:
				raise Exception(f"Input required: {mime_type.decode('utf-8')}")

			if seq < 6:
				raise Exception(f"Invalid status code: {seq}")
		except ValueError as e:
			# parse the continuation or EOF packet header
			seq = int(header)

		# ignore this packet if it's not the packet we're waiting for: packet n+1 or the first packet
		if (last_buffered != 0 and seq != last_buffered + 1) or (last_buffered == 0 and mime_type is None):
			sys.stderr.write(f"Ignoring unexpected packet {seq} and sending ack\n")
			s.send(f"{seq}\r\n".encode('utf-8'))
			continue

		if last_buffered == 0 and mime_type is not None:
			sys.stderr.write(f"Response is of type {mime_type.decode('utf-8')}\n")

		sys.stderr.write(f"Sending ack for packet {seq}\n")
		s.send(f"{seq}\r\n".encode('utf-8'))

		data = pkt[crlf + 2:]
		sys.stderr.write(f"Received packet {seq} with {len(data)} bytes of data\n")

		# concatenate the consequentive response chunks we have
		sys.stderr.write(f"Queueing packet {seq} for display\n")
		buffered += data
		last_buffered = seq

		# print the buffered text if we can
		try:
			print(buffered.decode('utf-8'))
			sys.stderr.write("Flushed the buffer to screen\n")
			buffered = b''
		except UnicodeDecodeError:
			sys.stderr.write("Cannot print buffered text until valid UTF-8\n")
			continue

		# stop once we printed everything until the end-of-file packet
		if not data:
			sys.stderr.write("Reached end of document\n")
			break

To use these clients, save to guppyc.py and:

python3 guppyc.py guppy://hd.206267.xyz/stats
python3 guppyc.py guppy://hd.206267.xyz/federated

Python server with support for out-of-order packets:

	#!/usr/bin/python3

	import socket
	from urllib.parse import urlparse, unquote
	import select
	import random
	import io
	import time
	import sys
	import logging
	import collections

	home = """# Home

	=> /lorem Lorem ipsum
	=> /echo Echo
	=> /rick.mp4 Rick Astley - Never Gonna Give You Up
	"""

	lorem_ipsum = """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pharetra diam sit amet nisl suscipit adipiscing bibendum est ultricies. Et tortor at risus viverra adipiscing at in tellus integer. Est ante in nibh mauris cursus mattis molestie. At varius vel pharetra vel turpis nunc. Consectetur adipiscing elit pellentesque habitant morbi tristique. Eu scelerisque felis imperdiet proin fermentum leo vel. At tempor commodo ullamcorper a. At augue eget arcu dictum varius duis at consectetur. Condimentum mattis pellentesque id nibh tortor id. Lorem ipsum dolor sit amet. Enim sed faucibus turpis in eu. Aenean sed adipiscing diam donec adipiscing tristique risus nec. Rutrum quisque non tellus orci ac auctor augue mauris augue. Posuere lorem ipsum dolor sit. Egestas erat imperdiet sed euismod nisi porta lorem mollis aliquam.

	Mi proin sed libero enim sed. Risus nec feugiat in fermentum posuere urna nec. Leo vel orci porta non pulvinar neque laoreet. Nisl purus in mollis nunc. Ipsum consequat nisl vel pretium. Sit amet tellus cras adipiscing enim eu turpis egestas. Nunc mattis enim ut tellus elementum sagittis vitae. Quis enim lobortis scelerisque fermentum dui faucibus. Fringilla est ullamcorper eget nulla. Viverra nibh cras pulvinar mattis nunc sed blandit. Placerat orci nulla pellentesque dignissim. Habitant morbi tristique senectus et netus et malesuada. Id eu nisl nunc mi ipsum faucibus vitae aliquet.

	Ultricies mi eget mauris pharetra et ultrices neque. Felis donec et odio pellentesque. Mauris vitae ultricies leo integer. A pellentesque sit amet porttitor eget dolor morbi. Et ligula ullamcorper malesuada proin libero nunc consequat. Donec massa sapien faucibus et molestie. Elementum tempus egestas sed sed. Ut sem viverra aliquet eget sit. Aliquam eleifend mi in nulla posuere sollicitudin. Et leo duis ut diam quam. Duis at consectetur lorem donec massa sapien faucibus et molestie. Sit amet venenatis urna cursus eget nunc scelerisque viverra mauris. Risus commodo viverra maecenas accumsan lacus. Vestibulum lectus mauris ultrices eros in cursus turpis massa. Purus sit amet volutpat consequat mauris nunc congue nisi. Enim nunc faucibus a pellentesque sit amet porttitor eget dolor. Auctor urna nunc id cursus metus aliquam eleifend.

	Ante in nibh mauris cursus mattis molestie a iaculis at. Quam pellentesque nec nam aliquam sem. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Non blandit massa enim nec dui nunc mattis. Est ante in nibh mauris cursus. A diam maecenas sed enim ut sem viverra aliquet. Ornare aenean euismod elementum nisi quis. Est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Euismod in pellentesque massa placerat duis ultricies lacus. Volutpat maecenas volutpat blandit aliquam etiam erat. Tincidunt ornare massa eget egestas. Tellus molestie nunc non blandit massa enim nec dui nunc. At quis risus sed vulputate odio. Urna molestie at elementum eu. Enim lobortis scelerisque fermentum dui faucibus in ornare quam viverra. Leo vel fringilla est ullamcorper. Morbi tristique senectus et netus et malesuada fames. Faucibus ornare suspendisse sed nisi lacus sed viverra.

	Tellus in hac habitasse platea dictumst vestibulum rhoncus. Praesent elementum facilisis leo vel fringilla est. Lorem ipsum dolor sit amet consectetur. Donec ultrices tincidunt arcu non sodales neque sodales. Dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. At quis risus sed vulputate. Tincidunt nunc pulvinar sapien et ligula ullamcorper malesuada proin. Orci sagittis eu volutpat odio facilisis. Ut tellus elementum sagittis vitae et leo duis ut diam. Donec et odio pellentesque diam volutpat commodo sed egestas egestas. Facilisis volutpat est velit egestas dui id ornare arcu odio. Rutrum quisque non tellus orci ac auctor augue mauris augue. Tortor condimentum lacinia quis vel eros donec ac. Ac tortor dignissim convallis aenean. Felis imperdiet proin fermentum leo vel orci porta. Purus in mollis nunc sed id. Eget aliquet nibh praesent tristique magna sit amet. Consequat nisl vel pretium lectus quam id leo.

	Aliquam purus sit amet luctus venenatis lectus magna. Ac tortor vitae purus faucibus ornare. Convallis posuere morbi leo urna. Nulla posuere sollicitudin aliquam ultrices sagittis orci a. Adipiscing elit ut aliquam purus sit amet luctus. Sit amet mattis vulputate enim. Pellentesque habitant morbi tristique senectus. Faucibus nisl tincidunt eget nullam non nisi. Purus gravida quis blandit turpis cursus in. Aliquam nulla facilisi cras fermentum odio eu feugiat pretium nibh. Pharetra et ultrices neque ornare aenean euismod.

	Suspendisse sed nisi lacus sed. Cursus sit amet dictum sit amet justo donec. Eget nunc lobortis mattis aliquam. Eget nullam non nisi est sit amet. Turpis cursus in hac habitasse platea dictumst quisque. Aliquam id diam maecenas ultricies mi. Vitae congue eu consequat ac felis. Facilisi nullam vehicula ipsum a arcu cursus vitae congue. Vitae nunc sed velit dignissim sodales. Placerat orci nulla pellentesque dignissim enim sit amet venenatis. Nibh tellus molestie nunc non. Id diam vel quam elementum pulvinar etiam non quam.

	Mus mauris vitae ultricies leo integer malesuada nunc. Varius quam quisque id diam vel quam. Lorem ipsum dolor sit amet consectetur. Congue quisque egestas diam in arcu cursus euismod quis. Id nibh tortor id aliquet lectus proin nibh nisl condimentum. Facilisis magna etiam tempor orci eu lobortis elementum nibh. Sit amet porttitor eget dolor morbi non. Et odio pellentesque diam volutpat commodo. Urna molestie at elementum eu facilisis. Enim neque volutpat ac tincidunt vitae. Proin libero nunc consequat interdum varius. Enim diam vulputate ut pharetra sit amet aliquam id. Eu augue ut lectus arcu bibendum at varius vel. Leo vel orci porta non pulvinar neque laoreet suspendisse. Orci eu lobortis elementum nibh tellus molestie nunc non blandit.

	Congue quisque egestas diam in arcu cursus. Tellus molestie nunc non blandit massa. Turpis massa tincidunt dui ut. Diam quis enim lobortis scelerisque fermentum dui. Sed id semper risus in hendrerit gravida rutrum quisque non. Amet porttitor eget dolor morbi non arcu risus quis. Dolor sit amet consectetur adipiscing elit pellentesque. Id donec ultrices tincidunt arcu. Ut placerat orci nulla pellentesque dignissim. Et netus et malesuada fames ac turpis egestas maecenas. Feugiat vivamus at augue eget.

	Sit amet commodo nulla facilisi nullam vehicula. Nunc consequat interdum varius sit amet mattis vulputate. Nisl rhoncus mattis rhoncus urna. Dui accumsan sit amet nulla facilisi morbi. Donec ac odio tempor orci dapibus ultrices in iaculis nunc. Tellus integer feugiat scelerisque varius morbi enim nunc faucibus a. Lobortis mattis aliquam faucibus purus in massa tempor. In cursus turpis massa tincidunt dui ut ornare lectus sit. Vitae et leo duis ut diam quam nulla porttitor. Sollicitudin aliquam ultrices sagittis orci a scelerisque purus semper. Vestibulum sed arcu non odio euismod lacinia at quis risus. A diam sollicitudin tempor id eu. Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum.

	Egestas fringilla phasellus faucibus scelerisque. Arcu non sodales neque sodales. Diam vulputate ut pharetra sit amet aliquam id diam. Eget mauris pharetra et ultrices neque ornare aenean. Vestibulum sed arcu non odio euismod. Nam aliquam sem et tortor consequat id porta. Leo integer malesuada nunc vel risus commodo viverra. Et leo duis ut diam quam nulla porttitor massa id. Lorem dolor sed viverra ipsum nunc aliquet bibendum. Sapien eget mi proin sed libero enim. Fermentum odio eu feugiat pretium. Scelerisque eu ultrices vitae auctor. Cursus euismod quis viverra nibh cras pulvinar mattis. Pulvinar neque laoreet suspendisse interdum consectetur libero id. A condimentum vitae sapien pellentesque habitant morbi tristique senectus.

	Massa vitae tortor condimentum lacinia quis vel eros donec ac. Cursus metus aliquam eleifend mi in nulla posuere sollicitudin aliquam. Quis auctor elit sed vulputate mi sit amet. Diam maecenas ultricies mi eget. Eget dolor morbi non arcu. Cras sed felis eget velit. Amet luctus venenatis lectus magna fringilla urna. Nec feugiat in fermentum posuere urna nec tincidunt praesent semper. Tincidunt ornare massa eget egestas. Vel eros donec ac odio tempor orci dapibus ultrices. Egestas sed tempus urna et pharetra pharetra massa massa ultricies. Aliquam nulla facilisi cras fermentum odio eu. Consequat mauris nunc congue nisi vitae suscipit tellus mauris. Nam at lectus urna duis convallis convallis tellus id. Et netus et malesuada fames ac turpis egestas. Faucibus purus in massa tempor nec feugiat nisl. Ultricies leo integer malesuada nunc vel. Ultricies mi eget mauris pharetra et ultrices neque ornare aenean. Tristique risus nec feugiat in fermentum posuere urna. Aenean pharetra magna ac placerat vestibulum lectus mauris ultrices.

	Eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada bibendum. Consequat nisl vel pretium lectus quam id leo. Ultrices eros in cursus turpis. Faucibus in ornare quam viverra orci sagittis. Dictum varius duis at consectetur. Quis auctor elit sed vulputate. Placerat duis ultricies lacus sed. Ut etiam sit amet nisl purus in. Varius vel pharetra vel turpis nunc eget lorem dolor sed. Turpis in eu mi bibendum neque egestas. Id neque aliquam vestibulum morbi blandit. Mauris commodo quis imperdiet massa tincidunt nunc. Amet commodo nulla facilisi nullam vehicula ipsum. Tortor posuere ac ut consequat semper viverra nam libero justo. Commodo nulla facilisi nullam vehicula ipsum a arcu.

	Ullamcorper velit sed ullamcorper morbi tincidunt ornare massa eget egestas. Ornare lectus sit amet est placerat in egestas erat imperdiet. In arcu cursus euismod quis viverra nibh cras pulvinar mattis. Luctus accumsan tortor posuere ac ut consequat semper viverra nam. Dolor magna eget est lorem ipsum. Lobortis feugiat vivamus at augue eget arcu dictum. Vestibulum mattis ullamcorper velit sed. Id nibh tortor id aliquet lectus. Ultricies lacus sed turpis tincidunt. Iaculis urna id volutpat lacus laoreet non. Tellus pellentesque eu tincidunt tortor aliquam. Enim nec dui nunc mattis enim ut. Quis varius quam quisque id diam. Purus ut faucibus pulvinar elementum integer. Placerat duis ultricies lacus sed turpis tincidunt id aliquet risus. Neque sodales ut etiam sit amet nisl purus. Nec feugiat nisl pretium fusce id velit.

	Ultrices eros in cursus turpis massa tincidunt dui ut. In massa tempor nec feugiat nisl pretium fusce. Sed id semper risus in hendrerit gravida rutrum quisque. Condimentum lacinia quis vel eros donec ac odio tempor. Nunc sed blandit libero volutpat sed cras. Et leo duis ut diam quam. Ullamcorper sit amet risus nullam. Semper auctor neque vitae tempus quam pellentesque nec nam aliquam. Urna id volutpat lacus laoreet non curabitur. Sem et tortor consequat id porta nibh. Rhoncus mattis rhoncus urna neque viverra justo nec ultrices. Tristique nulla aliquet enim tortor at auctor urna nunc. Scelerisque felis imperdiet proin fermentum leo vel orci porta. Massa ultricies mi quis hendrerit dolor magna. Suscipit tellus mauris a diam maecenas sed enim. Duis ut diam quam nulla porttitor massa. Tempus quam pellentesque nec nam aliquam sem. Vulputate sapien nec sagittis aliquam malesuada bibendum arcu. Amet purus gravida quis blandit. Risus commodo viverra maecenas accumsan lacus vel facilisis volutpat est.

	Aliquam id diam maecenas ultricies mi eget. Tincidunt dui ut ornare lectus. Arcu ac tortor dignissim convallis aenean et tortor at risus. Cursus risus at ultrices mi tempus imperdiet nulla. Eu consequat ac felis donec et odio. Curabitur gravida arcu ac tortor dignissim. Scelerisque viverra mauris in aliquam sem. Nullam non nisi est sit amet facilisis magna etiam tempor. Velit euismod in pellentesque massa placerat duis ultricies. Urna id volutpat lacus laoreet non curabitur. Praesent elementum facilisis leo vel fringilla est ullamcorper. Lorem sed risus ultricies tristique nulla aliquet enim tortor. Erat velit scelerisque in dictum non consectetur. Imperdiet dui accumsan sit amet nulla facilisi. Sapien et ligula ullamcorper malesuada proin. Amet commodo nulla facilisi nullam vehicula ipsum a arcu. Mi in nulla posuere sollicitudin aliquam ultrices sagittis orci. Urna condimentum mattis pellentesque id nibh tortor id aliquet. Amet venenatis urna cursus eget nunc. Elementum sagittis vitae et leo duis.

	Feugiat in fermentum posuere urna nec tincidunt. Sem nulla pharetra diam sit amet nisl suscipit adipiscing. Sed blandit libero volutpat sed cras. Arcu dictum varius duis at. Porttitor massa id neque aliquam vestibulum morbi blandit. Dui faucibus in ornare quam viverra orci sagittis. Nisi quis eleifend quam adipiscing vitae proin sagittis nisl. Duis ut diam quam nulla porttitor massa id neque. Convallis a cras semper auctor. Mauris a diam maecenas sed enim ut. Urna id volutpat lacus laoreet non curabitur gravida arcu ac.

	Pellentesque eu tincidunt tortor aliquam nulla. Semper risus in hendrerit gravida rutrum quisque non tellus orci. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Posuere morbi leo urna molestie at elementum eu facilisis sed. Donec pretium vulputate sapien nec sagittis. Feugiat sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi. Imperdiet nulla malesuada pellentesque elit eget gravida cum sociis. Donec enim diam vulputate ut pharetra sit amet aliquam id. Nunc id cursus metus aliquam eleifend mi. Urna nec tincidunt praesent semper.

	Pharetra pharetra massa massa ultricies. Mollis aliquam ut porttitor leo a diam sollicitudin tempor. Venenatis cras sed felis eget velit aliquet. Pellentesque habitant morbi tristique senectus et netus et malesuada fames. Integer enim neque volutpat ac tincidunt vitae semper quis. Sem integer vitae justo eget magna. Tortor posuere ac ut consequat semper viverra nam. Id donec ultrices tincidunt arcu non sodales neque sodales. Massa massa ultricies mi quis hendrerit dolor magna. Faucibus purus in massa tempor nec feugiat nisl. Vitae turpis massa sed elementum tempus egestas. Odio morbi quis commodo odio aenean sed adipiscing. Non diam phasellus vestibulum lorem sed. Malesuada nunc vel risus commodo viverra maecenas accumsan lacus vel. Integer quis auctor elit sed vulputate mi sit. Id interdum velit laoreet id donec ultrices tincidunt arcu. Sit amet porttitor eget dolor morbi. Sed augue lacus viverra vitae. Facilisis gravida neque convallis a. Donec ac odio tempor orci dapibus.

	Semper auctor neque vitae tempus quam pellentesque nec nam. Arcu cursus vitae congue mauris rhoncus aenean. Dignissim cras tincidunt lobortis feugiat vivamus at augue. Et tortor consequat id porta nibh venenatis. Sagittis id consectetur purus ut. Dictum at tempor commodo ullamcorper a lacus vestibulum. Tempor orci eu lobortis elementum nibh tellus molestie nunc. Dui nunc mattis enim ut. Volutpat est velit egestas dui id. Mauris pharetra et ultrices neque ornare aenean euismod. Cursus euismod quis viverra nibh. Dapibus ultrices in iaculis nunc sed augue.
	"""

	class SessionTimeoutException(Exception):
		pass

	class Chunk:
		def __init__(self, seq, header, data=b''):
			self.seq = seq
			self.raw = header.encode('utf-8') + data
			self.eof = len(data) == 0

	class Response:
		def __init__(self, mime_type, f):
			self.mime_type = mime_type
			self.seq = random.randint(20000, 65536)
			self.start = self.seq
			self.f = f
			self.chunks = []
			self.eof = None

		def close(self):
			self.f.close()

		def __iter__(self):
			self.pos = 0
			return self

		def __next__(self):
			if self.pos < len(self.chunks):
				seq, chunk = self.chunks[self.pos]
				self.pos += 1
				return seq, chunk

			if self.eof:
				raise StopIteration()

			data = self.f.read(chunk_size)

			self.seq += 1
			logging.debug(f"Building chunk {self.seq}")

			if not data:
				chunk = Chunk(self.seq, f"{self.seq}\r\n")
				self.eof = self.seq
			elif self.mime_type:
				chunk = Chunk(self.seq, f"{self.seq} {self.mime_type}\r\n", data)
				self.mime_type = None
			else:
				chunk = Chunk(self.seq, f"{self.seq}\r\n", data)

			self.chunks.append((self.seq, chunk))
			self.pos += 1
			return self.seq, chunk

		def ack(self, seq):
			self.chunks = [(oseq, chunk) for oseq, chunk in self.chunks if oseq != seq]

		def sent(self):
			return self.eof and not self.chunks

	class Session:
		def __init__(self, sock, src, mime_type, f, chunk_size):
			self.sock = sock
			self.src = src
			self.mime_type = mime_type
			self.sent = {}
			self.started = time.time()

			self.response = Response(mime_type, f)

		def close(self):
			self.response.close()

		def send(self):
			if time.time() > self.started + 30:
				raise SessionTimeoutException()

			unacked = 0

			now = time.time()
			for seq, chunk in self.response:
				sent = self.sent.get(seq, 0)
				if sent < now - 2:
					if sent:
						logging.info(f"Re-sending {seq} ({unacked}/8) to {self.src}")
					else:
						logging.info(f"Sending {seq} ({unacked}/8) to {self.src}")
					self.sock.sendto(chunk.raw, self.src)
					self.sent[seq] = now

				# allow only 8 chunks awaiting acknowledgement at a time
				unacked += 1
				if unacked == 8:
					break

		def ack(self, seq):
			if seq not in self.sent:
				raise Exception(f"Unknown packet for {self.src}: {seq}")

			logging.info(f"{self.src} has received {seq}")
			self.response.ack(seq)

			return self.response.sent()

	logging.basicConfig(level=logging.INFO)

	sock = socket.socket(type=socket.SOCK_DGRAM)
	sock.bind(('', 6775))

	sessions = collections.OrderedDict()
	while True:
		ready, _, _ = select.select([sock.fileno()], [], [], 0.1)

		finished = []

		if ready:
			pkt, src = sock.recvfrom(2048)
			if pkt.endswith(b'\r\n'):
				try:
					session = sessions.get(src)
					if session:
						try:
							seq = int(pkt[:len(pkt) - 2])
						except ValueError as e:
							# probably a duplicate request packet packet
							logging.exception("Received invalid packet", e)
						else:
							if session.ack(seq):
								logging.info(f"Session {src} has ended successfully")
								finished.append(src)
					else:
						if len(sessions) > 32:
							raise Exception("Too many sessions")

						if pkt.startswith(b'guppy://'):
							url = urlparse(pkt.decode('utf-8'))

							mime_type = "text/gemini"
							chunk_size = 512

							if url.path == '' or url.path == '/':
								f = io.BytesIO(home.encode('utf-8'))
								chunk_size = 512
							elif url.path == '/lorem':
								f = io.BytesIO(lorem_ipsum.encode('utf-8'))
							elif url.path == '/echo':
								mime_type = "text/plain"
								data = unquote(url.query).encode('utf-8')
								if not data:
									sock.sendto(b'1 Your name\r\n', src)
									raise Exception("Your name")

								f = io.BytesIO(data)
							elif url.path == '/rick.mp4':
								mime_type = "video/mp4"
								f = open('/tmp/rick.mp4', 'rb')
								chunk_size = 2048
							else:
								raise Exception(f"Invalid path")

							sessions[src] = Session(sock, src, mime_type, f, chunk_size)
				except Exception as e:
					logging.exception("Unhandled exception", e)

		for src, session in sessions.items():
			try:
				session.send()
			except SessionTimeoutException:
				logging.info(f"Session {src} has timed out")
				finished.append(src)
			except Exception as e:
				logging.exception("Unhandled exception", e)

		for src in finished:
			sessions[src].close()
			sessions.pop(src)

C server with out-of-order transmission of files in the working directory:

	#include <sys/types.h>
	#include <sys/socket.h>
	#include <netdb.h>
	#include <unistd.h>
	#include <stdlib.h>
	#include <arpa/inet.h>
	#include <string.h>
	#include <limits.h>
	#include <fcntl.h>
	#include <time.h>
	#include <stdio.h>
	#include <poll.h>
	#include <errno.h>
	#include <stdio.h>
	#include <arpa/inet.h>

	#define PORT "6775"
	#define MAX_SESSIONS 32
	#define MAX_CHUNKS 8
	#define CHUNK_SIZE 512

	int main(int argc, char *argv[])
	{
		static struct {
			struct {
				struct sockaddr_storage peer;
				char addrstr[INET6_ADDRSTRLEN];
				unsigned short port;
				struct timespec started;
				int fd, first, next, last;
				struct {
					char data[CHUNK_SIZE];
					struct timespec sent;
					ssize_t len;
					int seq;
				} chunks[MAX_CHUNKS];
			} sessions[MAX_SESSIONS];
		} server;
		static char buf[2049];
		char *end;
		struct pollfd pfd;
		const char *path, *errstr;
		const void *saddr;
		struct addrinfo hints = {
			.ai_family = AF_INET6,
			.ai_flags = AI_PASSIVE | AI_V4MAPPED,
			.ai_socktype = SOCK_DGRAM
		}, *addr;
		struct sockaddr_storage peer;
		struct timespec now;
		const struct sockaddr_in *peerv4 = (const struct sockaddr_in *)&peer;
		const struct sockaddr_in6 *peerv6 = (const struct sockaddr_in6 *)&peer;
		long seq;
		ssize_t len;
		int s, sndbuf = MAX_SESSIONS * MAX_CHUNKS * CHUNK_SIZE, one = 1, off, ret;
		unsigned int slot, active = 0, i, j, ready, waiting;
		socklen_t peerlen;
		unsigned short sport;

		// start listening for packets and increase the buffer size for sending
		if (getaddrinfo(NULL, PORT, &hints, &addr) != 0) return EXIT_FAILURE;
		if (
			(s = socket(addr->ai_family,addr->ai_socktype, addr->ai_protocol)) < 0
		) {
			freeaddrinfo(addr);
			return EXIT_FAILURE;
		}
		if (
			setsockopt(s, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)) < 0 ||
			setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0 ||
			bind(s, addr->ai_addr, addr->ai_addrlen) < 0
		) {
			close(s);
			freeaddrinfo(addr);
			return EXIT_FAILURE;
		}
		freeaddrinfo(addr);

		// restrict access to files outside the working directory
		if (chroot(".") < 0) {
			close(s);
			return EXIT_FAILURE;
		}

		pfd.events = POLLIN;
		pfd.fd = s;

		srand((unsigned int)time(NULL));

		for (i = 0; i < MAX_SESSIONS; ++i) {
			server.sessions[i].fd = -1;
			server.sessions[i].peer.ss_family = AF_UNSPEC;
		}

		while (1) {
			// wait for an incoming packet with timeout of 100ms
			pfd.revents = 0;
			ready = poll(&pfd, 1, 100);
			if (ready < 0) break;

			if (ready == 0 || !(pfd.revents & POLLIN)) goto respond;

			// receive a packet
			peerlen = sizeof(peer);
			if (
				(len = recvfrom(
					s,
					buf,
					sizeof(buf) - 1,
					0,
					(struct sockaddr *)&peer,
					&peerlen
				)) <= 0
			) break;

			// the smallest valid packet we can receive is 6\r\n
			if (len < 3 || buf[len - 2] != '\r' || buf[len - 1] != '\n') continue;

			// check if this packet belongs to an existing session by comparing the
			// source address of the packet
			switch (peer.ss_family) {
			case AF_INET:
				sport = ntohs(peerv4->sin_port);
				saddr = &peerv4->sin_addr;
				break;
			case AF_INET6:
				sport = ntohs(peerv6->sin6_port);
				saddr = &peerv6->sin6_addr;
				break;
			default:
				continue;
			}

			slot = MAX_SESSIONS;

			for (i = sport % MAX_SESSIONS; i < MAX_SESSIONS; ++i) {
				if (
					memcmp(
						&peer,
						&server.sessions[i].peer,
						sizeof(struct sockaddr_storage)
					) == 0
				) {
					slot = i;
					goto have_slot;
				}
			}

			for (i = 0; i < sport % MAX_SESSIONS; ++i) {
				if (
					memcmp(
						&peer,
						&server.sessions[i].peer,
						sizeof(struct sockaddr_storage)
					) == 0) {
					slot = i;
					break;
				}
			}

	have_slot:
			// if all slots are occupied, send an error packet
			if (slot == MAX_SESSIONS && active == MAX_SESSIONS) {
				fputs("Too many sessions", stderr);
				sendto(
					s,
					"4 Too many sessions\r\n",
					21,
					0,
					(const struct sockaddr *)&peer,
					peerlen
				);
				continue;
			}

			// if no session matches this packet ...
			if (slot == MAX_SESSIONS) {
				// parse and validate the request
				if (len < 11 || memcmp(buf, "guppy://", 8)) continue;
				buf[len - 2] = '\0';
				if (
					!(path = strchr(&buf[8], '/')) ||
					!path[0] ||
					(path[0] == '/' && !path[1])
				) path = "index.gmi";
				else ++path;

				// find an empty slot
				for (i = 0; i < MAX_SESSIONS; ++i) {
					if (server.sessions[i].fd < 0) {
						slot = i;
						break;
					}
				}

				if (
					!inet_ntop(
						peer.ss_family,
						saddr,
						server.sessions[slot].addrstr,
						sizeof(server.sessions[slot].addrstr)
					)
				) continue;

				if ((server.sessions[slot].fd = open(path, O_RDONLY)) < 0) {
					errstr = strerror(errno);
					fprintf(
						stderr,
						"Failed to open %s for %s:%hu: %s\n",
						path,
						server.sessions[slot].addrstr,
						server.sessions[slot].port,
						errstr
					);
					ret = snprintf(buf, sizeof(buf), "4 %s\r\n", errstr);
					if (ret > 0 && ret < sizeof(buf))
						sendto(
							s,
							buf,
							(size_t)ret,
							0,
							(const struct sockaddr *)&peer,
							peerlen
						);
					continue;
				}

				memcpy(
					&server.sessions[slot].peer,
					&peer,
					sizeof(struct sockaddr_storage)
				);
				server.sessions[slot].port = sport;
				server.sessions[slot].first = 6 + (rand() % SHRT_MAX);
				server.sessions[slot].next = server.sessions[slot].first;
				server.sessions[slot].last = 0;
				server.sessions[slot].started = now;
				for (i = 0; i < MAX_CHUNKS; ++i)
					server.sessions[slot].chunks[i].seq = 0;
				++active;
			} else {
				// extract the sequence number from the acknowledgement packet
				buf[len] = '\0';
				if (
					(seq = strtol(buf, &end, 10)) < server.sessions[slot].first ||
					(seq >= server.sessions[slot].next) ||
					!end ||
					len != (end - buf) + 2
				)
					goto respond;

				// acknowledge the packet
				for (i = 0; i < MAX_CHUNKS; ++i) {
					if (server.sessions[slot].chunks[i].seq == seq) {
						fprintf(
							stderr,
							"%s:%hu has received %ld\n",
							server.sessions[slot].addrstr,
							server.sessions[slot].port,
							seq
						);
						server.sessions[slot].chunks[i].seq = 0;
					} else if (server.sessions[slot].chunks[i].seq) ++waiting;
				}
			}

			// fill all free slots with more chunks that can be sent
			for (i = 0; i < MAX_CHUNKS && !server.sessions[slot].last; ++i) {
				if (server.sessions[slot].chunks[i].seq) continue;

				server.sessions[slot].chunks[i].seq = server.sessions[slot].next;

				if (
					server.sessions[slot].chunks[i].seq ==
														server.sessions[slot].first
				)
					off = sprintf(
						server.sessions[slot].chunks[i].data,
						"%d text/gemini\r\n",
						server.sessions[slot].chunks[i].seq
					);
				else
					off = sprintf(
						server.sessions[slot].chunks[i].data,
						"%d\r\n",
						server.sessions[slot].chunks[i].seq
					);

				server.sessions[slot].chunks[i].len = read(
					server.sessions[slot].fd,
					&server.sessions[slot].chunks[i].data[off],
					sizeof(server.sessions[slot].chunks[i].data) - off
				);
				if (server.sessions[slot].chunks[i].len < 0) {
					fprintf(
						stderr,
						"Failed to read file for %s:%hu: %s\n",
						server.sessions[slot].addrstr,
						server.sessions[slot].port,
						strerror(errno)
					);
					server.sessions[slot].chunks[i].len = off;
				} else if (server.sessions[slot].chunks[i].len == 0)
					server.sessions[slot].chunks[i].len = off;
				else server.sessions[slot].chunks[i].len += off;

				if (
					server.sessions[slot].chunks[i].len == off &&
					!server.sessions[slot].last
				) server.sessions[slot].last = server.sessions[slot].chunks[i].seq;
				server.sessions[slot].chunks[i].sent.tv_sec = 0;

				++server.sessions[slot].next;
			}

	respond:
			if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) break;

			for (i = 0; i < MAX_SESSIONS; ++i) {
				if (server.sessions[i].fd < 0) continue;

				// terminate sessions after 20s
				if (now.tv_sec > server.sessions[i].started.tv_sec + 20) {
					fprintf(
						stderr,
						"%s:%hu has timed out\n",
						server.sessions[i].addrstr,
						server.sessions[i].port
					);
					close(server.sessions[i].fd);
					server.sessions[i].fd = -1;
					--active;
					continue;
				}

				// send unacknowledged chunks if not sent or every 2s
				waiting = 0;
				for (j = 0; j < MAX_CHUNKS; ++j) {
					if (server.sessions[i].chunks[j].seq == 0) continue;

					++waiting;

					if (server.sessions[i].chunks[j].sent.tv_sec == 0) fprintf(
						stderr, "Sending %d to %s:%hu\n",
						server.sessions[i].chunks[j].seq,
						server.sessions[i].addrstr,
						server.sessions[i].port
					);
					else if (
						now.tv_sec < server.sessions[i].chunks[j].sent.tv_sec + 2
					) continue;
					else fprintf(
						stderr,
						"Resending %d to %s:%hu\n",
						server.sessions[i].chunks[j].seq,
						server.sessions[i].addrstr,
						server.sessions[i].port
					);

					if (
						sendto(
							s,
							server.sessions[i].chunks[j].data,
							server.sessions[i].chunks[j].len,
							0,
							(const struct sockaddr *)&server.sessions[i].peer,
							sizeof(server.sessions[i].peer)
						) < 0
					) fprintf(
						stderr,
						"Failed to send packet to %s:%hu: %s\n",
						server.sessions[i].addrstr,
						server.sessions[i].port,
						strerror(errno)
					);
					else server.sessions[i].chunks[j].sent = now;
				}

				// if all packets are acknowledged, terminate the session
				if (waiting == 1)
					fprintf(
						stderr,
						"%s:%hu has 1 pending packet\n",
						server.sessions[i].addrstr,
						server.sessions[i].port
					);
				else if (waiting > 1)
					fprintf(
						stderr,
						"%s:%hu has %d pending packets\n",
						server.sessions[i].addrstr,
						server.sessions[i].port,
						waiting
					);
				else {
					fprintf(
						stderr,
						"%s:%hu has received all packets\n",
						server.sessions[i].addrstr,
						server.sessions[i].port
					);
					close(server.sessions[i].fd);
					server.sessions[i].fd = -1;
					--active;
				}
			}
		}

		for (i = 0; i < MAX_SESSIONS; ++i) {
			if (server.sessions[i].fd >= 0) close(server.sessions[i].fd);
		}
		close(s);
		return EXIT_SUCCESS;
	}

Lagrange fork with guppy:// support

Kristall fork with guppy:// support

gplaces branch with guppy:// support

git clone -b guppy --recursive https://github.com/dimkr/gplaces
cd gplaces
make PREFIX=/tmp/guppy CONFDIR=/tmp/guppy/etc install
/tmp/guppy/bin/gplaces guppy://hd.206267.xyz/

tootik branch with guppy:// support

URLs you can use for testing and development:

guppy://hd.206267.xyz/

(tootik)

guppy://gemini.dimakrasner.com/

(this capsule, MicroPython translation of the Python server example running on a Pico W)

(Please do not assume that these code samples are perfect and 100% compliant with this document)

Terminology

"Must" means a strict requirement, a rule all conformant Guppy client or server must obey.

"Should" means a recommendation, something minimal clients or servers should do.

"May" means a soft recommendation, something good clients or servers should do.

URLs

If no port is specified in a guppy:// URL, clients and servers must fall back to 6775 ('gu').

MIME Types

Interactive clients must be able to display text/plain documents.

Interactive clients must be able to parse text/gemini (without the Spartan := type) documents and allow users to follow links.

If encoding is unspecified via the charset parameter of the MIME type field, the client must assume it's UTF-8. Clients which support ASCII but do not support UTF-8 may render documents with replacement characters.

Download vs. Upload

In Guppy, all URLs can be (theoretically) accompanied by user-provided input. The client must provide the user with means for sending a request with user-provided input, to any link line.

Server authors should inform users when input is required and describe what kind of input, using the link's user-friendly description.

If input is expected but not provided by the user, the server must respond with an error packet.

Security and Privacy

The protocol is unencrypted, and these concerns are beyond the scope of this document.

Limits

Clients and servers may restrict packet size, to allow slower but more reliable transfer.

Requests (the URL plus 2 bytes for the trailing \r\n) must fit in 2048 bytes.

Packet Order

Servers should transmit multiple packets at once, instead of waiting for the client to acknolwedge a packet before sending the next one.

Servers may limit the number of packets awaiting acknowledgement from the client, and wait with sending of the next continuation packets until the client acknowledges some or even all unacknowledged packets.

The server must not assume that lost continuation packet n does not need to be retransmitted, when packet n+1 is acknowledged by the client.

Trivial clients may ignore out-of-order packets and wait for the next packet to be retransmitted if previously received but ignored, at the cost of slow transfer speeds.

Clients that receive continuation or end-of-file packets in the wrong order should cache and acknowledge the packets, to prevent the server from sending them again and reduce overall transfer time.

Clients may limit the number of buffered packets and keep up to x chunks of the response in memory, when the server transmits many out-of-order packets. However, clients that save a limited number of out-of-order packets must leave room for the first response packet instead of failing when many continuation packets exhaust the buffer.

Chunked Responses

The server may send a chunked response, by sending one or more continuation packets.

Servers must transmit responses larger than 512 bytes in chunks of at least 512 bytes. If the response is less than 512 bytes, servers must send it as one piece, without continuation packets.

Clients should start displaying the response as soon as the first chunk is received.

Clients must not assume that the response is split on a line boundary: a long line may be sent in multiple response packets.

Clients must not assume that every response chunk contains a valid UTF-8 string: a continuation packet may end with the first byte of multi-byte sequence, while the rest of it is in the next response chunk.

Sessions

Clients must use the same source port for all packets they send within one "session".

Servers should associate the source address and source port combination of a request packet with a session. For example, if the server sends packet n to 1.2.3.4:9000 but 2.3.4.5:8000 acknowledges packet n, the server must not assume that 1.2.3.4:9000 has received the packet.

Servers must ignore additional request packets and duplicate acknowledgement packets in each session.

Servers should limit the number of active sessions, to protect themselves against denial of service.

Servers should end each session on timeout, by ignoring incoming packets and not sending any packets.

Servers that limit the number of active sessions and end sessions on timeout should ignore queued requests if the time they wait in the queue exceeds session timeout.

Lost Packets

Clients should re-transmit request and acknowledgement packets after a while, if nothing is received from the server.

If the client keeps receiving the same sucess, continuation or EOF packet, the acknowledgement packets for it were probably lost and the client must re-acknowledge it to avoid additional waste of bandwidth and allow servers that limit the number of unacknowledged packets to send the next chunk of the response.

The server should re-transmit a success, continuation or EOF packet after a while, if not acknowledged by the client.

Servers must ignore duplicate acknowledgement packets and additional request packets in the same session.

Clients must wait for the "end of file" packet, to differentiate between timeout, a partially received response and a successfully received response.

Packet Types

There are 8 packet types:

All packets begin with a "header", followed by \r\n.

TL;DR -

Requests

	url\r\n

The query part specifies user-provided input, percent-encoded.

The server must respond with a success, input prompt, redirect or error packet.

Success

	seq type\r\n
	data

The sequence number is an arbitrary number between 6 and 2147483647 (maximum value of a signed 32-bit integer), followed by a space character (0x20 byte). Clients must not assume that the sequence number cannot begin with a digit <= 5 and confuse success packets with sequence number 39 or 41 with redirect or error packets, respectively. Servers must pick a low enough sequence number, so the sequence number of the end-of-file packet does not exceed 2147483647.

The type field specifies the response MIME type and must not be empty.

Continuation

	seq\r\n
	data

The sequence number must increase by 1 in every continuation packet.

End-of-file

	seq\r\n

The server must mark the end of the transmission by sending a continuation packet without any data, even if the response fits in a single packet.

Acknowledgement

	seq\r\n

The client must acknowledge every success, continuation or EOF packet by echoing its sequence number back to the server.

Input prompt

	1 prompt\r\n

The client must show the prompt to the user and allow the user to repeat the request, this time with the user's input.

The client may remember the input prompt and ask the user for input on future access of this URL, without having to request the same URL without input first.

The client may allow the user to access a URL with user-provided input even without requesting this URL once to retrieve the prompt text.

Redirect

	3 url\r\n

The URL may be relative.

The client must inform the user of the redirection.

The client may remember redirected URLs. The server must not assume clients don't do this.

The client should limit the number of redirects during the handling of a single request.

The client may forbid a series of redirects, and may prompt the user to confirm each redirect.

Error

	4 error\r\n

Clients must display the error to the user.

Response to feedback

Using an error packet to tell the user they need to request a URL with input mixes telling the user there was an error (e.g. something broke) with an instruction to the user

This is intentional: errors are for the user, not for the client. They should be human-readable.

This also means that for any URL that should accept user input, the author would need to configure the Guppy server to return an error, which is kind of onerous.

Gemini servers respond with status code 1x when they expect input but none is provided. This is a similar mechanism, but without introducing a special status code requiring clients to implement the "retry the request after showing a prompt" logic.

Using an Error Packet to signify user input:
* user downloads gemtext
...

There's a missing first step here: user follows a link that says "enter search keywords" or "new post", then decides to attach input to the request.

It seems like this probably was changed but the acknowledgement section wasn't updated.
[...]
This contradicts just about everything said elsewhere about out-of-order packet handling so it probably just wasn't updated in some prior iteration.

True.

Note also that with the spec-provided 512 byte minimum chunk size, storing the sequence number in an unsigned 16-bit number caps the guaranteed download size at 16MB.

True. Although we're talking about small text files here, I increased the sequence number range to allow larger transfers.

One possible (but not guaranteed) ack failure indication would be receiving a re-transmission of an already-acked packet, but this is something the spec elsewhere suggests clients ignore, and is a pretty awkward heuristic to code.

Clients must re-acknowledge the packet if received again, so the server can stop sending it and continue if it's waiting for it to be acknowledged before it sends the next ones.

The server won't be able to distinguish re-transmission of the "same" request packet from a legitimate re-request [...]. These are indistinguishable because request packets don't have a sequence number.

The server can use the source address and port combination to ignore additional requests in the same "session", hence no application layer request ID is needed. That's one thing that UDP does provide :)

It's a classic mistake to look at complicated machinery like TCP and assume it's bloated.

I'm not assuming it's bloated, and Guppy is not a reaction to the so-called "bloat" of TCP. It's an experiment in designing a protocol simpler than Gopher and Spartan, which provides a similar feature set but with faster transfer speeds (for small documents) and using a much simpler software stack (i.e. including the implementation of TCP/IP, instead of judging simplicity by the application layer protocol alone).

Even though TCP contains a more complicated and convoluted solution to the problems of re-ordering and re-transmission, its use would be a massive simplification both for this spec and especially for implementors.

Implementors can implement a TFTP-style client, one that sends a request, waits for a single packet, acknowledges it, waits for the next packet and so on. If the client displays the first chunk of the response while waiting for the next one, and the document fits in 3-4 response packets, such a client should be good enough for most content and most users. Clients are compatible with servers that don't understand out-of-order transmission, and vice versa, so it is possible to implement a super simple but still useful Guppy client.

Any time you have a [...] protocol where a small packet to the server results in a large packet from the server will be exploited with a constant barrage of forged packets.

True, but this sentence also applies to TCP-based protcols. In general, any server exposed to the internet without any kind of rate limiting or load balancing will get DoSed or abused. For example, a TFTP server can limit the number of source addresses, source ports or address+port combinations it's willing to talk to at a given moment, and I don't see why the same concept can't be applied to a Guppy server.

In addition, unlike some UDP-based protocols, where both the request and the response are a single UDP packet, Guppy has the end-of-file packet even in pages that fit in one chunk: the server knows the "session" hasn't ended until the client acks this packet, so the server can count and limit active sesions. Please correct me if I'm wrong.