Blob


1 (ns gemini.core
2 (:require
3 [clojure.java.io :as io])
4 (:import
5 (java.net URI)
6 (com.omarpolo.gemini Request)))
8 (comment
9 (set! *warn-on-reflection* true)
10 )
13 ;; helpers
15 (def code-description
16 "Human description for every response code."
17 {10 "input"
18 11 "sensitive input"
19 20 "success"
20 30 "temporary redirect"
21 31 "permanent redirect"
22 40 "temporary failure"
23 41 "server unavailable"
24 42 "CGI error"
25 43 "proxy error"
26 44 "slow down"
27 50 "permanent failure"
28 51 "not found"
29 52 "gone"
30 53 "proxy request refused"
31 59 "bad request"
32 60 "client certificate required"
33 61 "certificate not authorized"
34 62 "certificate not valid"})
36 (defn is-input? [{c :code}] (= 1 (quot c 10)))
37 (defn is-success? [{c :code}] (= 2 (quot c 10)))
38 (defn is-redirect? [{c :code}] (= 3 (quot c 10)))
39 (defn is-temporary-failure? [{c :code}] (= 4 (quot c 10)))
40 (defn is-permanent-failure? [{c :code}] (= 5 (quot c 10)))
41 (defn is-client-cert-required? [{c :code}] (= 6 (quot c 10)))
45 (defn- parse-params [{:keys [proxy request follow-redirects?]}]
46 (when (and (:host proxy)
47 (not (:port proxy)))
48 (throw (ex-info "invalid proxy definition" {:got proxy
49 :reason "missing proxy host"})))
50 {:host (:host proxy)
51 :port (:port proxy)
52 :request (or request
53 (throw (ex-info ":request is nil" {})))
54 :follow-redirects? (case follow-redirects?
55 nil 0
56 false 0
57 true 5
58 follow-redirects?)})
60 (defn- resolve-uri [request meta]
61 (let [uri (URI. request)
62 rel (URI. meta)]
63 (str (.resolve uri rel))))
65 (defn- fetch' [host port uri]
66 (try
67 (let [req (cond
68 host (Request. ^String host ^int port ^String uri)
69 :else (Request. ^String uri))]
70 {:uri uri
71 :request req
72 :code (.getCode req)
73 :meta (.getMeta req)
74 :body (.body req)})
75 (catch Throwable e
76 {:error e})))
78 (defn fetch
79 "Make a gemini request. `params` is a map with the following
80 keys (only `:request` is mandatory):
82 - `:proxy`: a map of `:host` and `:port`, identifies the server to
83 send the requests to. This allows to use a gemini server as a
84 proxy, it doesn't do any other kind of proxying (e.g. SOCK5.)
86 - `:request` the URI (as string) to require.
88 - `:follow-redirects?` if `false` or `nil` don't follow redirects,
89 if `true` follow up to 5 redirects, or the number of redirects to
90 follow.
92 Return a map with `:request`, `:code`, `:meta`, `:body` on success
93 or `:error` on failure. The request needs to be closed when done
94 usign `close`."
95 [params]
96 (let [{:keys [host port request follow-redirects?] :as orig} (parse-params params)]
97 (loop [n follow-redirects?
98 request request
99 redirected? false]
100 (let [res (fetch' host port request)
101 redirect? (and (not (:error res))
102 (is-redirect? res))]
103 (cond
104 (:error res) res
105 (= follow-redirects? 0) res
106 (and (= 0 n)
107 redirect?) (do (.close ^Request (:request res))
108 (throw (ex-info "too many redirects"
109 {:original orig
110 :redirects follow-redirects?
111 :code (:code res)
112 :meta (:meta res)})))
113 redirect? (do (.close ^Request (:request res))
114 (recur (dec n)
115 (resolve-uri request (:meta res))
116 true))
117 :else res)))))
119 (defn body-as-string!
120 "Read all the response into a strings and returns it. The request
121 will be closed."
122 [{r :request}]
123 (let [sw (java.io.StringWriter.)]
124 (with-open [r ^Request r]
125 (io/copy (.body r) sw)
126 (.toString sw))))
128 (defn close
129 "Close a request."
130 [{r :request}]
131 (.close ^Request r))
133 (defmacro with-request
134 "Make a request, eval `body` when it succeed and automatically close
135 the request, or throw an exception if the request fails."
136 [[var req] & body]
137 `(let [~var ~req]
138 (when-let [e# (:error ~var)]
139 (throw e#))
140 (with-open [req# (:request ~var)]
141 ~@body)))