├── .gitignore ├── generator.go ├── go.mod ├── README.md ├── .drone.yml ├── go.sum ├── server.go └── mypatch.patch /.gitignore: -------------------------------------------------------------------------------- 1 | assets_vfsdata.go 2 | excalidraw/ 3 | static* -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "net/http" 8 | 9 | "github.com/shurcooL/vfsgen" 10 | ) 11 | 12 | func main() { 13 | var fs http.FileSystem = http.Dir("./excalidraw/build/") 14 | 15 | err := vfsgen.Generate(fs, vfsgen.Options{}) 16 | if err != nil { 17 | log.Fatalln(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module git.easyconnect.no/kjellkvinge/static 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/google/uuid v1.1.1 7 | github.com/gorilla/mux v1.7.4 8 | github.com/k0kubun/pp v3.0.1+incompatible 9 | github.com/mattn/go-colorable v0.1.6 // indirect 10 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect 11 | github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd 12 | ) 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # excalidrawserver 2 | 3 | Proof of concept. 4 | 5 | Bundle excalidraw in a simple binary 6 | 7 | All you have to do is to start the binary 8 | 9 | ``` 10 | ./excalidrawserver-linux 11 | # and go to localhost or an IP you host is listening to 12 | ``` 13 | 14 | ## Hacking on excalidraw 15 | ``` 16 | # create patch-set 17 | git format-patch master --stdout > ../mypatch.patch 18 | 19 | # apply patches 20 | git am ../mypatch.patch 21 | ``` 22 | see `.drone.ytml` 23 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: docker 3 | name: default 4 | 5 | steps: 6 | - name: excalidraw 7 | image: node:lts-stretch 8 | commands: 9 | - git clone https://github.com/excalidraw/excalidraw.git 10 | - cd excalidraw 11 | - git checkout a679ef7 12 | - git config --global user.email "drone@example.nope" 13 | - git config --global user.name "Drone" 14 | - git am ../mypatch.patch 15 | - npm install 16 | - REACT_APP_VERSION=$(git describe --tags --always) REACT_APP_HOST="/" npm run build:app:docker 17 | - cd .. 18 | 19 | - name: build 20 | image: golang:1.14 21 | commands: 22 | - go run generator.go 23 | - go build -o excalidrawserver-linux 24 | - GOOS=windows GOARCH=amd64 go build -o excalidrawserver.exe 25 | 26 | #for mac 27 | - GOOS=darwin GOARCH=amd64 go build -o excalidrawserver_darwin 28 | 29 | #for arm 30 | - GOOS=linux GOARCH=arm GOARM=5 go build -o excalidrawserver_arm 31 | 32 | - mkdir dist 33 | - mv excalidrawserver* dist/ 34 | 35 | 36 | - name: publish 37 | image: plugins/github-release 38 | settings: 39 | api_key: 40 | from_secret: github_token 41 | files: dist/* 42 | checksum: 43 | - md5 44 | when: 45 | event: tag 46 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 2 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 4 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 5 | github.com/k0kubun/pp v1.3.0 h1:r9td75hcmetrcVbmsZRjnxcIbI9mhm+/N6iWyG4TWe0= 6 | github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= 7 | github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= 8 | github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= 9 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 10 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 11 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 12 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= 13 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 14 | github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd h1:ug7PpSOB5RBPK1Kg6qskGBoP3Vnj/aNYFTznWvlkGo0= 15 | github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= 16 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= 18 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os" 9 | "sync" 10 | 11 | "github.com/gorilla/mux" 12 | ) 13 | 14 | //go:generate go run generator.go 15 | func main() { 16 | // fs := http.FileServer(http.Dir("./excalidraw/build/")) 17 | 18 | r := mux.NewRouter() 19 | fs := http.FileServer(assets) 20 | 21 | // API 22 | r.Handle("/api/v2/post/", new(countHandler)) 23 | r.Handle("/api/v2/{id}", new(countHandler)) 24 | 25 | // catchall - serve static files 26 | r.PathPrefix("/").Handler(fs) 27 | 28 | log.Println("Listening on :3001...") 29 | err := http.ListenAndServe(":3001", r) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | } 34 | 35 | type countHandler struct { 36 | mu sync.Mutex // guards n 37 | n int 38 | } 39 | 40 | func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 41 | h.mu.Lock() 42 | defer h.mu.Unlock() 43 | 44 | filenamebase := "/tmp/drawing" 45 | 46 | if r.Method == "POST" { 47 | h.n++ // increate id counter 48 | filename := fmt.Sprintf("%s_%d", filenamebase, h.n) 49 | for fileExists(filename) { 50 | h.n++ 51 | filename = fmt.Sprintf("%s_%d", filenamebase, h.n) 52 | } 53 | 54 | f, err := os.Create(filename) 55 | defer f.Close() 56 | b, err := ioutil.ReadAll(r.Body) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | f.Write(b) 61 | fmt.Fprintf(w, `{"data":"http://%s/api/v2/%d","id":"%d"}`, r.Host, h.n, h.n) 62 | return 63 | } 64 | 65 | id := mux.Vars(r)["id"] 66 | dat, err := ioutil.ReadFile(filenamebase + "_" + id) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | fmt.Fprint(w, string(dat)) 72 | 73 | } 74 | 75 | func fileExists(filename string) bool { 76 | if _, err := os.Stat(filename); err != nil { 77 | if os.IsNotExist(err) { 78 | return false 79 | } 80 | } 81 | return true 82 | } 83 | -------------------------------------------------------------------------------- /mypatch.patch: -------------------------------------------------------------------------------- 1 | From f5b8c40f0e6db9f38ddbe9a264cb6ddf3aeb0f45 Mon Sep 17 00:00:00 2001 2 | From: Kjell Kvinge 3 | Date: Mon, 29 Jun 2020 20:48:31 +0200 4 | Subject: [PATCH 1/3] show version 5 | 6 | --- 7 | src/components/LayerUI.tsx | 1 + 8 | src/css/styles.scss | 8 ++++++++ 9 | 2 files changed, 9 insertions(+) 10 | 11 | diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx 12 | index 9f52b91..a986680 100644 13 | --- a/src/components/LayerUI.tsx 14 | +++ b/src/components/LayerUI.tsx 15 | @@ -239,6 +239,7 @@ const LayerUI = ({ 16 | /> 17 | 18 | {renderEncryptedIcon()} 19 | +
v:{process.env.REACT_APP_VERSION}
20 | 21 | 22 | 23 | diff --git a/src/css/styles.scss b/src/css/styles.scss 24 | index 7d76f6d..fa87d56 100644 25 | --- a/src/css/styles.scss 26 | +++ b/src/css/styles.scss 27 | @@ -188,6 +188,14 @@ button, 28 | } 29 | } 30 | 31 | +#version { 32 | + color: darkgray; 33 | + padding-top: 2em; 34 | + padding-left: 1em; 35 | + font-size: small; 36 | + font-family: monospace; 37 | +} 38 | + 39 | .App-bottom-bar { 40 | position: absolute; 41 | top: 0; 42 | -- 43 | 2.17.1 44 | 45 | 46 | From ec4a608dcc3b322e1e697f2ccda0f63891af4678 Mon Sep 17 00:00:00 2001 47 | From: Kjell Kvinge 48 | Date: Tue, 30 Jun 2020 09:40:54 +0200 49 | Subject: [PATCH 2/3] Configurable api endpoints 50 | 51 | --- 52 | src/data/index.ts | 18 ++++++++++++------ 53 | 1 file changed, 12 insertions(+), 6 deletions(-) 54 | 55 | diff --git a/src/data/index.ts b/src/data/index.ts 56 | index f9c3bec..793d28e 100644 57 | --- a/src/data/index.ts 58 | +++ b/src/data/index.ts 59 | @@ -24,12 +24,18 @@ export { loadFromBlob } from "./blob"; 60 | export { saveAsJSON, loadFromJSON } from "./json"; 61 | export { saveToLocalStorage } from "./localStorage"; 62 | 63 | -const BACKEND_GET = "https://json.excalidraw.com/api/v1/"; 64 | - 65 | -const BACKEND_V2_POST = "https://json.excalidraw.com/api/v2/post/"; 66 | -const BACKEND_V2_GET = "https://json.excalidraw.com/api/v2/"; 67 | - 68 | -export const SOCKET_SERVER = "https://excalidraw-socket.herokuapp.com"; 69 | +// get backend host from env. default excalidraw 70 | +const HOST = process.env.REACT_APP_HOST 71 | + ? process.env.REACT_APP_HOST 72 | + : "https://json.excalidraw.com/"; 73 | +const BACKEND_GET = `${HOST}api/v1/`; 74 | + 75 | +const BACKEND_V2_POST = `${HOST}api/v2/post/`; 76 | +const BACKEND_V2_GET = `${HOST}api/v2/`; 77 | + 78 | +export const SOCKET_SERVER = process.env.REACT_APP_SOCKET_SERVER 79 | + ? process.env.REACT_APP_SOCKET_SERVER 80 | + : "https://excalidraw-socket.herokuapp.com"; 81 | 82 | export type EncryptedData = { 83 | data: ArrayBuffer; 84 | -- 85 | 2.17.1 86 | 87 | 88 | From 512c071f5f323a3ab52511f93cda1207d74b61a5 Mon Sep 17 00:00:00 2001 89 | From: Kjell Kvinge 90 | Date: Tue, 30 Jun 2020 09:42:30 +0200 91 | Subject: [PATCH 3/3] Disable crypto 92 | 93 | --- 94 | src/data/index.ts | 16 ++++++++-------- 95 | 1 file changed, 8 insertions(+), 8 deletions(-) 96 | 97 | diff --git a/src/data/index.ts b/src/data/index.ts 98 | index 793d28e..4515393 100644 99 | --- a/src/data/index.ts 100 | +++ b/src/data/index.ts 101 | @@ -86,7 +86,7 @@ const generateRandomID = async () => { 102 | return Array.from(arr, byteToHex).join(""); 103 | }; 104 | 105 | -const generateEncryptionKey = async () => { 106 | +/*const generateEncryptionKey = async () => { 107 | const key = await window.crypto.subtle.generateKey( 108 | { 109 | name: "AES-GCM", 110 | @@ -96,7 +96,7 @@ const generateEncryptionKey = async () => { 111 | ["encrypt", "decrypt"], 112 | ); 113 | return (await window.crypto.subtle.exportKey("jwk", key)).k; 114 | -}; 115 | +};*/ 116 | 117 | const createIV = () => { 118 | const arr = new Uint8Array(12); 119 | @@ -113,7 +113,7 @@ export const getCollaborationLinkData = (link: string) => { 120 | 121 | export const generateCollaborationLink = async () => { 122 | const id = await generateRandomID(); 123 | - const key = await generateEncryptionKey(); 124 | + const key = "none"; //await generateEncryptionKey(); 125 | return `${window.location.origin}${window.location.pathname}#room=${id},${key}`; 126 | }; 127 | 128 | @@ -190,7 +190,7 @@ export const exportToBackend = async ( 129 | const json = serializeAsJSON(elements, appState); 130 | const encoded = new TextEncoder().encode(json); 131 | 132 | - const key = await window.crypto.subtle.generateKey( 133 | + /* const key = await window.crypto.subtle.generateKey( 134 | { 135 | name: "AES-GCM", 136 | length: 128, 137 | @@ -214,18 +214,18 @@ export const exportToBackend = async ( 138 | // We use jwk encoding to be able to extract just the base64 encoded key. 139 | // We will hardcode the rest of the attributes when importing back the key. 140 | const exportedKey = await window.crypto.subtle.exportKey("jwk", key); 141 | - 142 | +*/ 143 | try { 144 | const response = await fetch(BACKEND_V2_POST, { 145 | method: "POST", 146 | - body: encrypted, 147 | + body: encoded, 148 | }); 149 | const json = await response.json(); 150 | if (json.id) { 151 | const url = new URL(window.location.href); 152 | // We need to store the key (and less importantly the id) as hash instead 153 | // of queryParam in order to never send it to the server 154 | - url.hash = `json=${json.id},${exportedKey.k!}`; 155 | + url.hash = `json=${json.id},none`; 156 | const urlString = url.toString(); 157 | 158 | window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString); 159 | @@ -254,7 +254,7 @@ export const importFromBackend = async ( 160 | return restore(elements, appState, { scrollToContent: true }); 161 | } 162 | let data; 163 | - if (privateKey) { 164 | + if (privateKey && privateKey !== "none") { 165 | const buffer = await response.arrayBuffer(); 166 | const key = await getImportedKey(privateKey, "decrypt"); 167 | const iv = new Uint8Array(12); 168 | -- 169 | 2.17.1 170 | 171 | --------------------------------------------------------------------------------