├── .gitignore ├── README.md ├── project.clj └── src └── plantuml_uma └── core.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .idea 13 | *.iml 14 | sample.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plantuml-uma 2 | 3 | Code for the [Documenting your architecture: Wireshark, PlantUML and a REPL to glue them all.](http://danlebrero.com/2017/04/06/documenting-your-architecture-wireshark-plantuml-and-a-repl/) blog entry. 4 | 5 | Generates a PlantUML sequence diagram given a Wireshark JSON capture. 6 | 7 | ## Usage 8 | 9 | Start REPL, load plantuml-uma.core and reload whole file after each change 10 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject plantuml-uma "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :url "http://example.com/FIXME" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [cheshire "5.6.3"] 8 | [clojure-humanize "0.2.2"] 9 | [net.sourceforge.plantuml/plantuml "2017.08"]]) 10 | 11 | -------------------------------------------------------------------------------- /src/plantuml_uma/core.clj: -------------------------------------------------------------------------------- 1 | (ns plantuml-uma.core 2 | (:require [cheshire.core :as json] 3 | [clojure.contrib.humanize :as human] 4 | clojure.string) 5 | (:import (java.io FileOutputStream) 6 | (net.sourceforge.plantuml SourceStringReader) 7 | (javax.swing ImageIcon JLabel JFrame) 8 | (java.awt Toolkit BorderLayout))) 9 | 10 | (defn http-data [frame] 11 | (let [http (-> frame 12 | (get "_source") 13 | (get "layers") 14 | (get "http")) 15 | request-method (first (keep #(get % "http.request.method") (vals http))) 16 | response-code (first (keep #(get % "http.response.code") (vals http)))] 17 | (-> http 18 | (select-keys ["http.user_agent" 19 | "http.request.full_uri" 20 | "http.response_in" 21 | "http.set_cookie" 22 | "http.host" 23 | "http.content_type"]) 24 | (assoc :response-code response-code) 25 | (assoc :method request-method)))) 26 | 27 | (defn other-data [frame] 28 | {:id (get-in frame ["_source" "layers" "frame" "frame.number"]) 29 | :time (long (* 1000 (Double/parseDouble (get-in frame ["_source" "layers" "frame" "frame.time_epoch"])))) 30 | :frame-size (Long/parseLong (get-in frame ["_source" "layers" "frame" "frame.len"])) 31 | :destination-ip (get-in frame ["_source" "layers" "ip" "ip.dst_host"])}) 32 | 33 | (defn parse-frame [frame] 34 | (merge (http-data frame) (other-data frame))) 35 | 36 | (defn uri-ends [what] 37 | (fn [x] 38 | (some-> x 39 | ^String (get "http.request.full_uri") 40 | (.matches (str ".*\\." what "$"))))) 41 | 42 | (def ignore-request? 43 | (some-fn 44 | (fn [x] 45 | (some-> x 46 | (get :destination-ip) 47 | (= "192.168.0.14"))) 48 | (uri-ends "js") 49 | (uri-ends "ico") 50 | (uri-ends "js.map") 51 | (uri-ends "png") 52 | (fn [x] 53 | (some-> x 54 | (get "http.request.full_uri") 55 | (.contains "login-status-iframe.html"))) 56 | (fn [x] 57 | (some-> x 58 | (get "http.request.full_uri") 59 | (.contains "sockjs-node"))))) 60 | 61 | (defn remove-boring-frames [all-frames] 62 | (let [to-ignore (filter ignore-request? (map parse-frame all-frames)) 63 | ids-to-remove (set (remove nil? (mapcat (juxt :id #(get % "http.response_in")) to-ignore)))] 64 | (remove 65 | (comp ids-to-remove :id) 66 | (map parse-frame all-frames)))) 67 | 68 | (defn from? [req] 69 | (if (.contains (get req "http.user_agent" "") "Mozilla") 70 | :browser 71 | :backend)) 72 | 73 | (defn to? [req] 74 | (cond 75 | (.contains (get req "http.request.full_uri" "") "http://t1.lumen.localhost:3030/api/") :backend 76 | (.contains (get req "http.request.full_uri" "") "http://t1.lumen.localhost:3030/env") :backend 77 | (.contains (get req "http.request.full_uri" "") "localhost:8080") :keycloak 78 | :default :nginx)) 79 | 80 | (defn join-req-and-resp [interesting-frames] 81 | (let [reqs (filter #(get % "http.response_in") interesting-frames) 82 | resp (fn [req] (first (filter 83 | (comp 84 | (partial = (get req "http.response_in")) 85 | :id) interesting-frames)))] 86 | (->> reqs 87 | (map (juxt identity resp)) 88 | (sort-by (comp #(Long/parseLong %) :id first)) 89 | (map (fn [[req res]] [(assoc req :from (from? req) :to (to? req)) 90 | (assoc res :from (from? req) :to (to? req))]))))) 91 | 92 | (defn traffic-size [request-response-pairs] 93 | (->> request-response-pairs 94 | (group-by (comp (juxt :from :to) first)) 95 | (map (fn [[from-to request-response-pairs]] 96 | [from-to 97 | {:from-to from-to 98 | :from->to (reduce + (map (comp :frame-size first) request-response-pairs)) 99 | :to->from (reduce + (map (comp :frame-size second) request-response-pairs))}])) 100 | (into {}))) 101 | 102 | (defn ->plantuml [request-response-pairs] 103 | (let [response? (fn [req-or-resp] (:response-code req-or-resp)) 104 | steps (sort-by (comp #(Long/parseLong %) :id) (apply concat request-response-pairs)) 105 | content-type (fn [type-str] (cond 106 | (nil? type-str) nil 107 | (clojure.string/includes? type-str "json") "json" 108 | (clojure.string/includes? type-str "html") "html" 109 | :default (throw (RuntimeException. type-str)))) 110 | path (fn [url] (.getPath (java.net.URL. url))) 111 | sequence-diagram-step (fn [{:keys [from to method frame-size] :as req-or-res}] 112 | (let [frame-size-str (str " (" (human/filesize frame-size) ")")] 113 | (if-not (response? req-or-res) 114 | [(str (name from) " -> " (name to) 115 | ": " (path (get req-or-res "http.request.full_uri")) 116 | (when-not (= "GET" method) (str " [" method "]")) 117 | frame-size-str)] 118 | [(let [resp-code (:response-code req-or-res) 119 | content-type (content-type (get req-or-res "http.content_type")) 120 | cookie (some-> (get req-or-res "http.set_cookie") (clojure.string/split #"=") first) 121 | request (ffirst (filter (fn [[_ res]] 122 | (= (:id res) (:id req-or-res))) 123 | request-response-pairs))] 124 | (try 125 | (str (name from) " <-- " (name to) 126 | ": " resp-code " " content-type 127 | (when cookie (str " [" cookie "]")) 128 | frame-size-str) 129 | (catch Exception e 130 | (println req-or-res) 131 | (throw e))))]))) 132 | sizes (traffic-size request-response-pairs) 133 | sizes-in-order (map (fn [from-to] (merge 134 | {:from-to from-to 135 | :from->to 0 136 | :to->from 0} 137 | (get sizes from-to))) 138 | [[:browser :nginx] 139 | [:browser :backend] 140 | [:browser :keycloak] 141 | [:backend :keycloak]]) 142 | size-note-fn (fn [{:keys [from-to from->to to->from]}] 143 | (str "note over " (name (first from-to)) 144 | ", " (name (second from-to)) 145 | ": ->" (human/filesize from->to) 146 | "/<-" (human/filesize to->from)))] 147 | (clojure.string/join 148 | "\n" 149 | (concat 150 | ["@startuml\n" 151 | "actor browser" 152 | "participant nginx" 153 | "participant backend" 154 | "participant keycloak"] 155 | (mapcat sequence-diagram-step steps) 156 | (map size-note-fn sizes-in-order) 157 | ["@enduml"])))) 158 | 159 | (defn create-image! [input-file output-file] 160 | (let [uml (-> input-file 161 | remove-boring-frames 162 | join-req-and-resp 163 | ->plantuml) 164 | out (FileOutputStream. (clojure.java.io/file output-file))] 165 | (-> (SourceStringReader. uml) 166 | (.generateImage out)) 167 | (.close out))) 168 | 169 | (def input-file "sample.json") 170 | (def output-img "sample.png") 171 | 172 | (defonce data (json/parse-string (slurp input-file))) 173 | 174 | (create-image! data output-img) 175 | 176 | (defonce img (ImageIcon. output-img)) 177 | 178 | (defonce jframe (doto 179 | (JFrame. "img") 180 | (.add (JLabel. img) BorderLayout/CENTER) 181 | (.pack) 182 | (.setVisible true))) 183 | 184 | (dotimes [_ 2] 185 | (let [img-icon (.getImage (Toolkit/getDefaultToolkit) output-img) 186 | max-height 1450 187 | img-icon (if (> (.getHeight img-icon) max-height) 188 | (.getScaledInstance img-icon (/ (* (.getWidth img-icon) max-height) 189 | (.getHeight img-icon)) max-height 1) 190 | img-icon)] 191 | (.setImage img img-icon)) 192 | (-> img .getImage .flush) 193 | (.repaint jframe)) 194 | 195 | --------------------------------------------------------------------------------