Commit 78fed72d authored by Sarman's avatar Sarman
Browse files

initial commit

parent 805d3fc9
\ No newline at end of file
# clojure_api_and_sockets_example
Example http-kit API server + socket server, internal scheduler, mount parts, honeysql
\ No newline at end of file
Example http-kit API server with swagger + socket server, internal scheduler, mount parts, honeysql
Need to create .lein-env file or set env variables, crypto keys, java-home if needed, db schema
:crypt-lock "some-secret-key3"
:socket-admin-pass "44acbaa115989f03632752fa625a2541"
:java-home "/opt/jdk"
:cl-database-url "jdbc:mysql://host:3306/dbname?zeroDateTimeBehavior=CONVERT_TO_NULL&user=username&password=yourpassword&useSSL=false"
\ No newline at end of file
# Thor is WebSocket benchmarking/load generator.
thor --amount 5000 ws://localhost:4444/socket?uid=youruid
# native-image -jar ./target/cl.jar -H:Class=cl.main
# static
native-image -jar ./target/cl.jar --static --libc=glibc -H:Class=cl.main
# mostly static only with glibc external
# native-image -jar ./target/cl.jar -H:+StaticExecutableWithDynamicLibC -H:Class=cl.main
lein with-profile uberjar uberjar
(defproject cl "0.1.0-SNAPSHOT"
:description "cl server"
:url ""
:dependencies [[org.clojure/clojure "1.10.3"]
[environ "1.2.0"]
[ring/ring-json "0.5.1"]
[http-kit "2.5.3"]
[ring/ring-devel "1.9.3"]
[compojure "1.6.2"]
[ring/ring-defaults "0.3.3"]
[ring-cors "0.1.13"]
[mount "0.1.16"]
[mysql/mysql-connector-java "8.0.25"]
;; _- db conversion
[camel-snake-kebab "0.4.2"]
;; for compile uberjar
[javax.servlet/servlet-api "2.5"]
[com.taoensso/timbre "5.1.2"]
[com.fzakaria/slf4j-timbre "0.3.21"]
;; scheduler
[im.chit/cronj "1.4.4"]
;; url utils
[com.cemerick/url "0.1.1"]
[com.github.seancorfield/next.jdbc "1.2.674"]
[com.github.seancorfield/honeysql "2.0.0-rc2"]
[metosin/compojure-api "2.0.0-alpha31"]
[clj-time "0.15.2"]
[lock-key "1.5.0"]
;; test
;; nice lib about all
[tupelo "21.06.15"]]
;; How to connect to remote REPL + ssh tunnel
;; ssh -L :6666:localhost:6666 -N -v
;; (-v is verbose to detect problems)
;; then connect from IDE to local port 6666 as usual
:repl-options {:port 6666
:timeout 120000}
:main cl.main
:uberjar-name "cl.jar"
;; See for the
;; various options available for the lein-ring plugin
:ring {:handler cl.handler/-main
;;:async? true
:auto-reload? true
:auto-refresh? true
:nrepl {:start? true
:port 4242}}
:profiles {:uberjar {:aot :all}
[[javax.servlet/servlet-api "2.5"]]}}
;; or use global LEIN_JVM_OPTS=-Xms4G -Xmx4G
;; or for prod JVM_OPTS=-Xms4G
;; + remote monitoring
;; Windows users: putty.exe -ssh user@serv -L 1616:p1-0:1616
;; Linux and Mac Users: ssh user@serv -L 1616:p1-0:1616
;; Start jconsole on your computer
;; jconsole localhost:1616
:jvm-opts ["-Xmx4g"
; for remote connect thru ssh tunnel
lein run
\ No newline at end of file
(echo "(start)"; cat <&0) | lein do clean, repl
\ No newline at end of file
java -cp ./cl.jar -Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}" cl.main
# then you can use REPL
# rlwrap nc localhost 5555
# or use unravel repl
# unravel localhost 5555
#(clojure.core/require '[ :refer [refresh]])
#user=> (refresh)
#(clojure.core/require '[clojure.repl :as r :refer [doc]])
\ No newline at end of file
(ns cl.core
[org.httpkit.server :as httpkit :refer [run-server]]
[ring.middleware.cors :as ring-cors :refer [wrap-cors]]
[ring.middleware.cookies :refer [wrap-cookies]] ;; for cookie store
;[clj-redis-session.core :only [redis-store]] ;; for redis store, if cluster
[ring.middleware.params :refer [wrap-params]]
[ring.middleware.reload :as ring-reload]
[ring.util.response :as ring-response]
[ring.util.request :as ring-request]
[mount.core :as mount :refer [defstate]]
[ :refer [refresh refresh-all]]
[ring.middleware.json :refer [wrap-json-response]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
[compojure.api.sweet :refer [defroutes]]
[compojure.route :as route]
[ring.middleware.session :as session :refer [wrap-session]]
[ring.middleware.session.memory :refer [memory-store]]
[environ.core :refer [env]]
[clojure.string :as str]
[lock-key.core :refer [decrypt decrypt-as-str decrypt-from-base64
encrypt encrypt-as-base64]]
[cl.log :as log]
[ :as ws-routes]
[cl.routes.http-routes :as http-routes]
[cl.libs.main :refer [string-to-bytes]]
[cl.models.user :as user]
[tupelo.core :refer [discarding-system-err]]
[cl.libs.api :as api]
[ring.util.http-response :as http :refer :all]))
;; clj-redis-session use Carmine as its Redis client
;; if needed
;(def redis-conn
; {:pool {}
; :spec {:uri "redis://clsession:1foo12bare2@localhost:9836/"}})
(defroutes app-routes
(route/not-found (ring-response/response {:error "Route not found"})))
(defn wrap-history
"Middleware that stores last 20 visited urls in session"
(fn [req]
(let [resp (handler req)
session (or (:session resp) (:session req))
updated-session (assoc session :history
(vec (take-last 20 (concat (:history session) [(ring-request/request-url req)]))))]
(assoc resp :session updated-session))))
(defn wrap-auth
"Middleware user auth + cache it in session for fun"
(fn [req]
(let [resp (handler req)
session (or (:session resp) (:session req))
headers (get req :headers)
apikey (get headers "apikey")
user-session (get session :user)
apikey-info (if-not (empty? apikey)
(let [decoded (decrypt-from-base64
apikey (env :crypt-lock))
splitted (when-not (empty? decoded)
(str/split decoded #"\|"))]
(catch Exception e (do
(println "Decode apikey error: " (.getMessage e))
;(throw (ex-info (str (api/answer http/forbidden "invalid-apikey" "Invalid apikey in headers" false)) {}))
updated-session (if-not (false? apikey-info)
(if-not (nil? apikey-info)
(if (= (str (get user-session :id)) (first apikey-info))
(let [count (:user-cached-count session 0)
user-cached-count {:user-cached-count (inc count)}]
(println "CACHED FROM SESSION: " (str (inc count)))
(let [u (user/get-user-info (first apikey-info))]
(println "NEW GET FROM DB")
{:user u}))
(println "ASSIGN REQ NIL")
{:user nil
:user-cached-count 0}
(if (false? apikey-info)
(api/simple-answer 401 "invalid-apikey" "Invalid apikey in headers" "")
(assoc resp :session (merge session updated-session))))))
;(do (mount/stop #'cl.core/my-server)
; (mount/start #'cl.core/my-server))
(defn in-dev? [] true)
(defn args->server-config [args]
{:port (Integer/parseInt
(or (first args)
(System/getenv "PORT")
:join? false
:server-header nil
:queue-size 204800})
(defn start-server [server-config]
(let [handler (if (in-dev?)
(ring-reload/wrap-reload #'app-routes)
(when-let [server (httpkit/run-server
(-> handler
(ring.middleware.json/wrap-json-body {:keywords? true})
:access-control-allow-origin #".+")
(wrap-session {
;:store use other store than default memory
:cookie-name "session"
:root "/"
:cookie-attrs {
:max-age 3600
:http-only true
(log/info "Server has started! =)")
;All state changing without server restart is experimental now
(defstate ; ^{:on-reload :noop}
:start (start-server (args->server-config
:stop (do
(my-server :timeout 10000)
(log/info "Server has stopped...")))
(ns cl.db
(:require [environ.core :refer [env]]
[next.jdbc :as jdbc]
[next.jdbc.result-set :as rs]
[honey.sql :as sql]
[mount.core :refer [defstate]]))
(def db (env :cl-database-url))
;; to start all the states call mount/start in handler.clj
(defstate ^:dynamic *db*
:start (jdbc/get-datasource db)
:stop (print "stop"))
(defn queryAll
(with-open [connection (jdbc/get-connection db)]
(let [sql (sql/format sql-statement {:dialect :mysql})]
(jdbc/execute! connection sql
{:builder-fn rs/as-unqualified-lower-maps}))))
(defn queryOne
(with-open [connection (jdbc/get-connection db)]
(let [sql (sql/format sql-statement {:dialect :mysql})]
(jdbc/execute-one! connection sql
{:builder-fn rs/as-unqualified-lower-maps}))))
(ns cl.libs.api
[ring.util.http-response :refer :all]
[clojure.main :refer [demunge]]
(defn internal-fn-name
(as-> (str f) $
(demunge $)
(or (re-find #"(.+)--\d+@" $)
(re-find #"(.+)@" $))
(last $))
(defn answer [http-code-name-fn
"HTTP api standard answer"
(let [status-fn (http-code-name-fn)]
(http-code-name-fn {
:status (:status status-fn)
:status-name (clojure.string/replace
(internal-fn-name http-code-name-fn)
#"ring.util.http-response/" "")
:internal-code internal-code
:desc desc
:body body ;; result data
(defn simple-answer [status
"HTTP api simpler standard answer"
:status status
:headers {}
:internal-code internal-code
:desc desc
:body body ;; result data
\ No newline at end of file
(ns cl.libs.main)
(defn string-to-bytes [s] (.getBytes s))
(ns cl.log
[taoensso.timbre :as timbre]
[taoensso.timbre.appenders.3rd-party.rotor :as rotor]))
; set up timbre aliases
;rotor log
(timbre/merge-config! {:appenders {:spit (rotor/rotor-appender {:path "./cl.log" :max-size (* 1024 1024), :backlog 5})}})
{:pattern "yyyy-MM-dd HH:mm:ss ZZ"
:locale (java.util.Locale. "ru_RU")
:timezone (java.util.TimeZone/getTimeZone "Europe/Moscow")}})
; Set the lowest-level
(timbre/set-level! :info)
(defn info [message]
(timbre/info message))
(ns cl.main
[cl.core :as core]
[cl.scheduler :as scheduler]
[mount.core :as mount :refer [start start-with-args]])
;; for prod only, dev use his own (mount/start) in run-dev script
(defn parse-args [args]
;; parse args here to return what's needed
(defn -main [& args]
(-> args
(ns cl.methods.get
[schema.core :as s]
[cl.log :as log]
[cl.models.user :as user]
[cl.libs.api :as api]
[ring.util.http-response :as http :refer :all]))
(defn get-test [request session]
;(println session)
;(let [count (:cached-count session 0)
; session (assoc session :cached-count (inc count))]
; (->
; (if (user/is-authenticated? session)
; (api/answer http/ok "test-ok-auth" "The test is ok, auth" "Auth")
; (api/answer http/ok "test-ok-no-auth" "The test is ok, no auth" "NO Auth"))
; (assoc :session session)))
(-> (if (user/is-authenticated? session)
(api/answer http/ok "test-ok-auth" "The test is ok, auth" "Auth")
(api/answer http/ok "test-ok-no-auth" "The test is ok, no auth" "NO Auth"))
;(assoc :session session)
(defn get-session-count-test [session]
(let [count (:count session 0)
session (assoc session :count (inc count))]
(api/answer http/ok "count" "Counter" {:foo "Bar", :count (:count session)})
(assoc :session session))))
(defn get-plus [x y]
(api/answer http/ok "plus" "Plus calculation" (+ x y)))
\ No newline at end of file
[schema.core :as s]
[cl.log :as log]
[cl.models.user :as user]
[cl.libs.api :as api]
[ring.util.http-response :as http :refer :all]))
(defn post-bot [challenge]
(api/answer http/ok "good" "Challenge answer" (str "Answer: " challenge)))
(ns cl.models.user
(:require [cl.db :as db]
[schema.core :as s]
[honey.sql :as sql]
[ring.swagger.json-schema :refer [field]]))
(s/defschema User
{:id (field s/Int {:example 1})})
(defn- clear-user-private-data [user]
(-> user
(dissoc :password)))
(defn is-authenticated? [request]
;(println request)
(get-in request [:user :id] false))
(defn get-user-info [id]
(-> (db/queryOne
{:select [:*]
:from [:users]
:where [:= :id id]})
;(get-user-info 1)
(ns cl.routes.http-routes
[compojure.api.sweet :refer :all]
[ring.swagger.swagger2 :refer :all]
[cl.methods.get :as api-get]
[ :as api-post]
[ring.util.http-response :as http :refer :all]
[schema.core :as s]
[ring.swagger.json-schema :refer [field]]))
(defn api-result [result]
(assoc {:http-code (field s/Int {:example 200})
:http-code-name (field s/Str {:example "ok"})
:internal-code (field s/Str {:example "some-unique-code"})
:desc (field s/Str {:example "Description"})}
:result result)
(defroutes http-routes
; Simple api test for correct auth
(GET "/api/test" request (api-get/get-test request (:session request)))
;(GET "/api/test" {session :session} (api-get/get-test session))
; Simple api session counter test
(GET "/api/session-count-test" {session :session} (api-get/get-session-count-test session))
; swagger powered real api
{:ui "/api-editor"
:spec "/swagger.json"
:data {:info {:title "Hello"
:description "Api self documentation"}}
:tags [{:name "api", :description "api"}]}}
(context "/api" []
:tags ["api"]
;; GET
(GET "/plus" []
:return (api-result s/Int)
:query-params [x :- s/Int, y :- s/Int]
:summary "Adds two numbers together"
(api-get/get-plus x y))
(POST "/bot" [challenge]
:return (api-result String)
:query-params [challenge]
(api-post/post-bot challenge))))
(:use [compojure.core :only (defroutes GET)]
[ring.util.codec :only (form-decode)]
[cheshire.core :refer :all]
[clojure.string :as str]
[cl.log :as log]
[lock-key.core :refer [decrypt decrypt-as-str decrypt-from-base64
encrypt encrypt-as-base64]]
[environ.core :refer [env]]))
(defn get-uid-from-encrypted-string
"Get uid from encrypted string, tune it for your needs"
uid-str (env :crypt-lock)))
(def connections
"All socket connections"
(atom {}))
(defn reset-all-connections
"Reset connections"
(log/info "Reset connections")
(reset! connections {}))
(defmulti ws-action (fn [params conn]
(keyword (:action params))))
(defmethod ws-action :test [params conn]
(log/info "test action")
(send! conn (generate-string {:test true}) false))
(defmethod ws-action :user-connected
[params conn]
"Just connected")
(defmethod ws-action :reset-server
[params _]
"Restart socket server and reset connections"
(log/info "reset-connections-by-command")
(defn not-authorized [conn]
(log/info "Auth error")
(send! conn
{:connected false
:message "not authorized"}) false))
(defn ws-handler
req conn
(if (:query-string req)
(let [query-map (keywordize-keys (form-decode (:query-string req)))
server-key (:server-key query-map)]
(if (= server-key (env :socket-admin-pass))
(swap! connections assoc conn