0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-02 12:28:54 -05:00

🎉 Add resource usage limits.

This commit is contained in:
Andrey Antukh 2021-01-25 09:29:41 +01:00 committed by Alonso Torres
parent 3c7fbb8fd6
commit 592153f968
8 changed files with 160 additions and 79 deletions

View file

@ -36,7 +36,8 @@
:storage-s3-region :eu-central-1 :storage-s3-region :eu-central-1
:storage-s3-bucket "penpot-devenv-assets-pre" :storage-s3-bucket "penpot-devenv-assets-pre"
:image-process-max-threads 2 :rlimits-password 10
:rlimits-image 2
:smtp-enabled false :smtp-enabled false
:smtp-default-reply-to "no-reply@example.com" :smtp-default-reply-to "no-reply@example.com"
@ -109,7 +110,8 @@
(s/def ::public-uri ::us/string) (s/def ::public-uri ::us/string)
(s/def ::backend-uri ::us/string) (s/def ::backend-uri ::us/string)
(s/def ::image-process-max-threads ::us/integer) (s/def ::rlimits-password ::us/integer)
(s/def ::rlimits-image ::us/integer)
(s/def ::google-client-id ::us/string) (s/def ::google-client-id ::us/string)
(s/def ::google-client-secret ::us/string) (s/def ::google-client-secret ::us/string)
@ -161,7 +163,6 @@
::http-server-debug ::http-server-debug
::http-server-port ::http-server-port
::http-server-cors ::http-server-cors
::image-process-max-threads
::ldap-auth-avatar-attribute ::ldap-auth-avatar-attribute
::ldap-auth-base-dn ::ldap-auth-base-dn
::ldap-auth-email-attribute ::ldap-auth-email-attribute
@ -179,6 +180,8 @@
::registration-domain-whitelist ::registration-domain-whitelist
::registration-enabled ::registration-enabled
::secret-key ::secret-key
::rlimits-password
::rlimits-image
::smtp-default-from ::smtp-default-from
::smtp-default-reply-to ::smtp-default-reply-to
::smtp-enabled ::smtp-enabled

View file

@ -97,6 +97,19 @@
{:metrics (ig/ref :app.metrics/metrics) {:metrics (ig/ref :app.metrics/metrics)
:svgc (ig/ref :app.svgparse/svgc)} :svgc (ig/ref :app.svgparse/svgc)}
;; RLimit definition for password hashing
:app.rlimits/password
(:rlimits-password cfg/config)
;; RLimit definition for image processing
:app.rlimits/image
(:rlimits-image cfg/config)
;; A collection of rlimits as hash-map.
:app.rlimits/all
{:password (ig/ref :app.rlimits/password)
:image (ig/ref :app.rlimits/image)}
:app.rpc/rpc :app.rpc/rpc
{:pool (ig/ref :app.db/pool) {:pool (ig/ref :app.db/pool)
:session (ig/ref :app.http.session/session) :session (ig/ref :app.http.session/session)
@ -104,6 +117,7 @@
:metrics (ig/ref :app.metrics/metrics) :metrics (ig/ref :app.metrics/metrics)
:storage (ig/ref :app.storage/storage) :storage (ig/ref :app.storage/storage)
:redis (ig/ref :app.redis/redis) :redis (ig/ref :app.redis/redis)
:rlimits (ig/ref :app.rlimits/all)
:svgc (ig/ref :app.svgparse/svgc)} :svgc (ig/ref :app.svgparse/svgc)}
:app.notifications/handler :app.notifications/handler
@ -283,7 +297,11 @@
:name "telemetry"}}))) :name "telemetry"}})))
(defmethod ig/init-key :default [_ data] data) (defmethod ig/init-key :default [_ data] data)
(defmethod ig/prep-key :default [_ data] (d/without-nils data)) (defmethod ig/prep-key :default
[_ data]
(if (map? data)
(d/without-nils data)
data))
(def system nil) (def system nil)

View file

