Commit Diff


commit - 958b98ffc8f0bd8a11fbf894dfced6fb3914c0a2
commit + 0c76ea8b82b9d5f72359186f2321d4257a917cb9
blob - 433d9d6f88de57d8f81b651ec51f38fe0d2de6db
blob + d6893054dd424cf0413db01feac1c84bf0d1594f
--- src/gemini/core.clj
+++ src/gemini/core.clj
@@ -2,54 +2,13 @@
   (:require
    [clojure.java.io :as io])
   (:import
+   (java.net URI)
    (com.omarpolo.gemini Request)))
 
-(defmacro ^:private request->map [& args]
-  `(try
-     (let [req# (Request. ~@args)]
-       {:request req#
-        :code    (.getCode req#)
-        :meta    (.getMeta req#)
-        :body    (.body req#)})
-     (catch Throwable e#
-       {:error e#})))
+(comment
+  (set! *warn-on-reflection* true)
+)
 
-(defn fetch
-  "Make a gemini request.  `uri` may be a URI, URL or string, and
-  represent the request to perform.  `host` and `port` are extracted
-  from the given `uri` in not given, and port defaults to 1965.  The
-  returned request needs to be closed when done."
-  ([uri]
-   (request->map uri))
-  ([host uri]
-   (fetch host 1965 uri))
-  ([host port uri]
-   (request->map host port uri)))
-
-(defn body-as-string!
-  "Read all the response into a strings and returns it.  The request
-  will be closed."
-  [{r :request}]
-  (let [sw (java.io.StringWriter.)]
-    (with-open [r r]
-      (io/copy (.body r) sw)
-      (.toString sw))))
-
-(defn close
-  "Close a request."
-  [{r :request}]
-  (.close r))
-
-(defmacro with-request
-  "Make a request, eval `body` when it succeed and automatically close
-  the request, or throw an exception if the request fails."
-  [[var req] & body]
-  `(let [~var ~req]
-     (when-let [e# (:error ~var)]
-       (throw e#))
-     (with-open [req# (:request ~var)]
-       ~@body)))
-
 
 ;; helpers
 
@@ -80,3 +39,103 @@
 (defn is-temporary-failure?    [{c :code}] (= 4 (/ c 10)))
 (defn is-permanent-failure?    [{c :code}] (= 5 (/ c 10)))
 (defn is-client-cert-required? [{c :code}] (= 6 (/ c 10)))
+
+
+
+(defn- parse-params [{:keys [proxy request follow-redirects?]}]
+  (when (and (:host proxy)
+             (not (:port proxy)))
+    (throw (ex-info "invalid proxy definition" {:got    proxy
+                                                :reason "missing proxy host"})))
+  {:host              (:host proxy)
+   :port              (:port proxy)
+   :request           (or request
+                          (throw (ex-info ":request is nil" {})))
+   :follow-redirects? (case follow-redirects?
+                        nil   0
+                        false 0
+                        true  5
+                        follow-redirects?)})
+
+(defn- resolve-uri [request meta]
+  (let [uri (URI. request)
+        rel (URI. meta)]
+    (str (.resolve uri rel))))
+
+(defn- fetch' [host port uri]
+  (try
+    (let [req (cond
+                host  (Request. ^String host ^int port ^String uri)
+                :else (Request. ^String uri))]
+      {:uri     uri
+       :request req
+       :code    (.getCode req)
+       :meta    (.getMeta req)
+       :body    (.body req)})
+    (catch Throwable e
+      {:error e})))
+
+(defn fetch
+  "Make a gemini request.  `params` is a map with the following
+  keys (only `:request` is mandatory):
+
+  - `:proxy`: a map of `:host` and `:port`, identifies the server to
+  send the requests to.  This allows to use a gemini server as a
+  proxy, it doesn't do any other kind of proxying (e.g. SOCK5.)
+
+  - `:request` the URI (as string) to require.
+
+  - `:follow-redirects?` if `false` or `nil` don't follow redirects,
+  if `true` follow up to 5 redirects, or the number of redirects to
+  follow.
+
+  Return a map with `:request`, `:code`, `:meta`, `:body` on success
+  or `:error` on failure.  The request needs to be closed when done
+  usign `close`."
+  [params]
+  (let [{:keys [host port request follow-redirects?] :as orig} (parse-params params)]
+    (loop [n           follow-redirects?
+           request     request
+           redirected? false]
+      (let [res       (fetch' host port request)
+            redirect? (and (not (:error res))
+                           (is-redirect? res))]
+        (cond
+          (:error res)            res
+          (= follow-redirects? 0) res
+          (and (= 0 n)
+               redirect?)         (do (.close ^Request (:request res))
+                                      (throw (ex-info "too many redirects"
+                                                      {:original  orig
+                                                       :redirects follow-redirects?
+                                                       :code      (:code res)
+                                                       :meta      (:meta res)})))
+          redirect?               (do (.close ^Request (:request res))
+                                      (recur (dec n)
+                                             (resolve-uri request (:meta res))
+                                             true))
+          :else                   res)))))
+
+(defn body-as-string!
+  "Read all the response into a strings and returns it.  The request
+  will be closed."
+  [{r :request}]
+  (let [sw (java.io.StringWriter.)]
+    (with-open [r ^Request r]
+      (io/copy (.body r) sw)
+      (.toString sw))))
+
+(defn close
+  "Close a request."
+  [{r :request}]
+  (.close ^Request r))
+
+(defmacro with-request
+  "Make a request, eval `body` when it succeed and automatically close
+  the request, or throw an exception if the request fails."
+  [[var req] & body]
+  `(let [~var ~req]
+     (when-let [e# (:error ~var)]
+       (throw e#))
+     (with-open [req# (:request ~var)]
+       ~@body)))