+(defun elpher-get-gemini-response (address renderer &optional force-ipv4)
+ "Retrieve gemini ADDRESS, then render using RENDERER.
+If FORCE-IPV4 is non-nil, explicitly look up and use IPv4 address corresponding
+to ADDRESS."
+ (if (not (gnutls-available-p))
+ (error "Cannot establish gemini connection: GnuTLS not available")
+ (unless (< (elpher-address-port address) 65536)
+ (error "Cannot establish gemini connection: port number > 65536"))
+ (condition-case nil
+ (let* ((kill-buffer-query-functions nil)
+ (port (elpher-address-port address))
+ (host (elpher-address-host address))
+ (response-string "")
+ (proc (open-network-stream "elpher-process"
+ nil
+ (if force-ipv4 (dns-query host) host)
+ (if (> port 0) port 1965)
+ :type 'tls
+ :nowait t))
+ (timer (run-at-time elpher-connection-timeout nil
+ (lambda ()
+ (elpher-process-cleanup)
+ (unless force-ipv4
+ ; Try again with IPv4
+ (message "Connection timed out. Retrying with IPv4.")
+ (elpher-get-gemini-response address renderer t))))))
+ (setq elpher-network-timer timer)
+ (set-process-coding-system proc 'binary)
+ (set-process-filter proc
+ (lambda (_proc string)
+ (cancel-timer timer)
+ (setq response-string
+ (concat response-string string))))
+ (set-process-sentinel proc
+ (lambda (proc event)
+ (condition-case the-error
+ (cond
+ ((string-prefix-p "open" event) ; request URL
+ (let ((inhibit-eol-conversion t))
+ (process-send-string
+ proc
+ (concat (elpher-address-to-url address)
+ "\r\n"))))
+ ((string-prefix-p "deleted" event)) ; do nothing
+ ((and (string-empty-p response-string)
+ (not force-ipv4))
+ ; Try again with IPv4
+ (message "Connection failed. Retrying with IPv4.")
+ (cancel-timer timer)
+ (elpher-get-gemini-response address renderer t))
+ (t
+ (funcall #'elpher-process-gemini-response
+ response-string
+ renderer)
+ (elpher-restore-pos)))
+ (error
+ (elpher-network-error address the-error))))))
+ (error
+ (error "Error initiating connection to server")))))
+
+(defun elpher-parse-gemini-response (response)
+ "Parse the RESPONSE string and return a list of components.
+The list is of the form (code meta body). A response of nil implies
+that the response was malformed."
+ (let ((header-end-idx (string-match "\r\n" response)))
+ (if header-end-idx
+ (let ((header (string-trim (substring response 0 header-end-idx)))
+ (body (substring response (+ header-end-idx 2))))
+ (if (>= (length header) 2)
+ (let ((code (substring header 0 2))
+ (meta (string-trim (substring header 2))))
+ (list code meta body))
+ (error "Malformed response: No response status found in header %s" header)))
+ (error "Malformed response: No CRLF-delimited header found"))))
+
+(defun elpher-process-gemini-response (response-string renderer)
+ "Process the gemini response RESPONSE-STRING and pass the result to RENDERER."
+ (let ((response-components (elpher-parse-gemini-response response-string)))
+ (let ((response-code (elt response-components 0))
+ (response-meta (elt response-components 1))
+ (response-body (elt response-components 2)))
+ (pcase (elt response-code 0)
+ (?1 ; Input required
+ (elpher-with-clean-buffer
+ (insert "Gemini server is requesting input."))
+ (let* ((query-string (read-string (concat response-meta ": ")))
+ (url (elpher-address-to-url (elpher-node-address elpher-current-node)))
+ (query-address (elpher-address-from-url (concat url "?" query-string))))
+ (elpher-get-gemini-response query-address renderer)))
+ (?2 ; Normal response
+ (funcall renderer response-body response-meta))
+ (?3 ; Redirect
+ (message "Following redirect to %s" response-meta)
+ (if (>= (length elpher-gemini-redirect-chain) 5)
+ (error "More than 5 consecutive redirects followed"))
+ (let ((redirect-address (elpher-address-from-gemini-url response-meta)))
+ (if (member redirect-address elpher-gemini-redirect-chain)
+ (error "Redirect loop detected"))
+ (if (not (string= (elpher-address-protocol redirect-address)
+ "gemini"))
+ (error "Server tried to automatically redirect to non-gemini URL: %s"
+ response-meta))
+ (add-to-list 'elpher-gemini-redirect-chain redirect-address)
+ (elpher-get-gemini-response redirect-address renderer)))
+ (?4 ; Temporary failure
+ (error "Gemini server reports TEMPORARY FAILURE for this request: %s %s"
+ response-code response-meta))
+ (?5 ; Permanent failure
+ (error "Gemini server reports PERMANENT FAILURE for this request: %s %s"
+ response-code response-meta))
+ (?6 ; Client certificate required
+ (error "Gemini server requires client certificate (unsupported at this time)"))
+ (_other
+ (error "Gemini server response unknown: %s %s"
+ response-code response-meta))))))
+
+(defun elpher-get-gemini-node (renderer)
+ "Getter which retrieves and renders a Gemini node and renders it using RENDERER."