@ -10,27 +10,24 @@
(ns app.media (ns app.media
"Media postprocessing." "Media postprocessing."
(:require (:require
[app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.media :as cm] [app.common.media :as cm]
[app.common.data :as d]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cfg] [app.config :as cfg]
[app.util.http :as http] [app.rlimits :as rlm]
[app.svgparse :as svg] [app.svgparse :as svg]
[clojure.core.async :as a] [app.util.http :as http]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.core :as fs]) [datoteka.core :as fs])
(:import (:import
java.io.ByteArrayInputStream java.io.ByteArrayInputStream
java.util.concurrent.Semaphore
org.im4java.core.ConvertCmd org.im4java.core.ConvertCmd
org.im4java.core.IMOperation org.im4java.core.IMOperation
org.im4java.core.Info)) org.im4java.core.Info))
(def semaphore (Semaphore. (:image-process-max-threads cfg/config 1)))
;; --- Generic specs ;; --- Generic specs
(s/def :internal.http.upload/filename ::us/string) (s/def :internal.http.upload/filename ::us/string)
@ -174,20 +171,14 @@
:hint (str "No impl found for process cmd:" cmd))) :hint (str "No impl found for process cmd:" cmd)))
(defn run (defn run
[params] [{:keys [rlimits]} params]
(try (us/assert map? rlimits)
(.acquire semaphore) (let [rlimit (get rlimits :image)]
(let [res (a/<!! (a/thread (when-not rlimit
(try (ex/raise :type :internal
(process params) :code :rlimit-not-configured
(catch Throwable e :hint ":image rlimit not configured"))
e))))] (rlm/execute rlimit (process params))))
(if (instance? Throwable res)
(throw res)
res))
(finally
(.release semaphore))))
;; --- Utility functions ;; --- Utility functions
@ -197,23 +188,3 @@
(ex/raise :type :validation (ex/raise :type :validation
:code :media-type-not-allowed :code :media-type-not-allowed
:hint "Seems like you are uploading an invalid media object"))) :hint "Seems like you are uploading an invalid media object")))
(defn download-media-object
[url]
(let [result (http/get! url {:as :byte-array})
data (:body result)
content-type (get (:headers result) "content-type")
format (cm/mtype->format content-type)]
(if (nil? format)
(ex/raise :type :validation
:code :media-type-not-allowed
:hint "Seems like the url points to an invalid media object.")
(let [tempfile (fs/create-tempfile)
filename (fs/name tempfile)]
(with-open [ostream (io/output-stream tempfile)]
(.write ostream data))
{:filename filename
:size (count data)
:tempfile tempfile
:content-type content-type}))))

View file

