├── guppy-spec.gmi ├── guppyc1.py ├── guppyc2.py ├── guppys.c ├── guppys.py └── index.gmi /guppy-spec.gmi: -------------------------------------------------------------------------------- 1 | # The Guppy Protocol Specification v0.4.4 2 | 3 | ## Overview 4 | 5 | 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. 6 | 7 | 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. 8 | 9 | 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. 10 | 11 | ## Changelog 12 | 13 | v0.4.4: 14 | * Relaxed the minimum chunk size requirement for large responses: >=512b chunks are recommended but not mandatory 15 | 16 | v0.4.3: 17 | * Removed download vs. upload, obsolete since v0.4 18 | 19 | v0.4.2: 20 | * Moved sample code to separate files 21 | 22 | v0.4.1: 23 | * Added link to Lagrange fork with guppy:// support 24 | * Added C server example 25 | * Added list of guppy:// URLs for testing 26 | * Explain why not Gopher 27 | 28 | v0.4: 29 | * Changed status codes and added prompts to make clients easier to implement by reusing existing Gemini or Spartan code 30 | * Added Python server examples 31 | * Added link to Kristall fork with ugly guppy:// support 32 | 33 | v0.3.2: 34 | => gemini://gemini.conman.org/boston/2023/10/25.1 (Response to warning by conman) 35 | * Define "session" 36 | * Add separate error handling sections 37 | * Add note about DoS 38 | * Various typo and formatting fixes 39 | * Add Python client samples 40 | 41 | v0.3.1: 42 | => gemini://gemini.ctrl-c.club/~tjp/gl/2023-10-25-guppy-v0.3.gmi (Response to feedback from tjp) 43 | * Drop v0.2 leftovers 44 | * Increase sequence number range 45 | * Clarify again that support for out-of-order transmission is not a must-have 46 | * Clarify retransmission related stuff 47 | * Put examples above the ugly details to make the spec easier to understand 48 | 49 | v0.3: 50 | * Out-of-order packets 51 | * Typo fixes 52 | * Overview clarification 53 | * Clarification about minimum packet size 54 | 55 | v0.2: 56 | => gemini://tilde.pink/~slondr/re-the-guppy-protocol.gmi (Response to feedback from slondr) 57 | * Packet sizes can go >= 512 bytes 58 | * Success, continuation and EOF packets now have an increasing sequence number in the header 59 | * Servers are allowed to re-transmit packets 60 | * Clients must ignore duplicate packets 61 | * Clients must acknowledge EOF 62 | * Typo and silly mistake fixes 63 | * Terminology section 64 | * Sample client and server 65 | 66 | v0.1: 67 | * First version 68 | 69 | ## Examples 70 | 71 | ### Success - Single Packet 72 | 73 | If the URL is guppy://localhost/a and the response is "# Title 1\n": 74 | 75 | ``` 76 | > guppy://localhost/a\r\n (request) 77 | < 566837578 text/gemini\r\n# Title 1\n (response) 78 | > 566837578\r\n (acknowledgment) 79 | < 566837579\r\n (end-of-file) 80 | > 566837579\r\n (acknowledgment) 81 | ``` 82 | 83 | ### Success - Single Packet with User Input 84 | 85 | If the URL is guppy://localhost/a and input is "b c": 86 | 87 | ``` 88 | > guppy://localhost/a?b%20c\r\n 89 | < 566837578 text/gemini\r\n# Title 1\n 90 | > 566837578\r\n 91 | < 566837579\r\n 92 | > 566837579\r\n 93 | ``` 94 | 95 | ### Success - Multiple Packets 96 | 97 | If the URL is guppy://localhost/a and the response is "# Title 1\nParagraph 1\n": 98 | 99 | ``` 100 | > guppy://localhost/a\r\n 101 | < 566837578 text/gemini\r\n# Title 1\n 102 | > 566837578\r\n 103 | < 566837579\r\nParagraph 1 104 | > 566837579\r\n 105 | < 566837580\r\n\n 106 | > 566837580\r\n 107 | < 566837581\r\n 108 | > 566837581\r\n 109 | ``` 110 | 111 | ### Success - Multiple Packets With Out of Order Packets 112 | 113 | If the URL is guppy://localhost/a and the response is "# Title 1\nParagraph 1\n": 114 | 115 | ``` 116 | > guppy://localhost/a\r\n 117 | < 566837578 text/gemini\r\n# Title 1\n 118 | < 566837579\r\nParagraph 1 119 | > 566837578\r\n 120 | < 566837579\r\nParagraph 1 121 | > 566837579\r\n 122 | < 566837579\r\nParagraph 1 123 | < 566837580\r\n\n 124 | > 566837580\r\n 125 | < 566837581\r\n 126 | > 566837581\r\n 127 | ``` 128 | 129 | ### Success - Simple Client, Sophisticated Server 130 | 131 | If the URL is guppy://localhost/a and the response is "# Title 1\nParagraph 1\n": 132 | 133 | ``` 134 | > guppy://localhost/a\r\n 135 | < 566837578 text/gemini\r\n# Title 1\n 136 | < 566837579\r\nParagraph 1 (server sends packet 566837579 without waiting for the client to acknowledge 566837578) 137 | > 566837578\r\n 138 | < 566837579\r\nParagraph 1 (server sends packet 566837579 again because the client didn't acknowledge it) 139 | < 566837580\r\n\n (server sends packet 566837580 without waiting for the client to acknowledge 566837579) 140 | > 566837579\r\n 141 | < 566837580\r\n\n (server sends packet 566837580 again because the client didn't acknowledge it) 142 | < 566837581\r\n (server sends packet 566837581 without waiting for the client to acknowledge 566837580) 143 | > 566837580\r\n 144 | < 566837581\r\n (server sends packet 566837581 again because the client didn't acknowledge it) 145 | > 566837581\r\n 146 | ``` 147 | 148 | ### Success - Multiple Packets With Unreliable Network 149 | 150 | If the URL is guppy://localhost/a and the response is "# Title 1\nParagraph 1\n": 151 | 152 | ``` 153 | > guppy://localhost/a\r\n 154 | < 566837578 text/gemini\r\n# Title 1\n 155 | > 566837578\r\n 156 | < 566837578 text/gemini\r\n# Title 1\n (acknowledgement arrived after the server re-transmitted the success packet) 157 | < 566837579\r\nParagraph 1 158 | < 566837579\r\nParagraph 1 (first continuation packet was lost) 159 | > 566837579\r\n 160 | < 566837580\r\n\n 161 | > 566837580\r\n 162 | > 566837580\r\n (first acknowledgement packet was lost and the client re-transmitted it while waiting for a continuation or EOF packet) 163 | < 566837581\r\n (server sends EOF after receiving the re-transmitted acknowledgement packet) 164 | < 566837581\r\n (first EOF packet was lost while server waits for client to acknowledge EOF) 165 | > 566837581\r\n 166 | ``` 167 | 168 | ### Input prompt 169 | 170 | ``` 171 | > guppy://localhost/greet\r\n 172 | < 1 Your name\r\n 173 | > guppy://localhost/greet?Guppy\r\n 174 | < 566837578 text/gemini\r\nHello Guppy\n 175 | > 566837578\r\n 176 | < 566837579\r\n 177 | > 566837579\r\n 178 | ``` 179 | 180 | ### Redirect - Absolute URL 181 | 182 | ``` 183 | > guppy://localhost/a\r\n 184 | < 3 guppy://localhost/b\r\n 185 | ``` 186 | 187 | ### Redirect - Relative URL 188 | 189 | ``` 190 | > guppy://localhost/a\r\n 191 | < 3 /b\r\n 192 | ``` 193 | 194 | ### Error 195 | 196 | ``` 197 | > guppy://localhost/search\r\n 198 | < 4 No search keywords specified\r\n 199 | ``` 200 | 201 | ## Terminology 202 | 203 | "Must" means a strict requirement, a rule all conformant Guppy client or server must obey. 204 | 205 | "Should" means a recommendation, something minimal clients or servers should do. 206 | 207 | "May" means a soft recommendation, something good clients or servers should do. 208 | 209 | ## URLs 210 | 211 | If no port is specified in a guppy:// URL, clients and servers must fall back to 6775 ('gu'). 212 | 213 | ## MIME Types 214 | 215 | Interactive clients must be able to display text/plain documents. 216 | 217 | Interactive clients must be able to parse text/gemini (without the Spartan := type) documents and allow users to follow links. 218 | 219 | 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. 220 | 221 | ## Security and Privacy 222 | 223 | The protocol is unencrypted, and these concerns are beyond the scope of this document. 224 | 225 | ## Limits 226 | 227 | Clients and servers may restrict packet size, to allow slower but more reliable transfer. 228 | 229 | Requests (the URL plus 2 bytes for the trailing \r\n) must fit in 2048 bytes. 230 | 231 | ## Packet Order 232 | 233 | Servers should transmit multiple packets at once, instead of waiting for the client to acknolwedge a packet before sending the next one. 234 | 235 | 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. 236 | 237 | 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. 238 | 239 | 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. 240 | 241 | 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. 242 | 243 | 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. 244 | 245 | ## Chunked Responses 246 | 247 | The server may send a chunked response, by sending one or more continuation packets. 248 | 249 | Servers should transmit responses larger than 512 bytes in chunks of at least 512 bytes. If the response is less than 512 bytes, servers should send it as one piece, without continuation packets. 250 | 251 | Clients should start displaying the response as soon as the first chunk is received. 252 | 253 | Clients must not assume that the response is split on a line boundary: a long line may be sent in multiple response packets. 254 | 255 | 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. 256 | 257 | ## Sessions 258 | 259 | Clients must use the same source port for all packets they send within one "session". 260 | 261 | 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. 262 | 263 | Servers must ignore additional request packets and duplicate acknowledgement packets in each session. 264 | 265 | Servers should limit the number of active sessions, to protect themselves against denial of service. 266 | 267 | Servers should end each session on timeout, by ignoring incoming packets and not sending any packets. 268 | 269 | 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. 270 | 271 | ## Lost Packets 272 | 273 | Clients should re-transmit request and acknowledgement packets after a while, if nothing is received from the server. 274 | 275 | 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. 276 | 277 | The server should re-transmit a success, continuation or EOF packet after a while, if not acknowledged by the client. 278 | 279 | Servers must ignore duplicate acknowledgement packets and additional request packets in the same session. 280 | 281 | Clients must wait for the "end of file" packet, to differentiate between timeout, a partially received response and a successfully received response. 282 | 283 | ## Packet Types 284 | 285 | There are 8 packet types: 286 | * Request 287 | * Success 288 | * Continuation 289 | * End-of-file 290 | * Acknowledgement 291 | * Input prompt 292 | * Redirect 293 | * Error 294 | 295 | All packets begin with a "header", followed by \r\n. 296 | 297 | TL;DR - 298 | * The server responds to a request (a URL) by sending a success, input, redirect or error packet 299 | * A success packet consist of a sequence number, a MIME type and data 300 | * A continuation packet is a success packet without a MIME type 301 | * The end of the response is marked by a continuation packet without data (an end-of-file packet) 302 | * The client acknowledges every packet by sending its sequence number back to the server 303 | 304 | ### Requests 305 | 306 | ``` 307 | url\r\n 308 | ``` 309 | 310 | The query part specifies user-provided input, percent-encoded. 311 | 312 | The server must respond with a success, input prompt, redirect or error packet. 313 | 314 | ### Success 315 | 316 | ``` 317 | seq type\r\n 318 | data 319 | ``` 320 | 321 | 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. 322 | 323 | The type field specifies the response MIME type and must not be empty. 324 | 325 | ### Continuation 326 | 327 | ``` 328 | seq\r\n 329 | data 330 | ``` 331 | 332 | The sequence number must increase by 1 in every continuation packet. 333 | 334 | ### End-of-file 335 | 336 | ``` 337 | seq\r\n 338 | ``` 339 | 340 | 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. 341 | 342 | ### Acknowledgement 343 | 344 | ``` 345 | seq\r\n 346 | ``` 347 | 348 | The client must acknowledge every success, continuation or EOF packet by echoing its sequence number back to the server. 349 | 350 | ### Input prompt 351 | 352 | ``` 353 | 1 prompt\r\n 354 | ``` 355 | 356 | The client must show the prompt to the user and allow the user to repeat the request, this time with the user's input. 357 | 358 | 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. 359 | 360 | 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. 361 | 362 | ### Redirect 363 | 364 | ``` 365 | 3 url\r\n 366 | ``` 367 | 368 | The URL may be relative. 369 | 370 | The client must inform the user of the redirection. 371 | 372 | The client may remember redirected URLs. The server must not assume clients don't do this. 373 | 374 | The client should limit the number of redirects during the handling of a single request. 375 | 376 | The client may forbid a series of redirects, and may prompt the user to confirm each redirect. 377 | 378 | ### Error 379 | 380 | ``` 381 | 4 error\r\n 382 | ``` 383 | 384 | Clients must display the error to the user. 385 | 386 | ## Response to feedback 387 | 388 | > 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 389 | 390 | This is intentional: errors are for the user, not for the client. They should be human-readable. 391 | 392 | > 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. 393 | 394 | 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. 395 | 396 | > Using an Error Packet to signify user input: 397 | > * user downloads gemtext 398 | > ... 399 | 400 | 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. 401 | 402 | > It seems like this probably was changed but the acknowledgement section wasn't updated. 403 | > [...] 404 | > This contradicts just about everything said elsewhere about out-of-order packet handling so it probably just wasn't updated in some prior iteration. 405 | 406 | True. 407 | 408 | > 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. 409 | 410 | True. Although we're talking about small text files here, I increased the sequence number range to allow larger transfers. 411 | 412 | > 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. 413 | 414 | 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. 415 | 416 | > 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. 417 | 418 | 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 :) 419 | 420 | > It's a classic mistake to look at complicated machinery like TCP and assume it's bloated. 421 | 422 | 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). 423 | 424 | > 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. 425 | 426 | 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. 427 | 428 | > 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. 429 | 430 | 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. 431 | 432 | 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. 433 | -------------------------------------------------------------------------------- /guppyc1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Copyright (c) 2023 Dima Krasner 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import socket 24 | import sys 25 | from urllib.parse import urlparse 26 | import select 27 | 28 | s = socket.socket(type=socket.SOCK_DGRAM) 29 | url = urlparse(sys.argv[1]) 30 | s.connect((url.hostname, 6775)) 31 | 32 | request = (sys.argv[1] + "\r\n").encode('utf-8') 33 | 34 | sys.stderr.write(f"Sending request for {sys.argv[1]}\n") 35 | s.send(request) 36 | 37 | buffered = b'' 38 | mime_type = None 39 | tries = 0 40 | last_buffered = 0 41 | chunks = {} 42 | while True: 43 | ready, _, _ = select.select([s.fileno()], [], [], 2) 44 | 45 | # if we still haven't received anything from the server, retry the request 46 | if len(chunks) == 0 and not ready: 47 | if tries > 5: 48 | raise Exception("All 5 tries have failed") 49 | 50 | sys.stderr.write(f"Retrying request for {sys.argv[1]}\n") 51 | s.send(request) 52 | tries += 1 53 | continue 54 | 55 | # if we're waiting for packet n+1, retry ack packet n 56 | if not ready and last_buffered > 0: 57 | sys.stderr.write(f"Retrying ack for packet {last_buffered}\n") 58 | s.send(f"{last_buffered}\r\n".encode('utf-8')) 59 | continue 60 | 61 | # receive and parse the next packet 62 | pkt = s.recv(4096) 63 | crlf = pkt.index(b'\r\n') 64 | header = pkt[:crlf] 65 | 66 | try: 67 | # parse the success packet header 68 | space = header.index(b' ') 69 | seq = int(header[:space]) 70 | mime_type = header[space + 1:] 71 | 72 | if seq == 4: 73 | raise Exception(f"Error: {mime_type.decode('utf-8')}") 74 | 75 | if seq == 3: 76 | raise Exception(f"Redirected to {mime_type.decode('utf-8')}") 77 | 78 | if seq == 1: 79 | raise Exception(f"Input required: {mime_type.decode('utf-8')}") 80 | 81 | if seq < 6: 82 | raise Exception(f"Invalid status code: {seq}") 83 | except ValueError as e: 84 | # parse the continuation or EOF packet header 85 | seq = int(header) 86 | 87 | if seq in chunks: 88 | sys.stderr.write(f"Ignoring duplicate packet {seq} and resending ack\n") 89 | s.send(f"{seq}\r\n".encode('utf-8')) 90 | continue 91 | 92 | if last_buffered == 0 and mime_type is not None: 93 | sys.stderr.write(f"Response is of type {mime_type.decode('utf-8')}\n") 94 | 95 | sys.stderr.write(f"Sending ack for packet {seq}\n") 96 | s.send(f"{seq}\r\n".encode('utf-8')) 97 | 98 | data = pkt[crlf + 2:] 99 | if last_buffered == 0 or seq == last_buffered + 1: 100 | sys.stderr.write(f"Received packet {seq} with {len(data)} bytes of data\n") 101 | else: 102 | sys.stderr.write(f"Received out-of-order packet {seq} with {len(data)} bytes of data\n") 103 | 104 | chunks[seq] = data 105 | 106 | # concatenate the consequentive response chunks we have 107 | while (last_buffered == 0 and mime_type is not None) or seq == last_buffered + 1: 108 | data = chunks[seq] 109 | sys.stderr.write(f"Queueing packet {seq} for display\n") 110 | buffered += data 111 | last_buffered = seq 112 | 113 | # print the buffered text if we can 114 | try: 115 | print(buffered.decode('utf-8')) 116 | last_buffered = seq 117 | sys.stderr.write("Flushed the buffer to screen\n") 118 | buffered = b'' 119 | except UnicodeDecodeError: 120 | sys.stderr.write("Cannot print buffered text until valid UTF-8\n") 121 | continue 122 | 123 | # stop once we printed everything until the end-of-file packet 124 | if not chunks[last_buffered]: 125 | sys.stderr.write("Reached end of document\n") 126 | break -------------------------------------------------------------------------------- /guppyc2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Copyright (c) 2023 Dima Krasner 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import socket 24 | import sys 25 | from urllib.parse import urlparse 26 | import select 27 | 28 | s = socket.socket(type=socket.SOCK_DGRAM) 29 | url = urlparse(sys.argv[1]) 30 | s.connect((url.hostname, 6775)) 31 | 32 | request = (sys.argv[1] + "\r\n").encode('utf-8') 33 | 34 | sys.stderr.write(f"Sending request for {sys.argv[1]}\n") 35 | s.send(request) 36 | 37 | buffered = b'' 38 | mime_type = None 39 | tries = 0 40 | last_buffered = 0 41 | while True: 42 | ready, _, _ = select.select([s.fileno()], [], [], 2) 43 | 44 | # if we still haven't received anything from the server, retry the request 45 | if last_buffered == 0 and not ready: 46 | if tries > 5: 47 | raise Exception("All 5 tries have failed") 48 | 49 | sys.stderr.write(f"Retrying request for {sys.argv[1]}\n") 50 | s.send(request) 51 | tries += 1 52 | continue 53 | 54 | # if we're waiting for packet n+1, retry ack packet n 55 | if not ready and last_buffered > 0: 56 | sys.stderr.write(f"Retrying ack for packet {last_buffered}\n") 57 | s.send(f"{last_buffered}\r\n".encode('utf-8')) 58 | continue 59 | 60 | # receive and parse the next packet 61 | pkt = s.recv(4096) 62 | crlf = pkt.index(b'\r\n') 63 | header = pkt[:crlf] 64 | 65 | try: 66 | # parse the success packet header 67 | space = header.index(b' ') 68 | seq = int(header[:space]) 69 | mime_type = header[space + 1:] 70 | 71 | if seq == 4: 72 | raise Exception(f"Error: {mime_type.decode('utf-8')}") 73 | 74 | if seq == 3: 75 | raise Exception(f"Redirected to {mime_type.decode('utf-8')}") 76 | 77 | if seq == 1: 78 | raise Exception(f"Input required: {mime_type.decode('utf-8')}") 79 | 80 | if seq < 6: 81 | raise Exception(f"Invalid status code: {seq}") 82 | except ValueError as e: 83 | # parse the continuation or EOF packet header 84 | seq = int(header) 85 | 86 | # ignore this packet if it's not the packet we're waiting for: packet n+1 or the first packet 87 | if (last_buffered != 0 and seq != last_buffered + 1) or (last_buffered == 0 and mime_type is None): 88 | sys.stderr.write(f"Ignoring unexpected packet {seq} and sending ack\n") 89 | s.send(f"{seq}\r\n".encode('utf-8')) 90 | continue 91 | 92 | if last_buffered == 0 and mime_type is not None: 93 | sys.stderr.write(f"Response is of type {mime_type.decode('utf-8')}\n") 94 | 95 | sys.stderr.write(f"Sending ack for packet {seq}\n") 96 | s.send(f"{seq}\r\n".encode('utf-8')) 97 | 98 | data = pkt[crlf + 2:] 99 | sys.stderr.write(f"Received packet {seq} with {len(data)} bytes of data\n") 100 | 101 | # concatenate the consequentive response chunks we have 102 | sys.stderr.write(f"Queueing packet {seq} for display\n") 103 | buffered += data 104 | last_buffered = seq 105 | 106 | # print the buffered text if we can 107 | try: 108 | print(buffered.decode('utf-8')) 109 | sys.stderr.write("Flushed the buffer to screen\n") 110 | buffered = b'' 111 | except UnicodeDecodeError: 112 | sys.stderr.write("Cannot print buffered text until valid UTF-8\n") 113 | continue 114 | 115 | # stop once we printed everything until the end-of-file packet 116 | if not data: 117 | sys.stderr.write("Reached end of document\n") 118 | break -------------------------------------------------------------------------------- /guppys.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Dima Krasner 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | 39 | #define PORT "6775" 40 | #define MAX_SESSIONS 32 41 | #define MAX_CHUNKS 8 42 | #define CHUNK_SIZE 512 43 | 44 | int main(int argc, char *argv[]) 45 | { 46 | static struct { 47 | struct { 48 | struct sockaddr_storage peer; 49 | char addrstr[INET6_ADDRSTRLEN]; 50 | unsigned short port; 51 | struct timespec started; 52 | int fd, first, next, last; 53 | struct { 54 | char data[CHUNK_SIZE]; 55 | struct timespec sent; 56 | ssize_t len; 57 | int seq; 58 | } chunks[MAX_CHUNKS]; 59 | } sessions[MAX_SESSIONS]; 60 | } server; 61 | static char buf[2049]; 62 | char *end; 63 | struct pollfd pfd; 64 | const char *path, *errstr; 65 | const void *saddr; 66 | struct addrinfo hints = { 67 | .ai_family = AF_INET6, 68 | .ai_flags = AI_PASSIVE | AI_V4MAPPED, 69 | .ai_socktype = SOCK_DGRAM 70 | }, *addr; 71 | struct sockaddr_storage peer; 72 | struct timespec now; 73 | const struct sockaddr_in *peerv4 = (const struct sockaddr_in *)&peer; 74 | const struct sockaddr_in6 *peerv6 = (const struct sockaddr_in6 *)&peer; 75 | long seq; 76 | ssize_t len; 77 | int s, sndbuf = MAX_SESSIONS * MAX_CHUNKS * CHUNK_SIZE, one = 1, off, ret; 78 | unsigned int slot, active = 0, i, j, ready, waiting; 79 | socklen_t peerlen; 80 | unsigned short sport; 81 | 82 | // start listening for packets and increase the buffer size for sending 83 | if (getaddrinfo(NULL, PORT, &hints, &addr) != 0) return EXIT_FAILURE; 84 | if ( 85 | (s = socket(addr->ai_family,addr->ai_socktype, addr->ai_protocol)) < 0 86 | ) { 87 | freeaddrinfo(addr); 88 | return EXIT_FAILURE; 89 | } 90 | if ( 91 | setsockopt(s, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)) < 0 || 92 | setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0 || 93 | bind(s, addr->ai_addr, addr->ai_addrlen) < 0 94 | ) { 95 | close(s); 96 | freeaddrinfo(addr); 97 | return EXIT_FAILURE; 98 | } 99 | freeaddrinfo(addr); 100 | 101 | // restrict access to files outside the working directory 102 | if (chroot(".") < 0) { 103 | close(s); 104 | return EXIT_FAILURE; 105 | } 106 | 107 | pfd.events = POLLIN; 108 | pfd.fd = s; 109 | 110 | srand((unsigned int)time(NULL)); 111 | 112 | for (i = 0; i < MAX_SESSIONS; ++i) { 113 | server.sessions[i].fd = -1; 114 | server.sessions[i].peer.ss_family = AF_UNSPEC; 115 | } 116 | 117 | while (1) { 118 | // wait for an incoming packet with timeout of 100ms if we have active sessions 119 | pfd.revents = 0; 120 | ready = poll(&pfd, 1, active ? 100 : -1); 121 | if (ready < 0) break; 122 | 123 | if (ready == 0 || !(pfd.revents & POLLIN)) goto respond; 124 | 125 | // receive a packet 126 | peerlen = sizeof(peer); 127 | if ( 128 | (len = recvfrom( 129 | s, 130 | buf, 131 | sizeof(buf) - 1, 132 | 0, 133 | (struct sockaddr *)&peer, 134 | &peerlen 135 | )) <= 0 136 | ) break; 137 | 138 | // the smallest valid packet we can receive is 6\r\n 139 | if (len < 3 || buf[len - 2] != '\r' || buf[len - 1] != '\n') continue; 140 | 141 | // check if this packet belongs to an existing session by comparing the 142 | // source address of the packet 143 | switch (peer.ss_family) { 144 | case AF_INET: 145 | sport = ntohs(peerv4->sin_port); 146 | saddr = &peerv4->sin_addr; 147 | break; 148 | case AF_INET6: 149 | sport = ntohs(peerv6->sin6_port); 150 | saddr = &peerv6->sin6_addr; 151 | break; 152 | default: 153 | continue; 154 | } 155 | 156 | slot = MAX_SESSIONS; 157 | 158 | for (i = sport % MAX_SESSIONS; i < MAX_SESSIONS; ++i) { 159 | if ( 160 | memcmp( 161 | &peer, 162 | &server.sessions[i].peer, 163 | sizeof(struct sockaddr_storage) 164 | ) == 0 165 | ) { 166 | slot = i; 167 | goto have_slot; 168 | } 169 | } 170 | 171 | for (i = 0; i < sport % MAX_SESSIONS; ++i) { 172 | if ( 173 | memcmp( 174 | &peer, 175 | &server.sessions[i].peer, 176 | sizeof(struct sockaddr_storage) 177 | ) == 0) { 178 | slot = i; 179 | break; 180 | } 181 | } 182 | 183 | have_slot: 184 | // if all slots are occupied, send an error packet 185 | if (slot == MAX_SESSIONS && active == MAX_SESSIONS) { 186 | fputs("Too many sessions", stderr); 187 | sendto( 188 | s, 189 | "4 Too many sessions\r\n", 190 | 21, 191 | 0, 192 | (const struct sockaddr *)&peer, 193 | peerlen 194 | ); 195 | continue; 196 | } 197 | 198 | // if no session matches this packet ... 199 | if (slot == MAX_SESSIONS) { 200 | // parse and validate the request 201 | if (len < 11 || memcmp(buf, "guppy://", 8)) continue; 202 | buf[len - 2] = '\0'; 203 | if ( 204 | !(path = strchr(&buf[8], '/')) || 205 | !path[0] || 206 | (path[0] == '/' && !path[1]) 207 | ) path = "index.gmi"; 208 | else ++path; 209 | 210 | // find an empty slot 211 | for (i = 0; i < MAX_SESSIONS; ++i) { 212 | if (server.sessions[i].fd < 0) { 213 | slot = i; 214 | break; 215 | } 216 | } 217 | 218 | if ( 219 | !inet_ntop( 220 | peer.ss_family, 221 | saddr, 222 | server.sessions[slot].addrstr, 223 | sizeof(server.sessions[slot].addrstr) 224 | ) 225 | ) continue; 226 | 227 | if ((server.sessions[slot].fd = open(path, O_RDONLY)) < 0) { 228 | errstr = strerror(errno); 229 | fprintf( 230 | stderr, 231 | "Failed to open %s for %s:%hu: %s\n", 232 | path, 233 | server.sessions[slot].addrstr, 234 | server.sessions[slot].port, 235 | errstr 236 | ); 237 | ret = snprintf(buf, sizeof(buf), "4 %s\r\n", errstr); 238 | if (ret > 0 && ret < sizeof(buf)) 239 | sendto( 240 | s, 241 | buf, 242 | (size_t)ret, 243 | 0, 244 | (const struct sockaddr *)&peer, 245 | peerlen 246 | ); 247 | continue; 248 | } 249 | 250 | memcpy( 251 | &server.sessions[slot].peer, 252 | &peer, 253 | sizeof(struct sockaddr_storage) 254 | ); 255 | server.sessions[slot].port = sport; 256 | server.sessions[slot].first = 6 + (rand() % SHRT_MAX); 257 | server.sessions[slot].next = server.sessions[slot].first; 258 | server.sessions[slot].last = 0; 259 | server.sessions[slot].started = now; 260 | for (i = 0; i < MAX_CHUNKS; ++i) 261 | server.sessions[slot].chunks[i].seq = 0; 262 | ++active; 263 | } else { 264 | // extract the sequence number from the acknowledgement packet 265 | buf[len] = '\0'; 266 | if ( 267 | (seq = strtol(buf, &end, 10)) < server.sessions[slot].first || 268 | (seq >= server.sessions[slot].next) || 269 | !end || 270 | len != (end - buf) + 2 271 | ) 272 | goto respond; 273 | 274 | // acknowledge the packet 275 | for (i = 0; i < MAX_CHUNKS; ++i) { 276 | if (server.sessions[slot].chunks[i].seq == seq) { 277 | fprintf( 278 | stderr, 279 | "%s:%hu has received %ld\n", 280 | server.sessions[slot].addrstr, 281 | server.sessions[slot].port, 282 | seq 283 | ); 284 | server.sessions[slot].chunks[i].seq = 0; 285 | } else if (server.sessions[slot].chunks[i].seq) ++waiting; 286 | } 287 | } 288 | 289 | // fill all free slots with more chunks that can be sent 290 | for (i = 0; i < MAX_CHUNKS && !server.sessions[slot].last; ++i) { 291 | if (server.sessions[slot].chunks[i].seq) continue; 292 | 293 | server.sessions[slot].chunks[i].seq = server.sessions[slot].next; 294 | 295 | if ( 296 | server.sessions[slot].chunks[i].seq == 297 | server.sessions[slot].first 298 | ) 299 | off = sprintf( 300 | server.sessions[slot].chunks[i].data, 301 | "%d text/gemini\r\n", 302 | server.sessions[slot].chunks[i].seq 303 | ); 304 | else 305 | off = sprintf( 306 | server.sessions[slot].chunks[i].data, 307 | "%d\r\n", 308 | server.sessions[slot].chunks[i].seq 309 | ); 310 | 311 | server.sessions[slot].chunks[i].len = read( 312 | server.sessions[slot].fd, 313 | &server.sessions[slot].chunks[i].data[off], 314 | sizeof(server.sessions[slot].chunks[i].data) - off 315 | ); 316 | if (server.sessions[slot].chunks[i].len < 0) { 317 | fprintf( 318 | stderr, 319 | "Failed to read file for %s:%hu: %s\n", 320 | server.sessions[slot].addrstr, 321 | server.sessions[slot].port, 322 | strerror(errno) 323 | ); 324 | server.sessions[slot].chunks[i].len = off; 325 | } else if (server.sessions[slot].chunks[i].len == 0) 326 | server.sessions[slot].chunks[i].len = off; 327 | else server.sessions[slot].chunks[i].len += off; 328 | 329 | if ( 330 | server.sessions[slot].chunks[i].len == off && 331 | !server.sessions[slot].last 332 | ) server.sessions[slot].last = server.sessions[slot].chunks[i].seq; 333 | server.sessions[slot].chunks[i].sent.tv_sec = 0; 334 | 335 | ++server.sessions[slot].next; 336 | } 337 | 338 | respond: 339 | if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) break; 340 | 341 | for (i = 0; i < MAX_SESSIONS; ++i) { 342 | if (server.sessions[i].fd < 0) continue; 343 | 344 | // terminate sessions after 20s 345 | if (now.tv_sec > server.sessions[i].started.tv_sec + 20) { 346 | fprintf( 347 | stderr, 348 | "%s:%hu has timed out\n", 349 | server.sessions[i].addrstr, 350 | server.sessions[i].port 351 | ); 352 | close(server.sessions[i].fd); 353 | server.sessions[i].fd = -1; 354 | --active; 355 | continue; 356 | } 357 | 358 | // send unacknowledged chunks if not sent or every 2s 359 | waiting = 0; 360 | for (j = 0; j < MAX_CHUNKS; ++j) { 361 | if (server.sessions[i].chunks[j].seq == 0) continue; 362 | 363 | ++waiting; 364 | 365 | if (server.sessions[i].chunks[j].sent.tv_sec == 0) fprintf( 366 | stderr, "Sending %d to %s:%hu\n", 367 | server.sessions[i].chunks[j].seq, 368 | server.sessions[i].addrstr, 369 | server.sessions[i].port 370 | ); 371 | else if ( 372 | now.tv_sec < server.sessions[i].chunks[j].sent.tv_sec + 2 373 | ) continue; 374 | else fprintf( 375 | stderr, 376 | "Resending %d to %s:%hu\n", 377 | server.sessions[i].chunks[j].seq, 378 | server.sessions[i].addrstr, 379 | server.sessions[i].port 380 | ); 381 | 382 | if ( 383 | sendto( 384 | s, 385 | server.sessions[i].chunks[j].data, 386 | server.sessions[i].chunks[j].len, 387 | 0, 388 | (const struct sockaddr *)&server.sessions[i].peer, 389 | sizeof(server.sessions[i].peer) 390 | ) < 0 391 | ) fprintf( 392 | stderr, 393 | "Failed to send packet to %s:%hu: %s\n", 394 | server.sessions[i].addrstr, 395 | server.sessions[i].port, 396 | strerror(errno) 397 | ); 398 | else server.sessions[i].chunks[j].sent = now; 399 | } 400 | 401 | // if all packets are acknowledged, terminate the session 402 | if (waiting == 1) 403 | fprintf( 404 | stderr, 405 | "%s:%hu has 1 pending packet\n", 406 | server.sessions[i].addrstr, 407 | server.sessions[i].port 408 | ); 409 | else if (waiting > 1) 410 | fprintf( 411 | stderr, 412 | "%s:%hu has %d pending packets\n", 413 | server.sessions[i].addrstr, 414 | server.sessions[i].port, 415 | waiting 416 | ); 417 | else { 418 | fprintf( 419 | stderr, 420 | "%s:%hu has received all packets\n", 421 | server.sessions[i].addrstr, 422 | server.sessions[i].port 423 | ); 424 | close(server.sessions[i].fd); 425 | server.sessions[i].fd = -1; 426 | --active; 427 | } 428 | } 429 | } 430 | 431 | for (i = 0; i < MAX_SESSIONS; ++i) { 432 | if (server.sessions[i].fd >= 0) close(server.sessions[i].fd); 433 | } 434 | close(s); 435 | return EXIT_SUCCESS; 436 | } 437 | -------------------------------------------------------------------------------- /guppys.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Copyright (c) 2023 Dima Krasner 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import socket 24 | from urllib.parse import urlparse, unquote 25 | import select 26 | import random 27 | import io 28 | import time 29 | import sys 30 | import logging 31 | import collections 32 | 33 | home = """# Home 34 | 35 | => /lorem Lorem ipsum 36 | => /echo Echo 37 | => /rick.mp4 Rick Astley - Never Gonna Give You Up 38 | """ 39 | 40 | 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. 41 | 42 | 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. 43 | 44 | 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. 45 | 46 | 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. 47 | 48 | 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. 49 | 50 | 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. 51 | 52 | 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. 53 | 54 | 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. 55 | 56 | 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. 57 | 58 | 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. 59 | 60 | 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. 61 | 62 | 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. 63 | 64 | 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. 65 | 66 | 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. 67 | 68 | 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. 69 | 70 | 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. 71 | 72 | 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. 73 | 74 | 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. 75 | 76 | 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. 77 | 78 | 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. 79 | """ 80 | 81 | class SessionTimeoutException(Exception): 82 | pass 83 | 84 | class Chunk: 85 | def __init__(self, seq, header, data=b''): 86 | self.seq = seq 87 | self.raw = header.encode('utf-8') + data 88 | self.eof = len(data) == 0 89 | 90 | class Response: 91 | def __init__(self, mime_type, f): 92 | self.mime_type = mime_type 93 | self.seq = random.randint(20000, 65536) 94 | self.start = self.seq 95 | self.f = f 96 | self.chunks = [] 97 | self.eof = None 98 | 99 | def close(self): 100 | self.f.close() 101 | 102 | def __iter__(self): 103 | self.pos = 0 104 | return self 105 | 106 | def __next__(self): 107 | if self.pos < len(self.chunks): 108 | seq, chunk = self.chunks[self.pos] 109 | self.pos += 1 110 | return seq, chunk 111 | 112 | if self.eof: 113 | raise StopIteration() 114 | 115 | data = self.f.read(chunk_size) 116 | 117 | self.seq += 1 118 | logging.debug(f"Building chunk {self.seq}") 119 | 120 | if not data: 121 | chunk = Chunk(self.seq, f"{self.seq}\r\n") 122 | self.eof = self.seq 123 | elif self.mime_type: 124 | chunk = Chunk(self.seq, f"{self.seq} {self.mime_type}\r\n", data) 125 | self.mime_type = None 126 | else: 127 | chunk = Chunk(self.seq, f"{self.seq}\r\n", data) 128 | 129 | self.chunks.append((self.seq, chunk)) 130 | self.pos += 1 131 | return self.seq, chunk 132 | 133 | def ack(self, seq): 134 | self.chunks = [(oseq, chunk) for oseq, chunk in self.chunks if oseq != seq] 135 | 136 | def sent(self): 137 | return self.eof and not self.chunks 138 | 139 | class Session: 140 | def __init__(self, sock, src, mime_type, f, chunk_size): 141 | self.sock = sock 142 | self.src = src 143 | self.mime_type = mime_type 144 | self.sent = {} 145 | self.started = time.time() 146 | 147 | self.response = Response(mime_type, f) 148 | 149 | def close(self): 150 | self.response.close() 151 | 152 | def send(self): 153 | if time.time() > self.started + 30: 154 | raise SessionTimeoutException() 155 | 156 | unacked = 0 157 | 158 | now = time.time() 159 | for seq, chunk in self.response: 160 | sent = self.sent.get(seq, 0) 161 | if sent < now - 2: 162 | if sent: 163 | logging.info(f"Re-sending {seq} ({unacked}/8) to {self.src}") 164 | else: 165 | logging.info(f"Sending {seq} ({unacked}/8) to {self.src}") 166 | self.sock.sendto(chunk.raw, self.src) 167 | self.sent[seq] = now 168 | 169 | # allow only 8 chunks awaiting acknowledgement at a time 170 | unacked += 1 171 | if unacked == 8: 172 | break 173 | 174 | def ack(self, seq): 175 | if seq not in self.sent: 176 | raise Exception(f"Unknown packet for {self.src}: {seq}") 177 | 178 | logging.info(f"{self.src} has received {seq}") 179 | self.response.ack(seq) 180 | 181 | return self.response.sent() 182 | 183 | logging.basicConfig(level=logging.INFO) 184 | 185 | sock = socket.socket(type=socket.SOCK_DGRAM) 186 | sock.bind(('', 6775)) 187 | 188 | sessions = collections.OrderedDict() 189 | while True: 190 | ready, _, _ = select.select([sock.fileno()], [], [], 0.1) 191 | 192 | finished = [] 193 | 194 | if ready: 195 | pkt, src = sock.recvfrom(2048) 196 | if pkt.endswith(b'\r\n'): 197 | try: 198 | session = sessions.get(src) 199 | if session: 200 | try: 201 | seq = int(pkt[:len(pkt) - 2]) 202 | except ValueError as e: 203 | # probably a duplicate request packet packet 204 | logging.exception("Received invalid packet", e) 205 | else: 206 | if session.ack(seq): 207 | logging.info(f"Session {src} has ended successfully") 208 | finished.append(src) 209 | else: 210 | if len(sessions) > 32: 211 | raise Exception("Too many sessions") 212 | 213 | if pkt.startswith(b'guppy://'): 214 | url = urlparse(pkt.decode('utf-8')) 215 | 216 | mime_type = "text/gemini" 217 | chunk_size = 512 218 | 219 | if url.path == '' or url.path == '/': 220 | f = io.BytesIO(home.encode('utf-8')) 221 | chunk_size = 512 222 | elif url.path == '/lorem': 223 | f = io.BytesIO(lorem_ipsum.encode('utf-8')) 224 | elif url.path == '/echo': 225 | mime_type = "text/plain" 226 | data = unquote(url.query).encode('utf-8') 227 | if not data: 228 | sock.sendto(b'1 Your name\r\n', src) 229 | raise Exception("Your name") 230 | 231 | f = io.BytesIO(data) 232 | elif url.path == '/rick.mp4': 233 | mime_type = "video/mp4" 234 | f = open('/tmp/rick.mp4', 'rb') 235 | chunk_size = 2048 236 | else: 237 | raise Exception(f"Invalid path") 238 | 239 | sessions[src] = Session(sock, src, mime_type, f, chunk_size) 240 | except Exception as e: 241 | logging.exception("Unhandled exception", e) 242 | 243 | for src, session in sessions.items(): 244 | try: 245 | session.send() 246 | except SessionTimeoutException: 247 | logging.info(f"Session {src} has timed out") 248 | finished.append(src) 249 | except Exception as e: 250 | logging.exception("Unhandled exception", e) 251 | 252 | for src in finished: 253 | sessions[src].close() 254 | sessions.pop(src) -------------------------------------------------------------------------------- /index.gmi: -------------------------------------------------------------------------------- 1 | # The Guppy Protocol 2 | 3 | ``` 4 | .. .;,,;,';. 5 | .,xO0K0Ox;. ..;;:::;,:,;,; 6 | .ck0KKKKKKK0kc. .';::';,',','''. 7 | 'd0KKKKKKKKKK0KOo. ':cc;,''''''''''. 8 | ,xKKKKKKKKKKKKK00KOd, .::,,'''''..''.'.......... 9 | ,xKKKKKKKKKKKKK0kxddxd' ......'.......................... 10 | l0KKKKKKKKKKK0d:;;;;,','....,;;,'.................................. 11 | l0KKKKKKKKKKOc;;;;;,,'.................................................. 12 | c0KKKKKKKKK0c;;;;;,,'...................................................''.. 13 | :OKKKKKKKKKx;;;;;,,,..................................................';;,;;'... 14 | ;kKKKKKKKKKk;,,,,,,,..................................................,:. .:;.. 15 | 'xKKKKKKKKK0x;;,,,,,...................................................;c:c,' 16 | .o0KKKKKKKKKKk:,,,,,'..........................................'.......... 17 | 'okO000000OOOxc:;,'.. ......................................''...... 18 | ........................... 19 | ................. 20 | lkdc:.... 21 | cOkxo. 22 | ' 23 | ``` 24 | 25 | Guppy is a simple protocol for quick transfer of small documents, inspired by Gemini, Spartan, Gopher and TFTP. In a nutshell, it's Gemini without TLS and some status codes, over UDP. 26 | 27 | => guppy-spec.gmi Protocol specification 28 | 29 | Servers: 30 | 31 | => https://github.com/dimkr/tootik tootik 32 | => guppys.c Sample server in C 33 | => guppys.py Sample server in Python 34 | 35 | Clients: 36 | 37 | => https://gmi.skyjake.fi/lagrange/ Lagrange (since 1.18.0) 38 | => https://github.com/MasterQ32/kristall Kristall (since b2fac23) 39 | => https://github.com/dimkr/gplaces gplaces (since 1.18.0) 40 | => guppyc1.py Sample client in Python 41 | => guppyc2.py Simpler but slower sample client in Python 42 | 43 | Sites: 44 | 45 | => guppy://guppy.000090000.xyz/ 46 | => guppy://hd.206267.xyz/ 47 | => guppy://gemini.dimakrasner.com/ 48 | --------------------------------------------------------------------------------