fuck cors article
This commit is contained in:
parent
530645cae8
commit
92d7c0e6bd
|
@ -0,0 +1,197 @@
|
|||
#+title: A quick CORS proxy in a few lines of Clojure
|
||||
#+description:
|
||||
#+keywords: clojure CORS web
|
||||
#+author: Yann Esposito
|
||||
#+email: yann@esposito.host
|
||||
#+date: [2024-03-08 Fri]
|
||||
#+lang: en
|
||||
#+options: auto-id:t
|
||||
#+startup: showeverything
|
||||
|
||||
I use a HTML file from one of my local directory as homepage.
|
||||
This HTML file contains in a single one page view, all links I generally want to
|
||||
jump to.
|
||||
|
||||
One of the section of links in this homepage contain a few website I host.
|
||||
And I wanted to query these websites to make a healthcheck from my file.
|
||||
It turns out that you cannot easily make an HTTP call to any external website
|
||||
from a `file://` in your Browser as your are almost immediately blocked by CORS.
|
||||
|
||||
I don't want to explain how CORS are working, most people don't get it correctly
|
||||
anyway.
|
||||
The important point is that it is a security measure that is *very easy* to circumvent.
|
||||
|
||||
Here is how to do it:
|
||||
|
||||
1. Create a webservice that will play a role of "proxy". For example, the
|
||||
webservice will read the ~?url=DESTINATION_URL~.
|
||||
2. When receiving a request, the server will make a similar HTTP call to
|
||||
~DESTINATION_URL~ and keep track of the ~Origin~ HTTP header of the request.
|
||||
3. Take the response from ~DESTINATION_URL~ and add a few headers, in particular
|
||||
add the ~Access-Control-Allow-Origin~ header that will contain the value in the
|
||||
~Origin~ header of the request.
|
||||
|
||||
That's it.
|
||||
|
||||
So I wrote that in few minutes and use it now. So I can make these call and
|
||||
detect when I see my homepage if one of my hosted website is not reachable.
|
||||
|
||||
* The Clojure code
|
||||
:PROPERTIES:
|
||||
:CUSTOM_ID: the-clojure-code
|
||||
:END:
|
||||
|
||||
Here is the code in a few lines of Clojure:
|
||||
|
||||
#+begin_src clojure
|
||||
(ns fuck-cors-app.core
|
||||
(:require
|
||||
[clj-http.client :as client]
|
||||
[ring.adapter.jetty :as jetty]
|
||||
[ring.middleware.params :refer [wrap-params]]
|
||||
[fuck-cors.core :refer [wrap-open-cors]])
|
||||
(:gen-class))
|
||||
|
||||
(defn handler
|
||||
[request]
|
||||
(if-let [url (get-in request [:query-params "url"])]
|
||||
(client/request {:request-method (:request-method request)
|
||||
:url url})
|
||||
{:status 200
|
||||
:headers {"Content-Type" "text/plain; charset=utf-8"}
|
||||
:body "Let's bypass CORS ok?"}))
|
||||
|
||||
(defn -main
|
||||
[& _args]
|
||||
(jetty/run-jetty
|
||||
(-> handler
|
||||
(wrap-params)
|
||||
(wrap-open-cors))
|
||||
{:port 1977
|
||||
:host "127.0.0.1"}))
|
||||
#+end_src
|
||||
|
||||
And that's it, this is a whole web application that will proxy any call to a
|
||||
website that do not allow you to call from some origin (like my `file://`) and
|
||||
will make it work anyway.
|
||||
|
||||
If you feel that using too many libraries is cheating, here is the actual almost full
|
||||
content of the lib taking care of handling CORS:
|
||||
|
||||
#+begin_src clojure
|
||||
(defn- host-from-req
|
||||
[request]
|
||||
(str (-> request :scheme name)
|
||||
"://"
|
||||
(get-in request [:headers "host"])))
|
||||
|
||||
(defn- get-header
|
||||
[request header-name]
|
||||
(let [rawref (get-in request [:headers header-name])]
|
||||
(if rawref
|
||||
(clojure.string/replace rawref #"(http://[^/]*).*$" "$1")
|
||||
nil)))
|
||||
|
||||
(defn wrap-open-cors
|
||||
"Open your Origin Policy to Everybody, no limit"
|
||||
[handler]
|
||||
(fn [request]
|
||||
(let [origin (get-header request "origin")
|
||||
referer (get-header request "referer")
|
||||
host (host-from-req request)
|
||||
origins (if origin
|
||||
origin
|
||||
(if referer
|
||||
referer
|
||||
host))
|
||||
headers {"Access-Control-Allow-Origin" origins
|
||||
"Access-Control-Allow-Headers" "Origin, X-Requested-With, Content-Type, Accept, Cache-Control, Accept-Language, Accept-Encoding, Authorization"
|
||||
"Access-Control-Allow-Methods" "HEAD, GET, POST, PUT, DELETE, OPTIONS, TRACE"
|
||||
"Access-Control-Allow-Credentials" "true"
|
||||
"Access-Control-Expose-Headers" "content-length"
|
||||
"Vary" "Accept-Encoding, Origin, Accept-Language"}]
|
||||
(-> (handler request)
|
||||
(update-in [:headers] #(into % headers))))))
|
||||
|
||||
(defn wrap-preflight
|
||||
"Add a preflight answer. Will break any OPTIONS handler, beware.
|
||||
To put AFTER wrap-open-cors"
|
||||
[handler]
|
||||
(fn [request]
|
||||
(if (= (request :request-method) :options)
|
||||
(into request {:status 200 :body "preflight complete"})
|
||||
(handler request))))
|
||||
#+end_src
|
||||
|
||||
I wrote it a long time ago, and I think I just found a potential bug related to the headers.
|
||||
I should probably retrieve all headers returned by the response, and add these
|
||||
header name to the ~Access-Control-Allow-Headers~. But this list of allowed
|
||||
headers will work most of the time.
|
||||
|
||||
|
||||
* Bonus frontend code to check the availability of a website
|
||||
:PROPERTIES:
|
||||
:CUSTOM_ID: bonus-frontend-code-to-check-the-availability-of-a-website
|
||||
:END:
|
||||
|
||||
As a bonus here is the code I use in my homepage to see if the website I am
|
||||
looking for are reachable or not.
|
||||
|
||||
Imagine you have an HTML block like this:
|
||||
|
||||
#+begin_src html
|
||||
<div class="healthcheck">
|
||||
...
|
||||
<a href="SOME_URL">website 1</a>
|
||||
...
|
||||
<a href="SOME_URL">website 2</a>
|
||||
...
|
||||
</div>
|
||||
#+end_src
|
||||
|
||||
I have a CSS rule that change the background of these link to green or red if I
|
||||
add the class ~ok~ or ~error~ to the ~<a>~.
|
||||
And here is the javascript code:
|
||||
|
||||
#+begin_src javascript
|
||||
// You can replace corsproxy.org (which is a public one)
|
||||
// by the one you are hoting.
|
||||
const corsproxyurl='https://corsproxy.org/?';
|
||||
async function healthchecklink(a) {
|
||||
var linkurl=a.href;
|
||||
if (linkurl != undefined) {
|
||||
var url = corsproxyurl + encodeURIComponent(linkurl);
|
||||
try {
|
||||
var response = await fetch(url, {method: 'GET',
|
||||
redirect: 'manual',
|
||||
signal: AbortSignal.timeout(3000)
|
||||
});
|
||||
if (response.ok || response.redirected || response.status === 0 ) {
|
||||
a.classList.add("ok");
|
||||
} else {
|
||||
a.classList.add("error");
|
||||
}
|
||||
} catch (err) {
|
||||
a.classList.add("error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkhealth() {
|
||||
var links = document.querySelectorAll('.healthcheck a');
|
||||
for (l in links) {
|
||||
healthchecklink(links[l]);
|
||||
}
|
||||
}
|
||||
|
||||
checkhealth();
|
||||
#+end_src
|
||||
|
||||
Notice the ~response.status == 0~, this is due to one of my website returning a
|
||||
303 redirection but some complication make it returns a status 0.
|
||||
If there were an error it would return an error status.
|
||||
|
||||
That's it.
|
||||
Another tool I created myself to prevent me using a service checking for the
|
||||
status of my website and sending me notifications about it.
|
||||
None of my website is crucial enough not be ok to wait a few hours to be re-enabled.
|
Loading…
Reference in a new issue