@ -0,0 +1,48 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020-2021 UXBOX Labs SL
(ns app.rlimits
"Resource usage limits (in other words: semaphores)."
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[clojure.spec.alpha :as s]
[integrant.core :as ig])
(:import
java.util.concurrent.Semaphore))
(s/def ::rlimit #(instance? Semaphore %))
(s/def ::rlimits (s/map-of ::us/keyword ::rlimit))
(derive ::password ::instance)
(derive ::image ::instance)
(defmethod ig/pre-init-spec ::instance [_]
(s/spec int?))
(defmethod ig/init-key ::instance
[_ permits]
(Semaphore. (int permits)))
(defn acquire!
[sem]
(.acquire ^Semaphore sem))
(defn release!
[sem]
(.release ^Semaphore sem))
(defmacro execute
[rlinst & body]
`(try
(acquire! ~rlinst)
~@body
(finally
(release! ~rlinst))))

View file

@ -9,11 +9,12 @@
(ns app.rpc (ns app.rpc
(:require (:require
[app.common.exceptions :as ex]
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.db :as db] [app.db :as db]
[app.metrics :as mtx] [app.metrics :as mtx]
[app.rlimits :as rlm]
[app.util.services :as sv] [app.util.services :as sv]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
@ -51,18 +52,38 @@
(cond->> {:status 200 :body result} (cond->> {:status 200 :body result}
(fn? (:transform-response mdata)) ((:transform-response mdata) request)))) (fn? (:transform-response mdata)) ((:transform-response mdata) request))))
(defn- wrap-impl (defn- wrap-with-metrics
[f mdata cfg prefix] [cfg f mdata prefix]
(let [mreg (get-in cfg [:metrics :registry]) (let [mreg (get-in cfg [:metrics :registry])
mobj (mtx/create mobj (mtx/create
{:name (-> (str "rpc_" (name prefix) "_" (::sv/name mdata) "_response_millis") {:name (-> (str "rpc_" (name prefix) "_" (::sv/name mdata) "_response_millis")
(str/replace "-" "_")) (str/replace "-" "_"))
:registry mreg :registry mreg
:type :summary :type :summary
:help (str/format "Service '%s' response time in milliseconds." (::sv/name mdata))}) :help (str/fmt "Service '%s' response time in milliseconds." (::sv/name mdata))})]
f (mtx/wrap-summary f mobj) (mtx/wrap-summary f mobj)))
spec (or (::sv/spec mdata) (s/spec any?))]
;; Wrap the rpc handler with a semaphore if it is specified in the
;; metadata asocciated with the handler.
(defn- wrap-with-rlimits
[cfg f mdata]
(if-let [key (:rlimit mdata)]
(let [rlinst (get-in cfg [:rlimits key])]
(when-not rlinst
(ex/raise :type :internal
:code :rlimit-not-configured
:hint (str/fmt "%s rlimit not configured" key)))
(log/debugf "Adding rlimit to '%s' rpc handler." (::sv/name mdata))
(fn [cfg params]
(rlm/execute rlinst (f cfg params))))
f))
(defn- wrap-impl
[cfg f mdata prefix]
(let [f (wrap-with-rlimits cfg f mdata)
f (wrap-with-metrics cfg f mdata prefix)
spec (or (::sv/spec mdata)
(s/spec any?))]
(log/debugf "Registering '%s' command to rpc service." (::sv/name mdata)) (log/debugf "Registering '%s' command to rpc service." (::sv/name mdata))
(fn [params] (fn [params]
(when (and (:auth mdata true) (not (uuid? (:profile-id params)))) (when (and (:auth mdata true) (not (uuid? (:profile-id params))))
@ -75,7 +96,7 @@
[cfg prefix vfn] [cfg prefix vfn]
(let [mdata (meta vfn)] (let [mdata (meta vfn)]
[(keyword (::sv/name mdata)) [(keyword (::sv/name mdata))
(wrap-impl (deref vfn) mdata cfg prefix)])) (wrap-impl cfg (deref vfn) mdata prefix)]))
(defn- resolve-query-methods (defn- resolve-query-methods
[cfg] [cfg]
@ -108,7 +129,7 @@
(s/def ::tokens fn?) (s/def ::tokens fn?)
(defmethod ig/pre-init-spec ::rpc [_] (defmethod ig/pre-init-spec ::rpc [_]
(s/keys :req-un [::db/pool ::storage ::session ::tokens ::mtx/metrics])) (s/keys :req-un [::db/pool ::storage ::session ::tokens ::mtx/metrics ::rlm/rlimits]))
(defmethod ig/init-key ::rpc (defmethod ig/init-key ::rpc
[_ cfg] [_ cfg]

View file

@ -7,8 +7,6 @@
;; ;;
;; Copyright (c) 2020-2021 UXBOX Labs SL ;; Copyright (c) 2020-2021 UXBOX Labs SL
;; TODO: move to file namespace, there are no media concept separated from file.
(ns app.rpc.mutations.media (ns app.rpc.mutations.media
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
@ -18,9 +16,11 @@
[app.db :as db] [app.db :as db]
[app.media :as media] [app.media :as media]
[app.rpc.queries.teams :as teams] [app.rpc.queries.teams :as teams]
[app.util.storage :as ust]
[app.util.services :as sv]
[app.storage :as sto] [app.storage :as sto]
[app.util.http :as http]
[app.util.services :as sv]
[app.util.storage :as ust]
[clojure.java.io :as io]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[datoteka.core :as fs])) [datoteka.core :as fs]))
@ -67,6 +67,27 @@
[info] [info]
(= (:mtype info) "image/svg+xml")) (= (:mtype info) "image/svg+xml"))
;; TODO: we need to properly delete temporary files.
(defn- download-media
[url]
(let [result (http/get! url {:as :byte-array})
data (:body result)
content-type (get (:headers result) "content-type")
format (cm/mtype->format content-type)]
(if (nil? format)
(ex/raise :type :validation
:code :media-type-not-allowed
:hint "Seems like the url points to an invalid media object.")
(let [tempfile (fs/create-tempfile)
filename (fs/name tempfile)]
(with-open [ostream (io/output-stream tempfile)]
(.write ostream data))
{:filename filename
:size (count data)
:tempfile tempfile
:content-type content-type}))))
(defn create-file-media-object (defn create-file-media-object
[{:keys [conn storage svgc] :as cfg} {:keys [id file-id is-local name content] :as params}] [{:keys [conn storage svgc] :as cfg} {:keys [id file-id is-local name content] :as params}]
(media/validate-media-type (:content-type content)) (media/validate-media-type (:content-type content))
@ -74,11 +95,11 @@
source-path (fs/path (:tempfile content)) source-path (fs/path (:tempfile content))
source-mtype (:content-type content) source-mtype (:content-type content)
source-info (media/run {:cmd :info :input {:path source-path :mtype source-mtype}}) source-info (media/run cfg {:cmd :info :input {:path source-path :mtype source-mtype}})
thumb (when (and (not (svg-image? source-info)) thumb (when (and (not (svg-image? source-info))
(big-enough-for-thumbnail? source-info)) (big-enough-for-thumbnail? source-info))
(media/run (assoc thumbnail-options (media/run cfg (assoc thumbnail-options
:cmd :generic-thumbnail :cmd :generic-thumbnail
:input {:mtype (:mtype source-info) :input {:mtype (:mtype source-info)
:path source-path}))) :path source-path})))
@ -116,11 +137,10 @@
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [file (select-file-for-update conn file-id)] (let [file (select-file-for-update conn file-id)]
(teams/check-edition-permissions! conn profile-id (:team-id file)) (teams/check-edition-permissions! conn profile-id (:team-id file))
(let [content (media/download-media-object url) (let [content (download-media url)
params' (merge params {:content content params' (merge params {:content content
:name (or name (:filename content))})] :name (or name (:filename content))})]
;; TODO: schedule to delete the tempfile created by media/download-media-object
(-> (assoc cfg :conn conn) (-> (assoc cfg :conn conn)
(create-file-media-object params')))))) (create-file-media-object params'))))))

View file

@ -54,7 +54,7 @@
(s/keys :req-un [::email ::password ::fullname] (s/keys :req-un [::email ::password ::fullname]
:opt-un [::token])) :opt-un [::token]))
(sv/defmethod ::register-profile {:auth false} (sv/defmethod ::register-profile {:auth false :rlimit :password}
[{:keys [pool tokens session storage] :as cfg} {:keys [token] :as params}] [{:keys [pool tokens session storage] :as cfg} {:keys [token] :as params}]
(when-not (:registration-enabled cfg/config) (when-not (:registration-enabled cfg/config)
(ex/raise :type :restriction (ex/raise :type :restriction
@ -200,7 +200,7 @@
(s/keys :req-un [::email ::password] (s/keys :req-un [::email ::password]
:opt-un [::scope])) :opt-un [::scope]))
(sv/defmethod ::login {:auth false} (sv/defmethod ::login {:auth false :rlimit :password}
[{:keys [pool] :as cfg} {:keys [email password scope] :as params}] [{:keys [pool] :as cfg} {:keys [email password scope] :as params}]
(letfn [(check-password [profile password] (letfn [(check-password [profile password]
(when (= (:password profile) "!") (when (= (:password profile) "!")
@ -292,7 +292,7 @@
(s/def ::update-profile-password (s/def ::update-profile-password
(s/keys :req-un [::profile-id ::password ::old-password])) (s/keys :req-un [::profile-id ::password ::old-password]))
(sv/defmethod ::update-profile-password (sv/defmethod ::update-profile-password {:rlimit :password}
[{:keys [pool] :as cfg} {:keys [password profile-id] :as params}] [{:keys [pool] :as cfg} {:keys [password profile-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(validate-password! conn params) (validate-password! conn params)
@ -315,7 +315,7 @@
(media/validate-media-type (:content-type file)) (media/validate-media-type (:content-type file))
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [profile (db/get-by-id conn :profile profile-id) (let [profile (db/get-by-id conn :profile profile-id)
_ (media/run {:cmd :info :input {:path (:tempfile file) _ (media/run cfg {:cmd :info :input {:path (:tempfile file)
:mtype (:content-type file)}}) :mtype (:content-type file)}})
photo (teams/upload-photo cfg params) photo (teams/upload-photo cfg params)
storage (assoc storage :conn conn)] storage (assoc storage :conn conn)]
@ -398,7 +398,7 @@
(s/def ::recover-profile (s/def ::recover-profile
(s/keys :req-un [::token ::password])) (s/keys :req-un [::token ::password]))
(sv/defmethod ::recover-profile {:auth false} (sv/defmethod ::recover-profile {:auth false :rlimit :password}
[{:keys [pool tokens] :as cfg} {:keys [token password]}] [{:keys [pool tokens] :as cfg} {:keys [token password]}]
(letfn [(validate-token [token] (letfn [(validate-token [token]
(let [tdata (tokens :verify {:token token :iss :password-recovery})] (let [tdata (tokens :verify {:token token :iss :password-recovery})]

View file

@ -249,7 +249,7 @@
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id) (teams/check-edition-permissions! conn profile-id team-id)
(let [team (teams/retrieve-team conn profile-id team-id) (let [team (teams/retrieve-team conn profile-id team-id)
_ (media/run {:cmd :info :input {:path (:tempfile file) _ (media/run cfg {:cmd :info :input {:path (:tempfile file)
:mtype (:content-type file)}}) :mtype (:content-type file)}})
photo (upload-photo cfg params)] photo (upload-photo cfg params)]
@ -265,11 +265,11 @@
(assoc team :photo-id (:id photo))))) (assoc team :photo-id (:id photo)))))
(defn upload-photo (defn upload-photo
[{:keys [storage]} {:keys [file]}] [{:keys [storage] :as cfg} {:keys [file]}]
(let [prefix (-> (bn/random-bytes 8) (let [prefix (-> (bn/random-bytes 8)
(bc/bytes->b64u) (bc/bytes->b64u)
(bc/bytes->str)) (bc/bytes->str))
thumb (media/run thumb (media/run cfg
{:cmd :profile-thumbnail {:cmd :profile-thumbnail
:format :jpeg :format :jpeg
:quality 85 :quality 85