Blame


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