Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

using the idea of the web_func to inject params to function #16

Merged
merged 8 commits into from
Dec 15, 2011
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,35 @@ and if we invoke function like (+ 1 3) with token1,

then we will get a correct answer 4.

## inject invoke parameter

Sometime, some parameters that method need (like client-ip) that the
web client can not get. we can dynamic inject this kind of parameters
(from ring request) into method.

the export-commands support options like
{:params-inject [ [:remote-addr] [:header "host]]}

and clj-rpc will (get-in request [:remote-addr]) and (get-in request
[:header "host"]) as the first and second parameter of the method that
client invokes.

Example:
```clojure
(defn fn-do-someting [client-ip username]
... )

(export-commands 'mynamespace ["fn-do-something"]
{:params-inject [ [:remote-addr] ]})
```

Then the client will invoke function like (fn-do-something usernmae)
and the clj-rpc will dynamic invoke like (fn-do-something "127.0.0.1"
username)

Notice: Now, clj-rpc only supprots to inject params into the front of the
parameters that client supplied .

## user data

Sometime , we need to access data (like session) related the connection.
Expand Down
2 changes: 1 addition & 1 deletion project.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(defproject clj-rpc "0.2.1"
(defproject clj-rpc "0.2.2-SNAPSHOT"
:description "simple rpc using clojure"
:dependencies [[org.clojure/clojure "1.3.0"]
[org.clojure/tools.logging "0.2.3"]
Expand Down
122 changes: 89 additions & 33 deletions src/clj_rpc/context.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
(:require [ring.middleware.cookies :as cookies]
[clj-rpc.user-data :as data]))

(defn wrap-client-ip
"let :remote-addr represents the real client ip even though
the client connects through a proxy server"
[handler]
(fn [request]
(let [ip-from-proxy (get-in request [:headers "X-Forwarded-For"])
request (if ip-from-proxy
(assoc request :remote-addr ip-from-proxy)
request)]
(handler request))))

(defn wrap-context
"accoding to the token
(come from cookie in the http request or the token parameters)
Expand All @@ -22,52 +33,97 @@

(defn add-context
"add context to specific command
options is a map include several keys
options is a collection include several keys
like [ [:requre-context true] [:params-checks ...] [... ...] ]

the meaning of option:
:require-context (true or false default false)
whether this command must have context
:params-checks
check parameters of the method be invoked statisfy the sepcific requirements
example :
{0 [:username]} -->
the first parameter must equals (get-in context [:username])"
the first parameter must equals (get-in context [:username])
:params-inject
inject parameters from request into the function
example :
[ [:remote-addr] [:server-name] ] -->
client-ip and server-name as the first and second parameter of the function
if client invoke (fun param1 param2) then the acutally invoke will be like
(fun client-ip server-name param1 param2)"
[command options]
(merge command options))
(assoc command :options options))

(defn add-context-to-command-map
"add context options to all the command in the command-map
options : same as add-context"
[command-map options]
(into {} (map (fn [[k v]] [k (merge v options)]) command-map)))
(into {} (map (fn [[k v]] [k (add-context v options)]) command-map)))

(defn error-method-request?
"return true if method-reqeust error
else false"
[method-request]
(boolean (:error method-request)))

(defmulti render-method-request
"adjust method request by option
return new method-request"
(fn [option-key option-value request method-request]
option-key))

(defn- check-authorization
"return nil if the command don't need authrization or
the context include the authorization information
else return {:code :unauthorized}"
[context command]
(when (and (get command :require-context)
(not (seq context)))
{:code :unauthorized}))
;;render method-request :require-context option
;;return method-request with error or original method-request
(defmethod render-method-request :require-context
[_ option-value request method-request]
(if (and option-value
(not (seq (get request :context) )))
(assoc method-request :error {:code :unauthorized})
method-request))

(defn- check-params
"return nil
if the params of the command satisfy the requirement of the command
provided specific context
else return {:code invalid-params :message error-message}"
[context command command-params]
(letfn [(fn-check [[k v]]
(if (not= (nth command-params k) (get-in context v))
;;render method-request :params-check option
;;return method-request with params error or original method-request
(defmethod render-method-request :params-check
[_ option-value request method-request]
(let [{context :context} request
{params :params} method-request
fn-check
(fn [[k v]]
(if (not= (nth params k) (get-in context v))
{:code :invalid-params
:message (str "the " k "th param must be " (get-in context v ))} ))]
(->> (:params-checks command)
(map fn-check )
(filter identity)
first)))
:message (str "the " k "th param must be "
(get-in context v))}))]
(if-let [error (->> option-value
(map fn-check )
(filter identity)
first)]
(assoc method-request :error error)
method-request)))

;;render method-request :params-inject option
;;inject the params from request needed by the command
;;into the actual params
;;return the new method-request
(defmethod render-method-request :params-inject
[_ option-value request method-request]
(if option-value
(update-in method-request [:params]
#(concat (map (partial get-in request) option-value) %))
method-request))

;;default throw RuntimeException
(defmethod render-method-request :default
[option-key option-value request method-request]
(throw (RuntimeException. (str "Unknown command option: " option-key
"method-request: " method-request ) )) )

(defn check-context
"check whether the command and params statisfy the requirement
if success return nil
else return {:code error-code :message error-message}
error-code refer to clj-rpc.rpc.clj"
[command context params]
(or (check-authorization context command)
(check-params context command params)))
(defn adjust-method-request
"return new method-request (possible with error message)"
[cmd request method-request]
(loop [options (:options cmd)
m-r method-request]
(let [[option-key option-value] (first options)]
(if (or (nil? option-key) (error-method-request? m-r))
m-r
(recur (rest options)
(render-method-request option-key option-value request m-r))))))
10 changes: 6 additions & 4 deletions src/clj_rpc/rpc.clj
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@
"execute a function f with params
return rpc response
id : used for generate the response"
[f params id]
[f {:keys [params id error]}]
(try
(if f
(mk-response (apply f params) id)
(mk-error :method-not-found id))
(if error
(mk-error (:code error) id (:message error))
(if f
(mk-response (apply f params) id)
(mk-error :method-not-found id)))
(catch ArityException e (mk-error :invalid-params id (.getMessage e)) )
(catch Exception e (mk-error :internal-error id (.getMessage e)))))
42 changes: 22 additions & 20 deletions src/clj_rpc/server.clj
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@
(defn export-commands
"export all functions (fn-names is null or empty)
or specify functions in the namespace ns
invoker of this method must notice the order of the options
options: options is a collection include several keys
like [ [:requre-context true] [:params-checks ...] [... ...] ]

options: is a map include several keys
the meaning of option:
:require-context (true or false default false)
whether this command must have context
:params-checks
Expand All @@ -52,7 +55,7 @@
(export-commands \"clojure.core\" nil)
(export-commands 'clojure.core ['+])
(export-commands 'clojure.core [\"+\"]
{:require-context true :params-check {0 [:id]}})"
[ [:require-context true] [:params-check {0 [:id]}] ])"
[ns fn-names & [options]]
(let [ns (symbol ns)]
(require ns)
Expand All @@ -66,15 +69,13 @@
"get the function from the command-map according the method-name and
execute this function with args
return the execute result"
[command-map context {:keys [method params id]}]
(logging/debug "execute-command == method-name: "
method " params: " params " id: " id)
(let [cmd (command-map method)
[command-map request method-request]
(logging/debug "execute-command == " method-request)
(let [cmd (command-map (:method method-request))
f (and cmd (command/.func cmd))
check-result (context/check-context cmd context params)]
(if check-result
(rpc/mk-error (:code check-result) id (:message check-result))
(rpc/execute-method f params id))))
new-method-request (context/adjust-method-request
cmd request method-request)]
(rpc/execute-method f new-method-request)))

(defn help-commands
"return the command list"
Expand All @@ -90,9 +91,9 @@
(defn rpc-invoke
"invoke rpc method
rpc-request can a map (one invoke) or a collection of map (multi invokes)"
[command-map context rpc-request]
[command-map request rpc-request]
(letfn [(fn-execute [r]
(execute-command command-map context
(execute-command command-map request
(change-str->keyword r)))]
(if (map? rpc-request)
(fn-execute rpc-request)
Expand All @@ -102,11 +103,11 @@
(ANY "/:s-method/help" [s-method]
(when-let [[f-encode] (protocol/serialization s-method)]
(f-encode (help-commands @commands))))
(POST "/:s-method/invoke" [s-method :as {context :context body :body}]
(let [rpc-request (slurp body)]
(POST "/:s-method/invoke" [s-method :as reqeust]
(let [rpc-request (slurp (:body reqeust))]
(logging/debug "invoking (" s-method ") request: " rpc-request)
(let [[f-encode f-decode] (protocol/serialization s-method)]
(f-encode (rpc-invoke @commands context (f-decode rpc-request))))))
(f-encode (rpc-invoke @commands reqeust (f-decode rpc-request))))))
(route/not-found "invalid url"))

;;define a jetty-instance used to start or stop
Expand All @@ -120,11 +121,12 @@
(reset! jetty-instance nil))

(defn build-hander [options]
(handler/site
(context/wrap-context main-routes
(:fn-get-context options)
(:cookie-attrs options)
(:token-cookie-key options))))
(-> main-routes
(context/wrap-context (:fn-get-context options)
(:cookie-attrs options)
(:token-cookie-key options))
(context/wrap-client-ip)
handler/site))

(defn start
"start jetty server
Expand Down
9 changes: 6 additions & 3 deletions src/clj_rpc/user_data.clj
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,19 @@
"存储用户数据,如果原来没有token,那么创建一个新的token
返回 data"
[data]
(do (if (not @*atom-token*) (reset! *atom-token* (uuid)) )
(swap! atom-user-datas assoc @*atom-token* data )
data))
(do
;;treat @atom-token* "" as nil
(if (not (seq @*atom-token*)) (reset! *atom-token* (uuid)) )
(swap! atom-user-datas assoc @*atom-token* data )
data))

(defn delete-user-data!
"删除用户相关数据,
纯粹副作用,返回nil"
[]
(do
(swap! atom-user-datas dissoc @*atom-token*)
(reset! *atom-token* "")
nil))

;;TODO 增加用户数据定期清理
54 changes: 33 additions & 21 deletions test/clj_rpc/test/context.clj
Original file line number Diff line number Diff line change
@@ -1,33 +1,45 @@
(ns clj-rpc.test.context
(:use [clj-rpc.context :reload true]
[clojure.test])
[clojure.test]
[midje.sweet])
(:require [clj-rpc.command :as command]))


(deftest check-add-context-to-command-map
(let [command-map {"str" (command/mk-command "str" #'str)
"concat" (command/mk-command "concat" #'concat)}
options {:require-context true :params-check {0 1}}
options [ [:require-context true] [:params-check {0 1}]]
new-map (add-context-to-command-map command-map options)
vs (vals new-map)]
(is (= (get-in (first vs) [:params-check]) {0 1}))
(is (= (get-in (second vs) [:params-check]) {0 1}))))
(is (= (get (first vs) :options) options))
(is (= (get (second vs) :options) options))))

(deftest test-check-context
(facts
"check whether or not the command and params statifies context requirement"
(let [cmd (-> (command/mk-command "concat" #'concat)
(add-context {:require-context true
:params-checks {0 [:p1] 1 [:p2]}}))
cmd-not-require (command/mk-command "str" #'str)]
(is (nil? (check-context cmd {:p1 [1 2] :p2 [3 4]}
[ [1 2] [3 4] [5 6]]))
"stastify authorized and params requirement")
(is (= :unauthorized (:code (check-context cmd nil [ [1 2] [3 4] [5 6]])))
"do not statify authorization requirement")
(is (= :unauthorized (:code (check-context cmd {} [ [1 2] [3 4] [5 6]])))
"do not statify authorization requirement")
(is (= :invalid-params (:code (check-context cmd {:p1 [1 2]}
[ [1 2] [3 4] [5 6]])))
"do not statify params check requirement")
(is (nil? (check-context cmd-not-require nil ["str1" "str2"]))
"do not require context check")))
(add-context [ [:require-context true]
[:params-check {0 [:p1] 1 [:p2]}]]))
cmd-not-require (command/mk-command "str" #'str)
method-request {:method "method" :params [ [1 2] [3 4] [5 6]]}]
;;stastify authorized and params requirement
(adjust-method-request cmd {:context {:p1 [1 2] :p2 [3 4]}}
method-request) => method-request

;;do not statify authorization requirement
(adjust-method-request cmd nil method-request) => (contains {:error {:code :unauthorized}})
(adjust-method-request cmd {} method-request) => (contains {:error {:code :unauthorized}})

;;do not statify params check requirement
(get-in (adjust-method-request cmd {:context {:p1 [1 2] }} method-request)
[:error :code]) => :invalid-params

;;do not require context check
(adjust-method-request cmd-not-require nil {:method "method" :params ["str1" "str2"]})
=> {:method "method" :params ["str1" "str2"]}))

;.;. The work itself praises the master. -- CPE Bach
(facts "test inject parasm"
(let [cmd (-> (command/mk-command "str" #'str)
(add-context {:params-inject [ [:remote-addr] [:inject]]})) ]
(adjust-method-request cmd {:remote-addr "192.168.1.1" :inject "inject-param2"}
{:method "method" :params ["p1" "p2"]})
=> {:method "method" :params ["192.168.1.1" "inject-param2" "p1" "p2"]}))
Loading