├── .gitignore ├── LICENSE ├── README.md ├── generate.sh ├── go.mod ├── go.sum └── pkg ├── dash ├── auth.go ├── config.go ├── dash.go ├── dashapp.go ├── dashappclient.go ├── dashcloud.go ├── dashfs.go ├── dashreflect.go ├── dbclient.go ├── expowait.go ├── limits.go ├── request.go └── runtime.go ├── dasherr └── dasherr.go ├── dashproto ├── dashproto.pb.go ├── dashproto.proto └── doc.go ├── dashutil ├── path.go ├── util.go └── validators.go └── keygen └── keygen.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .#* 3 | *.key 4 | *.crt 5 | *.jpg 6 | backup/ 7 | .idea/ 8 | *.log 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dashborg Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dashborg Go SDK 2 | 3 | Dashborg is a SDK that plugs directly into any backend service or script. With a couple lines of code you can register any running function with the Dashborg service and then build secure, modern, interactive tools on top of those functions using a simple, JavaScript free, HTML template language. 4 | 5 | Dashborg saves you time by handling all of the tedious details of frontend app creation and deployment (hosting, end-to-end security, authentication, transport, UI libraries, JavaScript frameworks, CSS frameworks, etc.). 6 | 7 | Dashborg works great for debugging, introspecting the running state of servers, viewing/updating configuration values, bite-sized internal tools, status pages, and reports. 8 | 9 | Dashborg is easy to get started with. You can have your first app deployed in 5-minutes (no account/registration required). Free tier covers most simple use cases. 10 | 11 | * GoDoc (pkg.go.dev) - https://pkg.go.dev/github.com/sawka/dashborg-go-sdk/pkg/dash 12 | * Dashborg Documentation Site - https://docs.dashborg.net/ 13 | * Tutorial - https://docs.dashborg.net/tutorials/t1/ 14 | 15 | Questions? [Join the Dashborg Slack Channel](https://join.slack.com/t/dashborgworkspace/shared_invite/zt-uphltkhj-r6C62szzoYz7_IIsoJ8WPg) 16 | 17 | ## Dashborg Hello World 18 | 19 | ```Golang 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | 25 | "github.com/sawka/dashborg-go-sdk/pkg/dash" 26 | ) 27 | 28 | var counter int 29 | 30 | func TestFn() (interface{}, error) { 31 | counter++ 32 | fmt.Printf("Calling TestFn counter:%d!\n", counter) 33 | return map[string]interface{}{"success": true, "message": "TestFn Output!", "counter": counter}, nil 34 | } 35 | 36 | func main() { 37 | // Configure and connect the Dashborg Client 38 | config := &dash.Config{AutoKeygen: true, AnonAcc: true} 39 | client, err := dash.ConnectClient(config) 40 | if err != nil { 41 | fmt.Printf("Error connecting DashborgCloudClient: %v\n", err) 42 | return 43 | } 44 | 45 | // Create a new app called "hello-world" 46 | app := client.AppClient().NewApp("hello-world") 47 | // Set the UI for our app 48 | app.WatchHtmlFile("./hello-world.html", nil) 49 | // Register a handler function (that can be called directly from the UI) 50 | app.Runtime().PureHandler("test-handler", TestFn) 51 | // Deploy the app, check for errors 52 | err = client.AppClient().WriteAndConnectApp(app) 53 | if err != nil { 54 | fmt.Printf("Error writing app: %v\n", err) 55 | return 56 | } 57 | // Wait forever while your program is responding to handler requests 58 | client.WaitForShutdown() 59 | } 60 | ``` 61 | 62 | The HTML template to render your app (save as hello-world.html): 63 | ```HTML 64 | 65 |

Hello World

66 |
67 | Run Test Handler 68 | 69 |
70 |
71 | 72 |
73 | ``` 74 | 75 | Run your program. A new public/private keypair will be created and used to secure your new Dashborg account. You'll also see a secure link to your new application. Click to view your application! 76 | 77 | That's it. You've created and deployed, secure, interactive web-app that is directly calling code running on your local machine! 78 | 79 | ## Adding BLOBs 80 | 81 | Want to show an image, or add a CSV file your end users to download, here's how to do it: 82 | ``` 83 | client.GlobalFSClient().SetPathFromFile("/image/myimage.jpg", "./path-to-image.jpg", &dash.FileOpts{MimeType: "image/jpeg"}) 84 | client.GlobalFSClient().SetPathFromFile("/mydata.csv", "./path-to-csv.csv", &dash.FileOpts{MimeType: "text/csv"}) 85 | ``` 86 | 87 | Show the image using a regular <img> tag in your HTML template. Using the path prefix "/@raw/" 88 | allows for raw http GET access to your uploaded content: 89 | ``` 90 | 91 | ``` 92 | 93 | Download the CSV using a standard HTML download link: 94 | ``` 95 | Download CSV 96 | ``` 97 | 98 | Or use a Dashborg download control (defined in the standard Dashborg UI package) to make it look nice: 99 | ``` 100 | Download CSV 101 | ``` 102 | 103 | ## Adding Static Data 104 | 105 | Dashborg uses JSON to transfer data between your app and the Dashborg service. You can send any 106 | static JSON-compatible data to Dashborg using SetJsonPath(). Static data is available to 107 | apps even when there is no backend connected. For dynamic data, use Runtime().Handler(). 108 | Here we'll set favorite color table to the path "/colors.json". We used the AppFSClient() instead of 109 | the GlobalFSClient(). That makes the data local to the app and is accessible at /@app/colors.json: 110 | ```golang 111 | type FavColor struct { 112 | Name string `json:"name"` 113 | Color string `json:"color"` 114 | Hex string `json:"hex"` 115 | } 116 | 117 | colors := make([]FavColor, 0) 118 | colors = append(colors, FavColor{"Mike", "blue", "#007fff"}) 119 | colors = append(colors, FavColor{"Chris", "red", "#ee0000"}) 120 | colors = append(colors, FavColor{"Jenny", "purple", "#a020f0"}) 121 | app.AppFSClient().SetJsonPath("/colors.json", colors, nil) 122 | ``` 123 | 124 | Load the data into our datamodel using the <d-data> tag. Read from blob "colors", set it into the 125 | frontend data model at ```$.colors```: 126 | ```html 127 | 128 | ``` 129 | 130 | Show the first color name as text using ``````. Use the hex color to show a 131 | small color square using a background-color style (attributes and styles are dynamic when they starts with ```*```): 132 | ```html 133 |
134 | 's favorite color is : 135 |
136 |
137 | ``` 138 | 139 | You can loop using the built-in `````` tag (each element is bound to ```.``` inside the loop): 140 | ```html 141 |
    142 | 143 |
  • 144 |
    145 |
    - Favorite Color is
    146 |
    147 |
    148 |
  • 149 |
    150 |
151 | ``` 152 | 153 | Or use a Dashborg Table Control (@index is bound to the loop counter): 154 | ```html 155 | 156 | 157 | 158 | 159 | 160 |
161 | 162 | 163 | ``` 164 | 165 | ## Advanced Handlers / Forms 166 | 167 | Dashborg handlers are registered with reflection. The first argument is an optional ```dash.Request``` interface, ```*dash.AppRequest``` struct, or ```context.Context```. The rest of the arguments come from the frontend code. Functions return void, interface{}, error, or (interface{}, error). Errors are shown in the application (or handled by special error handlers), and the interface{} return value can be consumed by the calling code. 168 | 169 | Handlers that use ```*dash.AppRequest``` can also manipulate the frontend directly (aside from their return value) by calling SetData() to set or change values in the frontend data-model. 170 | 171 | Here's a handler that manipulates the frontend's data model (data is automatically marshaled as JSON): 172 | ```golang 173 | func Multiply2(req *dash.AppRequest, num int) error { 174 | req.SetData("$.output", num*2) 175 | return nil 176 | } 177 | 178 | // ... 179 | 180 | app.Runtime().Handler("mult2", Multiply2) 181 | ``` 182 | 183 | Now we'll use a button to call the function, and a div to show the return value. Note that HTML inputs 184 | produce strings, so we must convert the string to a number using fn:int(). 185 | ```html 186 | 187 |
188 | 189 | Multiply 190 |
191 |
192 | Output is 193 |
194 |
195 | ``` 196 | 197 | ## Security 198 | 199 | All communication from your backend to the Dashborg service is done over HTTPS/gRPC. Your account is authenticated 200 | with a public/private keypair that can be auto-generated by the Dashborg SDK (AutoKeygen config setting). 201 | 202 | The frontend is served over HTTPS, and each account is hosted on its own subdomain to prevent inter-account XSS attacks 203 | The Dashborg frontend offers pre-built authentication methods, with JWT tokens that are 204 | created from your private-key (the default for new anonymous accounts), simple passwords, or user logins. 205 | 206 | ## Advanced Features 207 | 208 | * Write your own Dashborg components to reuse among your applications 209 | * Create staging/development zones to test your apps without affecting your production site 210 | * Assign roles to users (and passwords), set a list of allowed roles per app per zone 211 | * Navigate to the '/@fs' path on your Dashborg account to see all of your static data, applications, and handlers. 212 | 213 | ## Want to learn more? 214 | 215 | * **Doc Site**: https://docs.dashborg.net/ 216 | * **Tutorial**: https://docs.dashborg.net/tutorials/t1/ 217 | * **Binding Data to Your HTML**: https://docs.dashborg.net/docs/binding-data/ 218 | * **GoDoc**: https://pkg.go.dev/github.com/sawka/dashborg-go-sdk/pkg/dash 219 | 220 | Questions? [Join the Dashborg Slack Channel](https://join.slack.com/t/dashborgworkspace/shared_invite/zt-uphltkhj-r6C62szzoYz7_IIsoJ8WPg) 221 | 222 | 223 | -------------------------------------------------------------------------------- /generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | protoc --go_out=plugins=grpc,paths=source_relative:. pkg/dashproto/dashproto.proto 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sawka/dashborg-go-sdk 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.5.1 7 | github.com/golang-jwt/jwt/v4 v4.0.0 8 | github.com/golang/protobuf v1.5.2 9 | github.com/google/uuid v1.3.0 10 | google.golang.org/grpc v1.40.0 11 | google.golang.org/protobuf v1.27.1 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 6 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 7 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 8 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 9 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 10 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 11 | github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 14 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 15 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 16 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 17 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= 18 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 19 | github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= 20 | github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= 21 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 22 | github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= 23 | github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= 24 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 25 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 26 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 28 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 29 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 30 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 31 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 32 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 33 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 34 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 35 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 36 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 37 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 38 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 39 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 40 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 41 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 42 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 43 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 44 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 45 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 46 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 47 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 48 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 49 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 50 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 51 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 53 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 54 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 55 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 56 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 57 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 58 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 59 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 60 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 61 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 62 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 63 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 64 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 65 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 66 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 67 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 68 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 69 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 70 | golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= 71 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 72 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 73 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 74 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 75 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 76 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 77 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 78 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 79 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 80 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 82 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 83 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 84 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 85 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 86 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 87 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 88 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 89 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 90 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 91 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 92 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 93 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 94 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 95 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 96 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 97 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 98 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= 99 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 100 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 101 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 102 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 103 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 104 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 105 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 106 | google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= 107 | google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= 108 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 109 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 110 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 111 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 112 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 113 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 114 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 115 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 116 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 117 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 118 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 119 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 120 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 121 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 122 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 123 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 124 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 125 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 126 | -------------------------------------------------------------------------------- /pkg/dash/auth.go: -------------------------------------------------------------------------------- 1 | package dash 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const MaxAuthExp = 24 * time.Hour 8 | const AuthScopeZone = "zone" 9 | 10 | type AuthAtom struct { 11 | Type string `json:"type"` // auth type (password, noauth, dashborg, deauth, or user-defined) 12 | Ts int64 `json:"ts"` // expiration Ts (ms) of this auth atom 13 | RoleList []string `json:"role"` 14 | Id string `json:"id,omitempty"` 15 | Data map[string]interface{} `json:"data,omitempty"` 16 | } 17 | 18 | // Returns AuthAtom role. If AuthAtom is nil, returns "public" 19 | func (aa *AuthAtom) HasRole(role string) bool { 20 | if aa == nil { 21 | return false 22 | } 23 | for _, checkRole := range aa.RoleList { 24 | if checkRole == role { 25 | return true 26 | } 27 | } 28 | return false 29 | } 30 | 31 | func (aa *AuthAtom) IsSuper() bool { 32 | return aa.HasRole(RoleSuper) 33 | } 34 | 35 | func (aa *AuthAtom) GetRoleList() []string { 36 | if aa == nil { 37 | return nil 38 | } 39 | return aa.RoleList 40 | } 41 | -------------------------------------------------------------------------------- /pkg/dash/config.go: -------------------------------------------------------------------------------- 1 | package dash 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "regexp" 13 | "strconv" 14 | "time" 15 | 16 | "github.com/golang-jwt/jwt/v4" 17 | "github.com/google/uuid" 18 | "github.com/sawka/dashborg-go-sdk/pkg/dashutil" 19 | "github.com/sawka/dashborg-go-sdk/pkg/keygen" 20 | ) 21 | 22 | const ( 23 | TlsKeyFileName = "dashborg-client.key" 24 | TlsCertFileName = "dashborg-client.crt" 25 | DefaultProcName = "default" 26 | DefaultZoneName = "default" 27 | DefaultPanelName = "default" 28 | DefaultLocalServerAddr = "localhost:8082" 29 | DefaultConsoleHost = "console.dashborg.net" 30 | DefaultJWTValidFor = 24 * time.Hour 31 | DefaultJWTUserId = "jwt-user" 32 | DefaultJWTRole = RoleUser 33 | ) 34 | 35 | const consoleHostDev = "console.dashborg-dev.com:8080" 36 | 37 | var DefaultJWTOpts = &JWTOpts{ 38 | ValidFor: DefaultJWTValidFor, 39 | Role: DefaultJWTRole, 40 | UserId: DefaultJWTUserId, 41 | } 42 | 43 | type Config struct { 44 | // DASHBORG_ACCID, set to force an AccountId (must match certificate). If not set, AccountId is set from certificate file. 45 | // If AccId is given and AutoKeygen is true, and key/cert files are not found, Dashborg will create a new self-signed 46 | // keypair using the AccId given. 47 | // If AccId is given, and the certificate does not match, this will cause a panic. 48 | AccId string 49 | 50 | // Set to true for unregistered/unclaimed accounts. New accounts will only be created if this flag is set. 51 | AnonAcc bool 52 | 53 | // DASHBORG_ZONE defaults to "default" 54 | ZoneName string 55 | 56 | // Process Name Attributes. Only ProcName is required 57 | ProcName string // DASHBORG_PROCNAME (set from executable filename if not set) 58 | ProcIKey string // DASHBORG_PROCIKEY (optional, user-specified key to identify procs in a cluster) 59 | ProcTags map[string]string // optional, user-specified key/values to identify this proc 60 | 61 | KeyFileName string // DASHBORG_KEYFILE private key file (defaults to dashborg-client.key) 62 | CertFileName string // DASHBORG_CERTFILE certificate file, CN must be set to your Dashborg Account Id. (defaults to dashborg-client.crt) 63 | 64 | // Create a self-signed key/cert if they do not exist. 65 | // If AccId is set, will create a key with that AccId, if AccId is not set, it will create a new random AccId. 66 | AutoKeygen bool 67 | 68 | // DASHBORG_VERBOSE, set to true for extra debugging information 69 | Verbose bool 70 | 71 | // close this channel to force a shutdown of the Dashborg Cloud Client 72 | ShutdownCh chan struct{} 73 | 74 | // These are for internal testing, should not normally be set by clients. 75 | Env string // DASHBORG_ENV 76 | GrpcHost string // DASHBORG_GRPCHOST 77 | GrpcPort int // DASHBORG_GRPCPORT 78 | ConsoleHost string // DASHBORG_CONSOLEHOST 79 | 80 | setupDone bool // internal 81 | 82 | // Used to override the JWT keys (valid time, userid, and role) that are printed to the log when 83 | // apps are connected to the Dashborg service. 84 | // To suppress writing JWT keys to the log, set NoJWT in this structure. 85 | // If left as nil, DefaultJWTOpts will be used. 86 | JWTOpts *JWTOpts 87 | 88 | Logger *log.Logger // use to override the SDK's logger object 89 | } 90 | 91 | var cmdRegexp *regexp.Regexp = regexp.MustCompile("^.*/") 92 | 93 | func (c *Config) setDefaults() { 94 | if c.Logger == nil { 95 | c.Logger = log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Lmsgprefix) 96 | } 97 | c.AccId = dashutil.DefaultString(c.AccId, os.Getenv("DASHBORG_ACCID")) 98 | c.ZoneName = dashutil.DefaultString(c.ZoneName, os.Getenv("DASHBORG_ZONE"), DefaultZoneName) 99 | c.Env = dashutil.DefaultString(c.Env, os.Getenv("DASHBORG_ENV"), "prod") 100 | if c.Env == "prod" { 101 | c.GrpcHost = dashutil.DefaultString(c.GrpcHost, os.Getenv("DASHBORG_GRPCHOST"), "") 102 | } else { 103 | c.GrpcHost = dashutil.DefaultString(c.GrpcHost, os.Getenv("DASHBORG_GRPCHOST"), "") 104 | } 105 | if c.Env == "prod" { 106 | c.ConsoleHost = dashutil.DefaultString(c.ConsoleHost, os.Getenv("DASHBORG_CONSOLEHOST"), DefaultConsoleHost) 107 | } else { 108 | c.ConsoleHost = dashutil.DefaultString(c.ConsoleHost, os.Getenv("DASHBORG_CONSOLEHOST"), consoleHostDev) 109 | } 110 | if c.GrpcPort == 0 { 111 | if os.Getenv("DASHBORG_GRPCPORT") != "" { 112 | var err error 113 | c.GrpcPort, err = strconv.Atoi(os.Getenv("DASHBORG_GRPCPORT")) 114 | if err != nil { 115 | c.log("Invalid DASHBORG_GRPCPORT environment variable: %v\n", err) 116 | } 117 | } 118 | if c.GrpcPort == 0 { 119 | c.GrpcPort = 7632 120 | } 121 | } 122 | var cmdName string 123 | if len(os.Args) > 0 { 124 | cmdName = cmdRegexp.ReplaceAllString(os.Args[0], "") 125 | } 126 | c.ProcName = dashutil.DefaultString(c.ProcName, os.Getenv("DASHBORG_PROCNAME"), cmdName, DefaultProcName) 127 | c.ProcIKey = dashutil.DefaultString(c.ProcIKey, os.Getenv("DASHBORG_PROCIKEY"), "") 128 | c.KeyFileName = dashutil.DefaultString(c.KeyFileName, os.Getenv("DASHBORG_KEYFILE"), TlsKeyFileName) 129 | c.CertFileName = dashutil.DefaultString(c.CertFileName, os.Getenv("DASHBORG_CERTFILE"), TlsCertFileName) 130 | c.Verbose = dashutil.EnvOverride(c.Verbose, "DASHBORG_VERBOSE") 131 | 132 | if c.JWTOpts == nil { 133 | c.JWTOpts = DefaultJWTOpts 134 | } 135 | err := c.JWTOpts.Validate() 136 | if err != nil { 137 | panic(fmt.Sprintf("Invalid JWTOpts in config: %s", err)) 138 | } 139 | } 140 | 141 | func (c *Config) setDefaultsAndLoadKeys() { 142 | if !c.setupDone { 143 | c.setDefaults() 144 | c.loadKeys() 145 | c.setupDone = true 146 | } 147 | } 148 | 149 | func (c *Config) loadKeys() { 150 | if c.AutoKeygen { 151 | err := c.maybeMakeKeys(c.AccId) 152 | if err != nil { 153 | panic(err) 154 | } 155 | } 156 | if _, errKey := os.Stat(c.KeyFileName); os.IsNotExist(errKey) { 157 | panic(fmt.Sprintf("Dashborg key file does not exist file:%s", c.KeyFileName)) 158 | } 159 | if _, errCert := os.Stat(c.CertFileName); os.IsNotExist(errCert) { 160 | panic(fmt.Sprintf("Dashborg certificate file does not exist file:%s", c.CertFileName)) 161 | } 162 | certInfo, err := readCertInfo(c.CertFileName) 163 | if err != nil { 164 | panic(err) 165 | } 166 | if c.AccId != "" && certInfo.AccId != c.AccId { 167 | panic(fmt.Sprintf("Dashborg AccId read from certificate:%s does not match AccId in config:%s", certInfo.AccId, c.AccId)) 168 | } 169 | c.AccId = certInfo.AccId 170 | c.log("DashborgCloudClient KeyFile:%s CertFile:%s AccId:%s SHA-256:%s\n", c.KeyFileName, c.CertFileName, c.AccId, certInfo.Pk256) 171 | } 172 | 173 | func (c *Config) maybeMakeKeys(accId string) error { 174 | if c.KeyFileName == "" || c.CertFileName == "" { 175 | return fmt.Errorf("Empty/Invalid Key or Cert filenames") 176 | } 177 | _, errKey := os.Stat(c.KeyFileName) 178 | _, errCert := os.Stat(c.CertFileName) 179 | if errKey == nil && errCert == nil { 180 | return nil 181 | } 182 | if errKey == nil || errCert == nil { 183 | return fmt.Errorf("Cannot make key:%s cert:%s, one or both files already exist", c.KeyFileName, c.CertFileName) 184 | } 185 | if accId == "" { 186 | accId = uuid.New().String() 187 | } 188 | err := keygen.CreateKeyPair(c.KeyFileName, c.CertFileName, accId) 189 | if err != nil { 190 | return fmt.Errorf("Cannot create keypair err:%v", err) 191 | } 192 | c.log("Dashborg created new self-signed keypair %s / %s for new AccId:%s\n", c.KeyFileName, c.CertFileName, accId) 193 | return nil 194 | } 195 | 196 | type certInfo struct { 197 | AccId string 198 | Pk256 string 199 | PublicKey interface{} 200 | } 201 | 202 | func readCertInfo(certFileName string) (*certInfo, error) { 203 | certBytes, err := ioutil.ReadFile(certFileName) 204 | if err != nil { 205 | return nil, fmt.Errorf("Cannot read certificate file:%s err:%w", certFileName, err) 206 | } 207 | block, _ := pem.Decode(certBytes) 208 | if block == nil || block.Type != "CERTIFICATE" { 209 | return nil, fmt.Errorf("Certificate file malformed, failed to Decode PEM CERTIFICATE block from file:%s", certFileName) 210 | } 211 | cert, err := x509.ParseCertificate(block.Bytes) 212 | if err != nil || cert == nil { 213 | return nil, fmt.Errorf("Error parsing certificate from file:%s err:%w", certFileName, err) 214 | } 215 | cn := cert.Subject.CommonName 216 | if cn == "" || !dashutil.IsUUIDValid(cn) { 217 | return nil, fmt.Errorf("Invalid CN in certificate. CN should be set to Dashborg Account ID (UUID formatted, 36 chars) CN:%s", cn) 218 | } 219 | pubKeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey) 220 | if err != nil { 221 | return nil, fmt.Errorf("Cannot get PublicKey bytes from certificate") 222 | } 223 | pk256Str := dashutil.Sha256Base64(pubKeyBytes) 224 | return &certInfo{AccId: cn, Pk256: pk256Str, PublicKey: cert.PublicKey}, nil 225 | } 226 | 227 | func (c *Config) loadPrivateKey() (interface{}, error) { 228 | cert, err := tls.LoadX509KeyPair(c.CertFileName, c.KeyFileName) 229 | if err != nil { 230 | return nil, fmt.Errorf("Error loading x509 key pair cert[%s] key[%s]: %w", c.CertFileName, c.KeyFileName, err) 231 | } 232 | ecKey, ok := cert.PrivateKey.(*ecdsa.PrivateKey) 233 | if !ok { 234 | return nil, fmt.Errorf("Invalid private key %s, must be ECDSA", c.KeyFileName) 235 | } 236 | return ecKey, nil 237 | } 238 | 239 | // Creates a JWT token from the public/private keypair. 240 | // The jwtOpts parameter, if not nil, will override the config's JWTOpts field. 241 | func (c *Config) MakeAccountJWT(jwtOpts *JWTOpts) (string, error) { 242 | c.setDefaultsAndLoadKeys() 243 | if jwtOpts == nil { 244 | jwtOpts = c.GetJWTOpts() 245 | } 246 | if jwtOpts.NoJWT { 247 | return "", fmt.Errorf("NoJWT set in JWTOpts") 248 | } 249 | ecKey, err := c.loadPrivateKey() 250 | if err != nil { 251 | return "", err 252 | } 253 | err = jwtOpts.Validate() 254 | if err != nil { 255 | return "", err 256 | } 257 | jwtValidFor := jwtOpts.ValidFor 258 | if jwtValidFor == 0 { 259 | jwtValidFor = DefaultJWTValidFor 260 | } 261 | jwtRole := jwtOpts.Role 262 | if jwtRole == "" { 263 | jwtRole = DefaultJWTRole 264 | } 265 | jwtUserId := jwtOpts.UserId 266 | if jwtUserId == "" { 267 | jwtUserId = DefaultJWTUserId 268 | } 269 | claims := jwt.MapClaims{} 270 | claims["iss"] = "dashborg" 271 | claims["exp"] = time.Now().Add(jwtValidFor).Unix() 272 | claims["iat"] = time.Now().Add(-5 * time.Second).Unix() // skeww 273 | claims["jti"] = uuid.New().String() 274 | claims["dash-acc"] = c.AccId 275 | claims["aud"] = "dashborg-auth" 276 | claims["sub"] = jwtUserId 277 | claims["role"] = jwtRole 278 | token := jwt.NewWithClaims(jwt.GetSigningMethod("ES384"), claims) 279 | jwtStr, err := token.SignedString(ecKey) 280 | if err != nil { 281 | return "", fmt.Errorf("Error signing JWT: %w", err) 282 | } 283 | return jwtStr, nil 284 | } 285 | 286 | // Calls MakeAccountJWT, and panics on error. 287 | func (c *Config) MustMakeAccountJWT(jwtOpts *JWTOpts) string { 288 | rtn, err := c.MakeAccountJWT(jwtOpts) 289 | if err != nil { 290 | panic(err) 291 | } 292 | return rtn 293 | } 294 | 295 | func (c *Config) log(fmtStr string, args ...interface{}) { 296 | if c.Logger != nil { 297 | c.Logger.Printf(fmtStr, args...) 298 | } else { 299 | log.Printf(fmtStr, args...) 300 | } 301 | } 302 | 303 | func (c *Config) copyJWTOpts() JWTOpts { 304 | return *c.JWTOpts 305 | } 306 | 307 | // Returns the config's JWTOpts structure. Does not return nil. 308 | // If config's JWTOpts is nil, will return DefaultJWTOpts 309 | func (c *Config) GetJWTOpts() *JWTOpts { 310 | if c.JWTOpts == nil { 311 | return DefaultJWTOpts 312 | } 313 | return c.JWTOpts 314 | } 315 | -------------------------------------------------------------------------------- /pkg/dash/dash.go: -------------------------------------------------------------------------------- 1 | // Provides all of the functionality to create and deploy web applications with the Dashborg framework. 2 | package dash 3 | 4 | const ClientVersion = "go-0.7.4" 5 | 6 | const ( 7 | RoleSuper = "*" 8 | RolePublic = "public" 9 | RoleUser = "user" 10 | ) 11 | 12 | const ( 13 | AccTypeAnon = "anon" 14 | AccTypeFree = "free" 15 | AccTypePro = "pro" 16 | AccTypeEnterprise = "enterprise" 17 | ) 18 | 19 | const ( 20 | RequestMethodGet = "GET" 21 | RequestMethodPost = "POST" 22 | ) 23 | 24 | const RtnSetDataPath = "@rtn" 25 | -------------------------------------------------------------------------------- /pkg/dash/dashapp.go: -------------------------------------------------------------------------------- 1 | package dash 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/sawka/dashborg-go-sdk/pkg/dasherr" 8 | "github.com/sawka/dashborg-go-sdk/pkg/dashutil" 9 | ) 10 | 11 | var notAuthorizedErr = fmt.Errorf("Not Authorized") 12 | 13 | const RootAppPath = "/_/apps" 14 | const RootProcPath = "/_/procs" 15 | const MaxAppConfigSize = 10000 16 | const htmlMimeType = "text/html" 17 | const htmlBlobNs = "html" 18 | const rootHtmlKey = htmlBlobNs + ":" + "root" 19 | const jsonMimeType = "application/json" 20 | const initialHtmlPageDefault = "default" 21 | 22 | const ( 23 | requestTypeHtml = "html" 24 | requestTypeInit = "init" 25 | requestTypeData = "data" 26 | requestTypeAuth = "auth" 27 | requestTypeStream = "stream" 28 | requestTypeHandler = "handler" 29 | requestTypePath = "path" 30 | ) 31 | 32 | const ( 33 | VisTypeHidden = "hidden" // always hide 34 | VisTypeDefault = "default" // shown if user has permission 35 | VisTypeAlwaysVisible = "visible" // always show 36 | ) 37 | 38 | // AppConfig is passed as JSON to the container. this struct 39 | // helps with marshaling/unmarshaling the structure. 40 | // The AppConfig struct is generated by the App class. Normally 41 | // end-users do not have to deal with this struct directly. 42 | type AppConfig struct { 43 | AppName string `json:"appname"` 44 | ClientVersion string `json:"clientversion"` 45 | AppTitle string `json:"apptitle,omitempty"` 46 | AppVisType string `json:"appvistype,omitempty"` 47 | AppVisOrder float64 `json:"appvisorder,omitempty"` 48 | AllowedRoles []string `json:"allowedroles"` 49 | InitRequired bool `json:"initrequired"` 50 | OfflineAccess bool `json:"offlineaccess"` 51 | HtmlPath string `json:"htmlpath"` 52 | InitialHtmlPage string `json:"initialhtmlpage"` 53 | RuntimePath string `json:"runtimepath,omitempty"` // empty for ./runtime 54 | PagesEnabled bool `json:"pagesenabled,omitempty"` 55 | } 56 | 57 | type middlewareType struct { 58 | Name string 59 | Fn MiddlewareFuncType 60 | Priority float64 61 | } 62 | 63 | type MiddlewareNextFuncType func(req *AppRequest) (interface{}, error) 64 | type MiddlewareFuncType func(req *AppRequest, nextFn MiddlewareNextFuncType) (interface{}, error) 65 | 66 | type ProcInfo struct { 67 | StartTs int64 68 | ProcRunId string 69 | ProcName string 70 | ProcTags map[string]string 71 | HostData map[string]string 72 | } 73 | 74 | type App struct { 75 | client *DashCloudClient 76 | appName string 77 | appConfig AppConfig 78 | appRuntime *AppRuntimeImpl 79 | htmlStr string 80 | htmlFileName string 81 | htmlFileWatchOpts *WatchOpts 82 | htmlFromRuntime bool 83 | htmlExtPath string 84 | errs []error 85 | } 86 | 87 | // Returns the app's internal runtime. Used to set handler functions. 88 | // Errors that happen while setting Handlers will be available in app.Err(). 89 | func (app *App) Runtime() *AppRuntimeImpl { 90 | return app.appRuntime 91 | } 92 | 93 | // Set a different AppRuntimeImpl as this app's runtime. Not normally used except 94 | // in special cases. Should not set the runtime after the app is already connected to the Dashborg service. 95 | func (app *App) SetRuntime(apprt *AppRuntimeImpl) { 96 | if apprt == nil { 97 | apprt = MakeAppRuntime() 98 | } 99 | app.appRuntime = apprt 100 | } 101 | 102 | func makeApp(client *DashCloudClient, appName string) *App { 103 | var appNameErr error 104 | if !dashutil.IsAppNameValid(appName) { 105 | appNameErr = dasherr.ValidateErr(fmt.Errorf("MakeApp: Invalid appName '%s'", appName)) 106 | } 107 | rtn := &App{ 108 | client: client, 109 | appName: appName, 110 | appConfig: AppConfig{ 111 | ClientVersion: ClientVersion, 112 | AppName: appName, 113 | AllowedRoles: []string{RoleUser}, 114 | }, 115 | appRuntime: MakeAppRuntime(), 116 | } 117 | if appNameErr != nil { 118 | rtn.errs = append(rtn.errs, appNameErr) 119 | } 120 | return rtn 121 | } 122 | 123 | func makeAppFromConfig(client *DashCloudClient, cfg AppConfig) (*App, error) { 124 | cfg.ClientVersion = ClientVersion 125 | err := cfg.Validate() 126 | if err != nil { 127 | return nil, err 128 | } 129 | rtn := &App{ 130 | client: client, 131 | appName: cfg.AppName, 132 | appConfig: cfg, 133 | appRuntime: MakeAppRuntime(), 134 | } 135 | return rtn, nil 136 | } 137 | 138 | // Sets an external runtime path. If this is set, the app's own Runtime() will not be used. 139 | // Should call WriteApp, not WriteAndConnectApp if an external runtime is specified. 140 | func (app *App) SetExternalAppRuntimePath(runtimePath string) { 141 | app.appConfig.RuntimePath = runtimePath 142 | } 143 | 144 | // Returns true if the app has an external runtime 145 | func (app *App) HasExternalRuntime() bool { 146 | return app.getRuntimePath() != app.defaultRuntimePath() 147 | } 148 | 149 | // offline mode type is either OfflineModeEnable or OfflineModeDisable 150 | func (app *App) SetOfflineAccess(offlineAccess bool) { 151 | app.appConfig.OfflineAccess = offlineAccess 152 | } 153 | 154 | // Returns the App's AppConfig struct suitable for marshaling into JSON. Used 155 | // internally to send the app definition to the Dashborg service. Not normally called 156 | // by user facing code. 157 | func (app *App) AppConfig() (AppConfig, error) { 158 | if len(app.errs) > 0 { 159 | return AppConfig{}, app.Err() 160 | } 161 | err := app.validateHtmlOpts() 162 | if err != nil { 163 | return AppConfig{}, err 164 | } 165 | app.appConfig.HtmlPath = app.getHtmlPath() 166 | if app.appConfig.InitialHtmlPage == "" { 167 | app.appConfig.InitialHtmlPage = initialHtmlPageDefault 168 | } 169 | app.appConfig.RuntimePath = app.getRuntimePath() 170 | app.appConfig.ClientVersion = ClientVersion 171 | return app.appConfig, nil 172 | } 173 | 174 | // Validates the AppConfig. Returns an error if the config is not valid. 175 | func (config *AppConfig) Validate() error { 176 | if !dashutil.IsAppNameValid(config.AppName) { 177 | return dasherr.ValidateErr(fmt.Errorf("Invalid AppName: '%s'", config.AppName)) 178 | } 179 | _, _, _, err := dashutil.ParseFullPath(config.HtmlPath, true) 180 | if err != nil { 181 | return dasherr.ValidateErr(err) 182 | } 183 | _, _, err = dashutil.ParseHtmlPage(config.InitialHtmlPage) 184 | if err != nil { 185 | return dasherr.ValidateErr(err) 186 | } 187 | _, _, _, err = dashutil.ParseFullPath(config.RuntimePath, false) 188 | if err != nil { 189 | return dasherr.ValidateErr(err) 190 | } 191 | if !dashutil.IsClientVersionValid(config.ClientVersion) { 192 | return dasherr.ValidateErr(fmt.Errorf("Invalid ClientVersion")) 193 | } 194 | if !dashutil.IsRoleListValid(strings.Join(config.AllowedRoles, ",")) { 195 | return dasherr.ValidateErr(fmt.Errorf("Invalid AllowedRoles")) 196 | } 197 | if len(config.AppTitle) > 80 { 198 | return dasherr.ValidateErr(fmt.Errorf("AppTitle too long")) 199 | } 200 | if len(config.AllowedRoles) == 0 { 201 | return dasherr.ValidateErr(fmt.Errorf("AllowedRoles cannot be empty")) 202 | } 203 | return nil 204 | } 205 | 206 | func wrapHandler(handlerFn func(req *AppRequest) error) func(req *AppRequest) (interface{}, error) { 207 | wrappedHandlerFn := func(req *AppRequest) (interface{}, error) { 208 | err := handlerFn(req) 209 | return nil, err 210 | } 211 | return wrappedHandlerFn 212 | } 213 | 214 | // Set the roles that are allowed to access this app. By default the allowed roles are set to ["user"]. 215 | func (app *App) SetAllowedRoles(roles ...string) { 216 | app.appConfig.AllowedRoles = roles 217 | } 218 | 219 | // Sets an title for this app (that shows up in the App Switcher and in the navigation bar). 220 | // If not given, the app's title will be set to the app's name. 221 | // This is a static value (cannot be changed at runtime) and must be set before WriteApp is called. 222 | // To change the title at runtime, use the client path $state.dashborg.apptitle. 223 | func (app *App) SetAppTitle(title string) { 224 | app.appConfig.AppTitle = title 225 | } 226 | 227 | // SetAppVisibility controls whether the app shows in the UI's app-switcher (see VisType constants) 228 | // Apps will be sorted by displayOrder (and then AppTitle). displayOrder of 0 (the default) will 229 | // sort to the end of the list, not the beginning 230 | // visType is either VisTypeHidden, VisTypeDefault, or VisTypeAlwaysVisible 231 | func (app *App) SetAppVisibility(visType string, visOrder float64) { 232 | app.appConfig.AppVisType = visType 233 | app.appConfig.AppVisOrder = visOrder 234 | } 235 | 236 | // Clear all of an app's HTML settings (static, dynamic, watches, etc.) 237 | func (app *App) ClearHtml() { 238 | app.htmlStr = "" 239 | app.htmlFileName = "" 240 | app.htmlFileWatchOpts = nil 241 | app.htmlFromRuntime = false 242 | app.htmlExtPath = "" 243 | app.appConfig.HtmlPath = "" 244 | app.appConfig.InitialHtmlPage = "" 245 | } 246 | 247 | // Set a static HTML string as the app's HTML. Will be written to /@app/_/html 248 | func (app *App) SetHtml(htmlStr string) { 249 | app.ClearHtml() 250 | app.htmlStr = htmlStr 251 | return 252 | } 253 | 254 | // Set an app's HTML from the given fileName. Will be written to /@app/_/html 255 | func (app *App) SetHtmlFromFile(fileName string) { 256 | app.ClearHtml() 257 | app.htmlFileName = fileName 258 | return 259 | } 260 | 261 | // Set an app's HTML from the given fileName. *After* the app is connected or written 262 | // to the Dashborg service, the function will set up an fsnotify watcher on the given file. 263 | // If the file is changed, it will re-upload the file to the Dashborg service. Content 264 | // will be written to /@app/_/html 265 | func (app *App) WatchHtmlFile(fileName string, watchOpts *WatchOpts) { 266 | app.ClearHtml() 267 | app.htmlFileName = fileName 268 | if watchOpts == nil { 269 | watchOpts = &WatchOpts{} 270 | } 271 | app.htmlFileWatchOpts = watchOpts 272 | return 273 | } 274 | 275 | // For dynamic HTML. To get app's HTML, Dashborg will call the app runtime's HTML handler 276 | // (see App.Runtime().SetHtmlHandler). 277 | func (app *App) SetHtmlFromRuntime() { 278 | app.ClearHtml() 279 | app.htmlFromRuntime = true 280 | } 281 | 282 | // If set to true, Dashborg will call the runtime's init method (see App.Runtime().SetInitHandler) 283 | // before loading the app. If InitRequired is set, app cannot be viewed offline (App.SetOfflineAccess 284 | // should be false). Any error returned in the init function will cause the app not to load. 285 | func (app *App) SetInitRequired(initRequired bool) { 286 | app.appConfig.InitRequired = initRequired 287 | } 288 | 289 | // Set PagesEnabled to true to allow your app to serve different frontend/UI pages. When PagesEnabled 290 | // is false, the app will only have one logical page (single page app). 291 | func (app *App) SetPagesEnabled(pagesEnabled bool) { 292 | app.appConfig.PagesEnabled = pagesEnabled 293 | } 294 | 295 | // Returns the app's name. 296 | func (app *App) AppName() string { 297 | return app.appConfig.AppName 298 | } 299 | 300 | func (app *App) validateHtmlOpts() error { 301 | var htmlTypes []string 302 | if app.htmlStr != "" { 303 | htmlTypes = append(htmlTypes, fmt.Sprintf("static-html [len=%d]", len(app.htmlStr))) 304 | } 305 | if app.htmlFileName != "" { 306 | htmlTypes = append(htmlTypes, fmt.Sprintf("html file [%s]", app.htmlFileName)) 307 | } 308 | if app.htmlExtPath != "" { 309 | htmlTypes = append(htmlTypes, fmt.Sprintf("html path [%s]", app.htmlExtPath)) 310 | } 311 | if app.htmlFromRuntime { 312 | htmlTypes = append(htmlTypes, fmt.Sprintf("html from runtime")) 313 | } 314 | if len(htmlTypes) >= 2 { 315 | return dasherr.ValidateErr(fmt.Errorf("Invalid App HTML configuration (multiple conflicting types): %s", strings.Join(htmlTypes, ", "))) 316 | } 317 | return nil 318 | } 319 | 320 | func (app *App) getHtmlPath() string { 321 | if app.htmlStr != "" || app.htmlFileName != "" { 322 | return app.defaultHtmlPath() 323 | } 324 | if app.htmlFromRuntime { 325 | return fmt.Sprintf("%s:@html", app.getRuntimePath()) 326 | } 327 | if app.htmlExtPath != "" { 328 | return app.htmlExtPath 329 | } 330 | if app.appConfig.HtmlPath != "" { 331 | return app.appConfig.HtmlPath 332 | } 333 | return app.defaultHtmlPath() 334 | } 335 | 336 | func (app *App) getRuntimePath() string { 337 | if app.appConfig.RuntimePath != "" { 338 | return app.appConfig.RuntimePath 339 | } 340 | return app.defaultRuntimePath() 341 | } 342 | 343 | func (app *App) defaultRuntimePath() string { 344 | return app.AppPath() + AppRuntimeSubPath 345 | } 346 | 347 | func (app *App) defaultHtmlPath() string { 348 | return app.AppPath() + AppHtmlSubPath 349 | } 350 | 351 | // Returns any error during the setup phase of an app. Errors can be checked manually 352 | // after functions like Runtime().Handler(), but will also be returned when the app 353 | // is written or connected to the Dashborg service. 354 | func (app *App) Err() error { 355 | var errs []error 356 | errs = append(app.errs, app.appRuntime.errs...) 357 | return dashutil.ConvertErrArray(errs) 358 | } 359 | 360 | // Returns the canonical app path - /_/apps/[app-name] 361 | func (app *App) AppPath() string { 362 | return AppPathFromName(app.appName) 363 | } 364 | 365 | // Returns a FSClient that is rooted at AppPath() (/_/apps/[app-name]). 366 | // Allows you to write data/files that are local to the app. Can be 367 | // accessed in the UI using /@app/[relative-path]. 368 | func (app *App) AppFSClient() *DashFSClient { 369 | if !dashutil.IsAppNameValid(app.appName) { 370 | return &DashFSClient{client: app.client, rootPath: "@error:InvalidAppName"} 371 | } 372 | return &DashFSClient{client: app.client, rootPath: app.AppPath()} 373 | } 374 | -------------------------------------------------------------------------------- /pkg/dash/dashappclient.go: -------------------------------------------------------------------------------- 1 | package dash 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/sawka/dashborg-go-sdk/pkg/dasherr" 9 | "github.com/sawka/dashborg-go-sdk/pkg/dashutil" 10 | ) 11 | 12 | const ( 13 | AppRuntimeSubPath = "/_/runtime" 14 | AppHtmlSubPath = "/_/html" 15 | ) 16 | 17 | type DashAppClient struct { 18 | client *DashCloudClient 19 | } 20 | 21 | // Create a new app with the given name. The app is just created 22 | // client side, it is not written to the server. 23 | // Invalid names will be reported in App.Err() 24 | func (dac *DashAppClient) NewApp(appName string) *App { 25 | return makeApp(dac.client, appName) 26 | } 27 | 28 | // Create a new app from the passed in AppConfig. Config must be valid and complete. 29 | // Normal end users should use LoadApp(), not this function. 30 | func (dac *DashAppClient) NewAppFromConfig(cfg AppConfig) (*App, error) { 31 | return makeAppFromConfig(dac.client, cfg) 32 | } 33 | 34 | // Tries to load an app from the Dashborg service with the given name. If no existing 35 | // app is found then if createIfNotFound is false will return nil, nil. If createIfNotFound 36 | // is true, will return NewApp(appName). 37 | func (dac *DashAppClient) LoadApp(appName string, createIfNotFound bool) (*App, error) { 38 | appPath := AppPathFromName(appName) 39 | finfos, _, err := dac.client.fileInfo(appPath, nil, false) 40 | if err != nil { 41 | return nil, err 42 | } 43 | if finfos == nil || len(finfos) == 0 { 44 | if createIfNotFound { 45 | return dac.NewApp(appName), nil 46 | } 47 | return nil, nil 48 | } 49 | finfo := finfos[0] 50 | if finfo.FileType != FileTypeApp || finfo.AppConfigJson == "" { 51 | return nil, nil 52 | } 53 | var config AppConfig 54 | err = json.Unmarshal([]byte(finfo.AppConfigJson), &config) 55 | if err != nil { 56 | return nil, err 57 | } 58 | return makeAppFromConfig(dac.client, config) 59 | } 60 | 61 | // Writes the app to the Dashborg service. Note that the app runtime will 62 | // *not* be connected. This is used to create or update an app's settings, 63 | // offline apps, or apps with external runtimes. 64 | func (dac *DashAppClient) WriteApp(app *App) error { 65 | return dac.baseWriteApp(app, false) 66 | } 67 | 68 | // Writes the app to the Dashborg service and connects the app's runtime to 69 | // receive requests. If an app uses an external runtime, you should call 70 | // WriteApp(), not WriteAndConnectApp(). 71 | func (dac *DashAppClient) WriteAndConnectApp(app *App) error { 72 | return dac.baseWriteApp(app, true) 73 | } 74 | 75 | // Given an app name, returns the canonical path (e.g. /_/apps/[appName]) 76 | func AppPathFromName(appName string) string { 77 | return RootAppPath + "/" + appName 78 | } 79 | 80 | // Removes the app and any static data/runtimes/files under the canonical app root. 81 | // So any file that was created using the AppFSClient() will also be removed. 82 | // Any connected runtimes will also be disconnected. 83 | func (dac *DashAppClient) RemoveApp(appName string) error { 84 | if !dashutil.IsAppNameValid(appName) { 85 | return dasherr.ValidateErr(fmt.Errorf("Invalid App Name")) 86 | } 87 | err := dac.client.removeAppPath(appName) 88 | if err != nil { 89 | return err 90 | } 91 | return nil 92 | } 93 | 94 | // Connects the app's runtime without modifying any settings (does not write 95 | // the app config to the Dashborg service). Note that a different WriteApp 96 | // or WriteAndConenctApp must have already created the app. 97 | func (dac *DashAppClient) ConnectAppRuntime(app *App) error { 98 | appConfig, err := app.AppConfig() 99 | if err != nil { 100 | return err 101 | } 102 | if app.Runtime() == nil { 103 | return dasherr.ValidateErr(fmt.Errorf("No AppRuntime to connect, app.Runtime() is nil")) 104 | } 105 | if app.HasExternalRuntime() { 106 | return dasherr.ValidateErr(fmt.Errorf("App has specified an external runtime path '%s', use DashFS().LinkAppRuntime() to connect", app.getRuntimePath())) 107 | } 108 | runtimePath := appConfig.RuntimePath 109 | err = dac.client.connectLinkRpc(appConfig.RuntimePath) 110 | if err != nil { 111 | return err 112 | } 113 | dac.client.connectLinkRuntime(runtimePath, app.Runtime()) 114 | return nil 115 | } 116 | 117 | // Creates a URL to link to an app given its name. Optional jwtOpts to override the 118 | // config's default jwt options. 119 | func (dac *DashAppClient) MakeAppUrl(appNameOrPath string, jwtOpts *JWTOpts) (string, error) { 120 | if appNameOrPath == "" { 121 | return "", fmt.Errorf("Invalid App Path") 122 | } 123 | if appNameOrPath[0] == '/' { 124 | return dac.client.GlobalFSClient().MakePathUrl(appNameOrPath, jwtOpts) 125 | } 126 | appName := appNameOrPath 127 | accHost := dac.client.getAccHost() 128 | baseUrl := accHost + dashutil.MakeAppPath(dac.client.Config.ZoneName, appName) 129 | if jwtOpts == nil { 130 | jwtOpts = dac.client.Config.GetJWTOpts() 131 | } 132 | if jwtOpts.NoJWT { 133 | return baseUrl, nil 134 | } 135 | jwtToken, err := dac.client.Config.MakeAccountJWT(jwtOpts) 136 | if err != nil { 137 | return "", err 138 | } 139 | return fmt.Sprintf("%s?jwt=%s", baseUrl, jwtToken), nil 140 | } 141 | 142 | func (dac *DashAppClient) baseWriteApp(app *App, shouldConnect bool) error { 143 | appConfig, err := app.AppConfig() 144 | if err != nil { 145 | return err 146 | } 147 | if shouldConnect && app.HasExternalRuntime() { 148 | return dasherr.ValidateErr(fmt.Errorf("App has specified an external runtime path '%s', use DashFS().LinkAppRuntime() to connect", app.getRuntimePath())) 149 | } 150 | roles := appConfig.AllowedRoles 151 | appConfigJson, err := dashutil.MarshalJson(appConfig) 152 | if err != nil { 153 | return dasherr.JsonMarshalErr("AppConfig", err) 154 | } 155 | fs := dac.client.GlobalFSClient() 156 | err = fs.SetRawPath(app.AppPath(), nil, &FileOpts{FileType: FileTypeApp, MimeType: MimeTypeDashborgApp, AllowedRoles: roles, AppConfigJson: appConfigJson}, nil) 157 | if err != nil { 158 | return err 159 | } 160 | // test html for error earlier 161 | htmlPath := appConfig.HtmlPath 162 | htmlFileOpts := &FileOpts{MimeType: MimeTypeHtml, AllowedRoles: roles} 163 | if app.htmlStr != "" { 164 | err = fs.SetStaticPath(htmlPath, bytes.NewReader([]byte(app.htmlStr)), htmlFileOpts) 165 | } else if app.htmlFileName != "" { 166 | if app.htmlFileWatchOpts == nil { 167 | err = fs.SetPathFromFile(htmlPath, app.htmlFileName, htmlFileOpts) 168 | } else { 169 | err = fs.WatchFile(htmlPath, app.htmlFileName, htmlFileOpts, app.htmlFileWatchOpts) 170 | } 171 | } 172 | if err != nil { 173 | return err 174 | } 175 | if shouldConnect { 176 | runtimePath := appConfig.RuntimePath 177 | err = fs.LinkAppRuntime(runtimePath, app.Runtime(), &FileOpts{AllowedRoles: roles}) 178 | if err != nil { 179 | return err 180 | } 181 | } 182 | appLink, err := dac.MakeAppUrl(appConfig.AppName, nil) 183 | if err == nil { 184 | dac.client.log("Dashborg App Link [%s]: %s\n", appConfig.AppName, appLink) 185 | } 186 | return nil 187 | } 188 | -------------------------------------------------------------------------------- /pkg/dash/dashcloud.go: -------------------------------------------------------------------------------- 1 | package dash 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/sawka/dashborg-go-sdk/pkg/dasherr" 8 | "github.com/sawka/dashborg-go-sdk/pkg/dashutil" 9 | ) 10 | 11 | func ConnectClient(config *Config) (*DashCloudClient, error) { 12 | config.setDefaultsAndLoadKeys() 13 | container := makeCloudClient(config) 14 | err := container.startClient() 15 | if err != nil { 16 | return nil, err 17 | } 18 | return container, nil 19 | } 20 | 21 | type ReflectProcType struct { 22 | StartTs int64 `json:"startts"` 23 | ProcName string `json:"procname"` 24 | ProcIKey string `json:"procikey"` 25 | ProcTags map[string]string `json:"proctags"` 26 | ProcRunId string `json:"procrunid"` 27 | } 28 | 29 | type JWTOpts struct { 30 | NoJWT bool 31 | ValidFor time.Duration 32 | UserId string 33 | Role string 34 | } 35 | 36 | func (jwtOpts *JWTOpts) Validate() error { 37 | if jwtOpts.NoJWT { 38 | return nil 39 | } 40 | if jwtOpts.ValidFor < 0 { 41 | return dasherr.ValidateErr(fmt.Errorf("Invalid ValidTime (negative)")) 42 | } 43 | if jwtOpts.ValidFor > 24*time.Hour { 44 | return dasherr.ValidateErr(fmt.Errorf("Maximum validFor for JWT tokens is 24-hours")) 45 | } 46 | if jwtOpts.Role != "" && !dashutil.IsRoleListValid(jwtOpts.Role) { 47 | return dasherr.ValidateErr(fmt.Errorf("Invalid Role")) 48 | } 49 | if jwtOpts.UserId != "" && !dashutil.IsUserIdValid(jwtOpts.UserId) { 50 | return dasherr.ValidateErr(fmt.Errorf("Invalid UserId")) 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/dash/dashfs.go: -------------------------------------------------------------------------------- 1 | package dash 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "time" 13 | 14 | "github.com/fsnotify/fsnotify" 15 | "github.com/sawka/dashborg-go-sdk/pkg/dasherr" 16 | "github.com/sawka/dashborg-go-sdk/pkg/dashutil" 17 | ) 18 | 19 | const ( 20 | MimeTypeDashborgHtml = "text/x-dashborg-html" 21 | MimeTypeHtml = "text/html" 22 | MimeTypeJson = "application/json" 23 | MimeTypeDashborgApp = "application/x-dashborg+json" 24 | ) 25 | 26 | const ( 27 | FileTypeStatic = "static" 28 | FileTypeRuntimeLink = "rt-link" 29 | FileTypeAppRuntimeLink = "rt-app-link" 30 | FileTypeDir = "dir" 31 | FileTypeApp = "app" 32 | ) 33 | 34 | // Represents the metadata for a "file" in the Dashborg FS. 35 | // Returned from DashFSClient.FileInfo() or DashFSClient.DirInfo(). 36 | type FileInfo struct { 37 | ParentDir string `json:"parentdir"` 38 | FileName string `json:"filename"` 39 | Path string `json:"path"` 40 | Size int64 `json:"size"` 41 | CreatedTs int64 `json:"createdts"` 42 | UpdatedTs int64 `json:"updatedts"` 43 | Sha256 string `json:"sha256"` 44 | FileType string `json:"filetype"` 45 | MimeType string `json:"mimetype"` 46 | AllowedRoles []string `json:"allowedroles"` 47 | EditRoles []string `json:"editroles"` 48 | Display string `json:"display,omitempty"` 49 | MetadataJson string `json:'metadata,omitempty"` // json-string 50 | Description string `json:"description,omitempty"` 51 | Hidden bool `json:"hidden,omitempty"` 52 | Removed bool `json:"removed,omitempty"` 53 | ProcLinks []string `json:"proclinks,omitempty"` 54 | TxId string `json:"txid,omitempty"` 55 | AppConfigJson string `json:"appconfig"` // json-string 56 | } 57 | 58 | // Unmarshals the FileInfo's metadata into an object (like json.Unmarshal). 59 | func (finfo *FileInfo) BindMetadata(obj interface{}) error { 60 | return json.Unmarshal([]byte(finfo.MetadataJson), obj) 61 | } 62 | 63 | // Returns true if this FileInfo is a RuntimeLink or AppRuntimeLink (can have an attached Runtime). 64 | func (finfo *FileInfo) IsLinkType() bool { 65 | return finfo.FileType == FileTypeRuntimeLink || finfo.FileType == FileTypeAppRuntimeLink 66 | } 67 | 68 | // Special return value from handler functions to return BLOB data. 69 | type BlobReturn struct { 70 | Reader io.Reader 71 | MimeType string 72 | } 73 | 74 | // Options that set or update a new file's FileInfo metadata. 75 | // Not all options are required for all file types. 76 | type FileOpts struct { 77 | FileType string `json:"filetype"` 78 | Sha256 string `json:"sha256"` 79 | Size int64 `json:"size"` 80 | MimeType string `json:"mimetype"` 81 | AllowedRoles []string `json:"allowedroles,omitempty"` 82 | EditRoles []string `json:"editroles,omitempty"` 83 | Display string `json:"display,omitempty"` 84 | MetadataJson string `json:"metadata,omitempty"` 85 | Description string `json:"description,omitempty"` 86 | NoMkDirs bool `json:"nomkdirs,omitempty"` 87 | Hidden bool `json:"hidden,omitempty"` 88 | AppConfigJson string `json:"appconfig"` // json-string 89 | } 90 | 91 | // Marshals (json.Marshal) an object to the FileInfo.Metadata field. 92 | func (opts *FileOpts) SetMetadata(obj interface{}) error { 93 | metaStr, err := dashutil.MarshalJson(obj) 94 | if err != nil { 95 | return err 96 | } 97 | if len(metaStr) > dashutil.MetadataJsonMax { 98 | return dasherr.ValidateErr(fmt.Errorf("Metadata too large")) 99 | } 100 | opts.MetadataJson = metaStr 101 | return nil 102 | } 103 | 104 | // Returns true if this FileOpts is a RuntimeLink or AppRuntimeLink (can have an attached Runtime). 105 | func (opts *FileOpts) IsLinkType() bool { 106 | return opts.FileType == FileTypeRuntimeLink || opts.FileType == FileTypeAppRuntimeLink 107 | } 108 | 109 | // Options to pass to DashFSClient.DirInfo() 110 | type DirOpts struct { 111 | RoleList []string `json:"rolelist"` 112 | ShowHidden bool `json:"showhidden"` 113 | Recursive bool `json:"recursive"` 114 | } 115 | 116 | // Options to pass to DashFSClient.WatchFile(). Controls how fsnotify watches the given file. 117 | type WatchOpts struct { 118 | ThrottleTime time.Duration 119 | ShutdownCh chan struct{} 120 | } 121 | 122 | type DashFSClient struct { 123 | rootPath string 124 | client *DashCloudClient 125 | } 126 | 127 | // Low-level function to set a Dashborg FS path. Not normally called by end users. This function 128 | // is called by SetJsonPath, LinkRuntime, LinkAppRuntime, SetPathFromFile, SetStaticPath, and WatchFile. 129 | func (fs *DashFSClient) SetRawPath(path string, r io.Reader, fileOpts *FileOpts, runtime LinkRuntime) error { 130 | if path == "" || path[0] != '/' { 131 | return dasherr.ValidateErr(fmt.Errorf("Path must begin with '/'")) 132 | } 133 | return fs.client.setRawPath(fs.rootPath+path, r, fileOpts, runtime) 134 | } 135 | 136 | // Sets static JSON data to the given path. FileOpts is optional (type will be set to "static", 137 | // and mimeType to "application/json"). 138 | func (fs *DashFSClient) SetJsonPath(path string, data interface{}, fileOpts *FileOpts) error { 139 | var jsonBuf bytes.Buffer 140 | enc := json.NewEncoder(&jsonBuf) 141 | enc.SetEscapeHTML(false) 142 | err := enc.Encode(data) 143 | if err != nil { 144 | return dasherr.JsonMarshalErr("JsonData", err) 145 | } 146 | reader := bytes.NewReader(jsonBuf.Bytes()) 147 | if fileOpts == nil { 148 | fileOpts = &FileOpts{} 149 | } 150 | err = UpdateFileOptsFromReadSeeker(reader, fileOpts) 151 | if err != nil { 152 | return err 153 | } 154 | if fileOpts.MimeType == "" { 155 | fileOpts.MimeType = MimeTypeJson 156 | } 157 | return fs.SetRawPath(path, reader, fileOpts, nil) 158 | } 159 | 160 | // Sets the data from the given fileName as static data to the given Dashborg FS path. 161 | // fileOpts is required, and must specify at least a mimeType for the file contents. 162 | func (fs *DashFSClient) SetPathFromFile(path string, fileName string, fileOpts *FileOpts) error { 163 | fd, err := os.Open(fileName) 164 | if err != nil { 165 | return err 166 | } 167 | err = UpdateFileOptsFromReadSeeker(fd, fileOpts) 168 | if err != nil { 169 | return err 170 | } 171 | return fs.SetRawPath(path, fd, fileOpts, nil) 172 | } 173 | 174 | // Will call Seek(0, 0) on the reader twice, once at the beginning and once at the end. 175 | // If an error is returned, the seek position is not specified. If no error is returned 176 | // the reader will be reset to the beginning. 177 | // A []byte can be wrapped in a bytes.Buffer to use this function (error will always be nil) 178 | func UpdateFileOptsFromReadSeeker(r io.ReadSeeker, fileOpts *FileOpts) error { 179 | if fileOpts == nil { 180 | return dasherr.ValidateErr(fmt.Errorf("Must pass non-nil FileOpts (set at least MimeType)")) 181 | } 182 | _, err := r.Seek(0, 0) 183 | if err != nil { 184 | return err 185 | } 186 | h := sha256.New() 187 | numCopyBytes, err := io.Copy(h, r) 188 | if err != nil { 189 | return err 190 | } 191 | hashVal := h.Sum(nil) 192 | hashValStr := base64.StdEncoding.EncodeToString(hashVal[:]) 193 | _, err = r.Seek(0, 0) 194 | if err != nil { 195 | return err 196 | } 197 | fileOpts.FileType = FileTypeStatic 198 | fileOpts.Sha256 = hashValStr 199 | fileOpts.Size = numCopyBytes 200 | return nil 201 | } 202 | 203 | func (fs *DashFSClient) runWatchedSetPath(path string, fileName string, fileOpts *FileOpts) { 204 | err := fs.SetPathFromFile(path, fileName, fileOpts) 205 | if err != nil { 206 | log.Printf("Error calling SetPathFromFile (watched file) path=%s file=%s err=%v\n", dashutil.SimplifyPath(path, nil), fileName, err) 207 | } else { 208 | log.Printf("Watcher called SetPathFromFile path=%s file=%s size=%d hash=%s\n", dashutil.SimplifyPath(path, nil), fileName, fileOpts.Size, fileOpts.Sha256) 209 | } 210 | } 211 | 212 | // First calls SetPathFromFile. If that that fails, an error is returned and the file will *not* be watched 213 | // (watching only starts if this function returns nil). The given file will be watched using fsnotify. 214 | // Every time fsnotify detects a file modification, the file will be be re-uploaded using SetPathFromFile. 215 | // watchOpts may be nil, which will use default settings (Throttle time of 1 second, no shutdown channel). 216 | // This is function is recommended for use in development environments. 217 | func (fs *DashFSClient) WatchFile(path string, fileName string, fileOpts *FileOpts, watchOpts *WatchOpts) error { 218 | watcher, err := fsnotify.NewWatcher() 219 | if err != nil { 220 | return err 221 | } 222 | if fileOpts == nil { 223 | fileOpts = &FileOpts{} 224 | } 225 | if watchOpts == nil { 226 | watchOpts = &WatchOpts{ThrottleTime: time.Second} 227 | } 228 | err = fs.SetPathFromFile(path, fileName, fileOpts) 229 | if err != nil { 230 | return err 231 | } 232 | err = watcher.Add(fileName) 233 | if err != nil { 234 | return err 235 | } 236 | go func() { 237 | var needsRun bool 238 | lastRun := time.Now() 239 | defer watcher.Close() 240 | var timer *time.Timer 241 | for { 242 | var timerCh <-chan time.Time 243 | if timer != nil { 244 | timerCh = timer.C 245 | } 246 | select { 247 | case event, ok := <-watcher.Events: 248 | if !ok { 249 | return 250 | } 251 | if event.Op == fsnotify.Write || event.Op == fsnotify.Create { 252 | dur := time.Since(lastRun) 253 | if dur < watchOpts.ThrottleTime { 254 | needsRun = true 255 | if timer == nil { 256 | timer = time.NewTimer(watchOpts.ThrottleTime - dur) 257 | } 258 | } else { 259 | needsRun = false 260 | fs.runWatchedSetPath(path, fileName, fileOpts) 261 | lastRun = time.Now() 262 | } 263 | } 264 | 265 | case err, ok := <-watcher.Errors: 266 | if !ok { 267 | return 268 | } 269 | log.Printf("DashFS Watch Error path=%s file=%s err=%v\n", dashutil.SimplifyPath(path, nil), fileName, err) 270 | return 271 | 272 | case <-timerCh: 273 | if needsRun { 274 | timer = nil 275 | needsRun = false 276 | fs.runWatchedSetPath(path, fileName, fileOpts) 277 | lastRun = time.Now() 278 | } 279 | 280 | case <-watchOpts.ShutdownCh: 281 | return 282 | } 283 | } 284 | }() 285 | return nil 286 | } 287 | 288 | // Removes (deletes) the specified path from Dashborg FS. 289 | func (fs *DashFSClient) RemovePath(path string) error { 290 | if path == "" || path[0] != '/' { 291 | return fmt.Errorf("Path must begin with '/'") 292 | } 293 | return fs.client.removePath(fs.rootPath + path) 294 | } 295 | 296 | // Gets the FileInfo associated with path. If the file is not found, will return nil, nil. 297 | func (fs *DashFSClient) FileInfo(path string) (*FileInfo, error) { 298 | if path == "" || path[0] != '/' { 299 | return nil, fmt.Errorf("Path must begin with '/'") 300 | } 301 | rtn, _, err := fs.client.fileInfo(fs.rootPath+path, nil, false) 302 | if err != nil { 303 | return nil, err 304 | } 305 | if len(rtn) == 0 { 306 | return nil, nil 307 | } 308 | return rtn[0], nil 309 | } 310 | 311 | // Gets the directory info assocaited with path. dirOpts may be nil (in which case defaults are used). 312 | // If the directory does not exist, []*FileInfo will have length of 0, and error will be nil. 313 | func (fs *DashFSClient) DirInfo(path string, dirOpts *DirOpts) ([]*FileInfo, error) { 314 | if dirOpts == nil { 315 | dirOpts = &DirOpts{} 316 | } 317 | if path == "" || path[0] != '/' { 318 | return nil, fmt.Errorf("Path must begin with '/'") 319 | } 320 | rtn, _, err := fs.client.fileInfo(fs.rootPath+path, dirOpts, false) 321 | return rtn, err 322 | } 323 | 324 | // Connects a LinkRuntime to the given path. 325 | func (fs *DashFSClient) LinkRuntime(path string, rt LinkRuntime, fileOpts *FileOpts) error { 326 | if hasErr, ok := rt.(HasErr); ok { 327 | err := hasErr.Err() 328 | if err != nil { 329 | return err 330 | } 331 | } 332 | if fileOpts == nil { 333 | fileOpts = &FileOpts{} 334 | } 335 | fileOpts.FileType = FileTypeRuntimeLink 336 | if path == "" || path[0] != '/' { 337 | return fmt.Errorf("Path must begin with '/'") 338 | } 339 | return fs.client.setRawPath(fs.rootPath+path, nil, fileOpts, rt) 340 | } 341 | 342 | // Connects an AppRuntime to the given path. Normally this function is not called directly. 343 | // When an app is connected to the Dashborg backend, its runtime is also linked. 344 | func (fs *DashFSClient) LinkAppRuntime(path string, apprt LinkRuntime, fileOpts *FileOpts) error { 345 | if hasErr, ok := apprt.(HasErr); ok { 346 | err := hasErr.Err() 347 | if err != nil { 348 | return err 349 | } 350 | } 351 | if fileOpts == nil { 352 | fileOpts = &FileOpts{} 353 | } 354 | fileOpts.FileType = FileTypeAppRuntimeLink 355 | if path == "" || path[0] != '/' { 356 | return fmt.Errorf("Path must begin with '/'") 357 | } 358 | return fs.client.setRawPath(fs.rootPath+path, nil, fileOpts, apprt) 359 | } 360 | 361 | // Sets static data to the given Dashborg FS path, with data from an io.ReadSeeker. 362 | // Will always seek to the beginning of the stream (to compute the SHA-256 and size using 363 | // UpdateFileOptsFromReadSeeker). 364 | func (fs *DashFSClient) SetStaticPath(path string, r io.ReadSeeker, fileOpts *FileOpts) error { 365 | if fileOpts == nil { 366 | fileOpts = &FileOpts{} 367 | } 368 | fileOpts.FileType = FileTypeStatic 369 | err := UpdateFileOptsFromReadSeeker(r, fileOpts) 370 | if err != nil { 371 | return err 372 | } 373 | return fs.SetRawPath(path, r, fileOpts, nil) 374 | } 375 | 376 | // Creates a /@fs/ URL link to the given path. If jwtOpts are specified, it will override 377 | // the defaults in the config. 378 | func (fs *DashFSClient) MakePathUrl(path string, jwtOpts *JWTOpts) (string, error) { 379 | if path == "" || !dashutil.IsFullPathValid(path) { 380 | return "", fmt.Errorf("Invalid Path") 381 | } 382 | if jwtOpts == nil { 383 | jwtOpts = fs.client.Config.GetJWTOpts() 384 | } 385 | pathLink := fs.client.getAccHost() + "/@fs" + fs.rootPath + path 386 | if jwtOpts.NoJWT { 387 | return pathLink, nil 388 | } 389 | err := jwtOpts.Validate() 390 | if err != nil { 391 | return "", err 392 | } 393 | jwtToken, err := fs.client.Config.MakeAccountJWT(jwtOpts) 394 | if err != nil { 395 | return "", err 396 | } 397 | return fmt.Sprintf("%s?jwt=%s", pathLink, jwtToken), nil 398 | } 399 | 400 | // Calls MakePathUrl, panics on error. 401 | func (fs *DashFSClient) MustMakePathUrl(path string, jwtOpts *JWTOpts) string { 402 | rtn, err := fs.MakePathUrl(path, jwtOpts) 403 | if err != nil { 404 | panic(err) 405 | } 406 | return rtn 407 | } 408 | 409 | // Connects a link runtime *without* creating or updating its FileInfo. 410 | // Note the difference between this function and LinkRuntime(). LinkRuntime() takes 411 | // FileOpts and will create/update the path. 412 | func (fs *DashFSClient) ConnectLinkRuntime(path string, runtime LinkRuntime) error { 413 | if !dashutil.IsFullPathValid(path) { 414 | return fmt.Errorf("Invalid Path") 415 | } 416 | if runtime == nil { 417 | return fmt.Errorf("LinkRuntime() error, runtime must not be nil") 418 | } 419 | err := fs.client.connectLinkRpc(path) 420 | if err != nil { 421 | return err 422 | } 423 | fs.client.connectLinkRuntime(path, runtime) 424 | return nil 425 | } 426 | -------------------------------------------------------------------------------- /pkg/dash/dashreflect.go: -------------------------------------------------------------------------------- 1 | package dash 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | 9 | "github.com/sawka/dashborg-go-sdk/pkg/dashutil" 10 | ) 11 | 12 | var errType = reflect.TypeOf((*error)(nil)).Elem() 13 | var interfaceType = reflect.TypeOf((*interface{})(nil)).Elem() 14 | var appReqType = reflect.TypeOf(&AppRequest{}) 15 | var reqType = reflect.TypeOf((*Request)(nil)).Elem() 16 | var contextType = reflect.TypeOf((*context.Context)(nil)).Elem() 17 | 18 | func checkOutput(mType reflect.Type, outputTypes ...reflect.Type) bool { 19 | if mType.NumOut() != len(outputTypes) { 20 | return false 21 | } 22 | for idx, t := range outputTypes { 23 | if !mType.Out(idx).AssignableTo(t) { 24 | return false 25 | } 26 | } 27 | return true 28 | } 29 | 30 | func ca_unmarshalNil(hType reflect.Type, args []reflect.Value, argNum int) { 31 | for argNum < hType.NumIn() { 32 | args[argNum] = reflect.Zero(hType.In(argNum)) 33 | argNum++ 34 | } 35 | } 36 | 37 | func ca_unmarshalSingle(hType reflect.Type, args []reflect.Value, argNum int, jsonStr string) error { 38 | if argNum >= hType.NumIn() { 39 | return nil 40 | } 41 | argV, err := unmarshalToType(jsonStr, hType.In(argNum)) 42 | if err != nil { 43 | return err 44 | } 45 | args[argNum] = argV 46 | argNum++ 47 | ca_unmarshalNil(hType, args, argNum) 48 | return nil 49 | } 50 | 51 | func ca_unmarshalMulti(hType reflect.Type, args []reflect.Value, argNum int, jsonStr string) error { 52 | if argNum >= hType.NumIn() { 53 | return nil 54 | } 55 | outVals := make([]interface{}, hType.NumIn()-argNum) 56 | dataArgsNum := hType.NumIn() - argNum 57 | for i := argNum; i < hType.NumIn(); i++ { 58 | outVals[i-argNum] = reflect.New(hType.In(i)).Interface() 59 | } 60 | err := json.Unmarshal([]byte(jsonStr), &outVals) 61 | if err != nil { 62 | return err 63 | } 64 | // outvals can be shorter than hType.NumIn() - argNum (if json is short) 65 | for i := 0; i < len(outVals) && i < dataArgsNum; i++ { 66 | if outVals[i] == nil { 67 | args[i+argNum] = reflect.Zero(hType.In(i + argNum)) 68 | } else { 69 | args[i+argNum] = reflect.ValueOf(outVals[i]).Elem() 70 | } 71 | } 72 | argNum += len(outVals) 73 | ca_unmarshalNil(hType, args, argNum) 74 | return nil 75 | } 76 | 77 | // params: 78 | // * Context 79 | // * *Request 80 | // * AppStateType (if specified in App) 81 | // * data-array args 82 | func makeCallArgs(hType reflect.Type, req Request, appReq bool, pureHandler bool, stateType reflect.Type) ([]reflect.Value, error) { 83 | rtn := make([]reflect.Value, hType.NumIn()) 84 | if hType.NumIn() == 0 { 85 | return rtn, nil 86 | } 87 | argNum := 0 88 | if hType.In(argNum) == contextType { 89 | rtn[argNum] = reflect.ValueOf(req.Context()) 90 | argNum++ 91 | } 92 | if argNum == hType.NumIn() { 93 | return rtn, nil 94 | } 95 | if hType.In(argNum) == appReqType { 96 | if !appReq { 97 | return nil, fmt.Errorf("LinkRuntime functions must use dash.Request, not *dash.AppRequest") 98 | } 99 | if pureHandler { 100 | return nil, fmt.Errorf("PureHandlers must use dash.Request, not *dash.AppRequest") 101 | } 102 | rtn[argNum] = reflect.ValueOf(req) 103 | argNum++ 104 | } else if hType.In(argNum) == reqType { 105 | rtn[argNum] = reflect.ValueOf(req) 106 | argNum++ 107 | } 108 | if argNum == hType.NumIn() { 109 | return rtn, nil 110 | } 111 | rawData := req.RawData() 112 | if stateType != nil && stateType == hType.In(argNum) { 113 | stateV, err := unmarshalToType(rawData.AppStateJson, stateType) 114 | if err != nil { 115 | return nil, fmt.Errorf("Cannot unmarshal appStateJson to type:%v err:%v", hType.In(1), err) 116 | } 117 | rtn[argNum] = stateV 118 | argNum++ 119 | } 120 | if argNum == hType.NumIn() { 121 | return rtn, nil 122 | } 123 | var dataInterface interface{} 124 | req.BindData(&dataInterface) 125 | if dataInterface == nil { 126 | ca_unmarshalNil(hType, rtn, argNum) 127 | } else if reflect.ValueOf(dataInterface).Kind() == reflect.Slice { 128 | err := ca_unmarshalMulti(hType, rtn, argNum, rawData.DataJson) 129 | if err != nil { 130 | return nil, err 131 | } 132 | } else { 133 | err := ca_unmarshalSingle(hType, rtn, argNum, rawData.DataJson) 134 | if err != nil { 135 | return nil, err 136 | } 137 | } 138 | return rtn, nil 139 | } 140 | 141 | func validateHandler(hType reflect.Type, appReq bool, pureHandler bool, stateType reflect.Type) error { 142 | if hType.Kind() != reflect.Func { 143 | return fmt.Errorf("handlerFn must be a func") 144 | } 145 | if hType.NumOut() != 0 && !checkOutput(hType, errType) && !checkOutput(hType, interfaceType) && !checkOutput(hType, interfaceType, errType) { 146 | return fmt.Errorf("Invalid handlerFn return, must return void, error, interface{}, or (interface{}, error)") 147 | } 148 | if hType.NumIn() == 0 { 149 | return nil 150 | } 151 | 152 | argNum := 0 153 | // check optional context 154 | if hType.In(argNum) == contextType { 155 | argNum++ 156 | } 157 | if hType.NumIn() <= argNum { 158 | return nil 159 | } 160 | // check optional first argument: *dash.AppRequest / dash.Request 161 | if hType.In(argNum) == appReqType { 162 | if !appReq { 163 | return fmt.Errorf("LinkRuntime functions must use dash.Request, not *dash.AppRequest") 164 | } 165 | if pureHandler { 166 | return fmt.Errorf("PureHandlers must use dash.Request, not *dash.AppRequest") 167 | } 168 | argNum++ 169 | } else if hType.In(argNum) == reqType { 170 | argNum++ 171 | } 172 | if hType.NumIn() <= argNum { 173 | return nil 174 | } 175 | 176 | // optional state type 177 | if stateType != nil && hType.In(argNum) == stateType { 178 | argNum++ 179 | } else if stateType != nil && stateType.Kind() == reflect.Ptr && hType.In(argNum) == stateType.Elem() { 180 | return fmt.Errorf("StateType is %v (pointer), but argument is not a pointer: %v", stateType, hType.In(argNum)) 181 | } else if stateType != nil && stateType.Kind() == reflect.Struct && hType.In(argNum) == reflect.PtrTo(stateType) { 182 | return fmt.Errorf("StateType is %v (struct), but argument a pointer: %v", stateType, hType.In(argNum)) 183 | } 184 | 185 | if hType.NumIn() <= argNum { 186 | return nil 187 | } 188 | 189 | // some basic static checking of the rest of the arguments 190 | for ; argNum < hType.NumIn(); argNum++ { 191 | inType := hType.In(argNum) 192 | if inType == appReqType { 193 | return fmt.Errorf("Invalid arg #%d, *dash.AppRequest must be first argument", argNum+1) 194 | } 195 | if inType == reqType { 196 | return fmt.Errorf("Invalid arg #%d, dash.Request must be first argument", argNum+1) 197 | } 198 | if stateType != nil && inType == stateType { 199 | return fmt.Errorf("Invalid arg #%d, state-type %v, must be first argument or second argument after dash.Request", argNum+1, stateType) 200 | } 201 | if inType.Kind() == reflect.Func || inType.Kind() == reflect.Chan || inType.Kind() == reflect.UnsafePointer { 202 | return fmt.Errorf("Invalid arg #%d, cannot marshal into func/chan/unsafe.Pointer", argNum+1) 203 | } 204 | if inType.Kind() == reflect.Map { 205 | if inType.Key().Kind() != reflect.String { 206 | return fmt.Errorf("Invalid arg #%d (%v) map arguments must have string keys", argNum, inType) 207 | } 208 | } 209 | } 210 | 211 | return nil 212 | } 213 | 214 | func unmarshalToType(jsonData string, rtnType reflect.Type) (reflect.Value, error) { 215 | if jsonData == "" { 216 | return reflect.Zero(rtnType), nil 217 | } 218 | if rtnType.Kind() == reflect.Ptr { 219 | rtnV := reflect.New(rtnType.Elem()) 220 | err := json.Unmarshal([]byte(jsonData), rtnV.Interface()) 221 | if err != nil { 222 | return reflect.Value{}, err 223 | } 224 | return rtnV, nil 225 | } else { 226 | rtnV := reflect.New(rtnType) 227 | err := json.Unmarshal([]byte(jsonData), rtnV.Interface()) 228 | if err != nil { 229 | return reflect.Value{}, err 230 | } 231 | return rtnV.Elem(), nil 232 | } 233 | return reflect.Zero(rtnType), nil 234 | } 235 | 236 | // Handler registers a handler using reflection. 237 | // Return value must be return void, interface{}, error, or (interface{}, error). 238 | // First optional argument to the function is a *dash.AppRequest. 239 | // Second optional argument is the AppStateType (if one has been set in the app runtime). 240 | // The rest of the arguments are mapped to the request Data as an array. If request Data is longer, 241 | // the arguments are ignored. If request Data is shorter, the missing arguments are set to their zero value. 242 | // If request Data is not an array, it will be converted to a single element array, if request Data is null 243 | // it will be converted to a zero-element array. The handler will throw an error if the Data or AppState 244 | // values cannot be converted to their respective go types (using json.Unmarshal). 245 | func (apprt *AppRuntimeImpl) Handler(name string, handlerFn interface{}, opts ...*HandlerOpts) { 246 | singleOpt := getSingleOpt(opts) 247 | err := handlerInternal(apprt, name, handlerFn, true, singleOpt) 248 | if err != nil { 249 | apprt.addError(fmt.Errorf("Error adding handler '%s': %w", name, err)) 250 | } 251 | } 252 | 253 | func (apprt *AppRuntimeImpl) PureHandler(name string, handlerFn interface{}, opts ...*HandlerOpts) { 254 | singleOpt := getSingleOpt(opts) 255 | singleOpt.PureHandler = true 256 | err := handlerInternal(apprt, name, handlerFn, true, singleOpt) 257 | if err != nil { 258 | apprt.addError(fmt.Errorf("Error adding handler '%s': %w", name, err)) 259 | } 260 | } 261 | 262 | func (linkrt *LinkRuntimeImpl) Handler(name string, handlerFn interface{}, opts ...*HandlerOpts) { 263 | singleOpt := getSingleOpt(opts) 264 | err := handlerInternal(linkrt, name, handlerFn, false, singleOpt) 265 | if err != nil { 266 | linkrt.addError(fmt.Errorf("Error adding handler '%s': %w", name, err)) 267 | } 268 | } 269 | 270 | func (linkrt *LinkRuntimeImpl) PureHandler(name string, handlerFn interface{}, opts ...*HandlerOpts) { 271 | singleOpt := getSingleOpt(opts) 272 | singleOpt.PureHandler = true 273 | err := handlerInternal(linkrt, name, handlerFn, false, singleOpt) 274 | if err != nil { 275 | linkrt.addError(fmt.Errorf("Error adding handler '%s': %w", name, err)) 276 | } 277 | } 278 | 279 | func getSingleOpt(opts []*HandlerOpts) HandlerOpts { 280 | if len(opts) == 0 || opts[0] == nil { 281 | return HandlerOpts{} 282 | } 283 | return *opts[0] 284 | } 285 | 286 | func handlerInternal(rti runtimeImplIf, name string, handlerFn interface{}, isAppRuntime bool, opts HandlerOpts) error { 287 | if !dashutil.IsPathFragValid(name) { 288 | return fmt.Errorf("Invalid handler name") 289 | } 290 | hfn, err := convertHandlerFn(rti, handlerFn, isAppRuntime, opts) 291 | if err != nil { 292 | return err 293 | } 294 | hinfo, err := makeHandlerInfo(rti, name, handlerFn, opts) 295 | if err != nil { 296 | return err 297 | } 298 | rti.setHandler(name, handlerType{HandlerFn: hfn, Opts: opts, HandlerInfo: hinfo}) 299 | return nil 300 | } 301 | 302 | func convertHandlerFn(rti runtimeImplIf, handlerFn interface{}, isAppRuntime bool, opts HandlerOpts) (handlerFuncType, error) { 303 | hType := reflect.TypeOf(handlerFn) 304 | err := validateHandler(hType, isAppRuntime, opts.PureHandler, rti.getStateType()) 305 | if err != nil { 306 | return nil, err 307 | } 308 | hVal := reflect.ValueOf(handlerFn) 309 | hfn := func(req *AppRequest) (interface{}, error) { 310 | args, err := makeCallArgs(hType, req, isAppRuntime, opts.PureHandler, rti.getStateType()) 311 | if err != nil { 312 | return nil, err 313 | } 314 | rtnVals := hVal.Call(args) 315 | return convertRtnVals(hType, rtnVals) 316 | } 317 | return hfn, nil 318 | } 319 | 320 | func isIntType(t reflect.Type) bool { 321 | switch t.Kind() { 322 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 323 | return true 324 | 325 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 326 | return true 327 | 328 | default: 329 | return false 330 | } 331 | } 332 | 333 | func makeTypeInfo(t reflect.Type) (*runtimeTypeInfo, error) { 334 | if isIntType(t) { 335 | return &runtimeTypeInfo{Type: "int", Strict: true}, nil 336 | } 337 | switch t.Kind() { 338 | case reflect.Float32, reflect.Float64: 339 | return &runtimeTypeInfo{Type: "float", Strict: true}, nil 340 | 341 | case reflect.String: 342 | return &runtimeTypeInfo{Type: "string", Strict: true}, nil 343 | 344 | case reflect.Bool: 345 | return &runtimeTypeInfo{Type: "bool", Strict: true}, nil 346 | 347 | case reflect.Interface: 348 | return &runtimeTypeInfo{Type: "any", Strict: false}, nil 349 | 350 | case reflect.Ptr: 351 | return makeTypeInfo(t.Elem()) 352 | 353 | case reflect.Array, reflect.Slice: 354 | elemType, err := makeTypeInfo(t.Elem()) 355 | if err != nil { 356 | return nil, err 357 | } 358 | return &runtimeTypeInfo{Type: "array", Strict: true, ElemType: elemType}, nil 359 | 360 | case reflect.Map: 361 | elemType, err := makeTypeInfo(t.Elem()) 362 | if err != nil { 363 | return nil, err 364 | } 365 | keyType := t.Key() 366 | if keyType.Kind() != reflect.String { 367 | return nil, fmt.Errorf("Invalid map type, key must be type string") 368 | } 369 | return &runtimeTypeInfo{Type: "map", Strict: true, ElemType: elemType}, nil 370 | 371 | case reflect.Struct: 372 | numField := t.NumField() 373 | var fieldTypes []*runtimeTypeInfo 374 | for i := 0; i < numField; i++ { 375 | field := t.Field(i) 376 | if field.PkgPath != "" { 377 | continue 378 | } 379 | fieldType, err := makeTypeInfo(field.Type) 380 | if err != nil { 381 | return nil, err 382 | } 383 | fieldTypes = append(fieldTypes, fieldType) 384 | } 385 | return &runtimeTypeInfo{Type: "struct", Strict: true, FieldTypes: fieldTypes}, nil 386 | 387 | case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.UnsafePointer, reflect.Func: 388 | return nil, fmt.Errorf("Invalid Type: %v", t) 389 | } 390 | return nil, fmt.Errorf("Invalid Type: %v", t) 391 | } 392 | 393 | func makeTypeInfoFromReturn(hType reflect.Type) (*runtimeTypeInfo, error) { 394 | if hType.NumOut() == 0 { 395 | return nil, nil 396 | } 397 | if hType.NumOut() == 1 { 398 | if hType.Out(0) == errType { 399 | return nil, nil 400 | } 401 | return makeTypeInfo(hType.Out(0)) 402 | } 403 | if hType.NumOut() == 2 { 404 | if hType.Out(1) != errType { 405 | return nil, fmt.Errorf("Invalid func return type, if a func returns 2 values, second value must be type 'error'") 406 | } 407 | return makeTypeInfo(hType.Out(0)) 408 | } 409 | return nil, fmt.Errorf("Invalid func return type (can only return a maximum of 2 values)") 410 | } 411 | 412 | func checkContextArg(hType reflect.Type, argNum *int) bool { 413 | if *argNum >= hType.NumIn() { 414 | return false 415 | } 416 | argType := hType.In(*argNum) 417 | if argType == contextType { 418 | (*argNum)++ 419 | return true 420 | } 421 | return false 422 | } 423 | 424 | func checkReqArg(hType reflect.Type, argNum *int) bool { 425 | if *argNum >= hType.NumIn() { 426 | return false 427 | } 428 | argType := hType.In(*argNum) 429 | if argType == reqType || argType == appReqType { 430 | (*argNum)++ 431 | return true 432 | } 433 | return false 434 | } 435 | 436 | func checkAppStateArg(hType reflect.Type, argNum *int, appStateType reflect.Type) bool { 437 | if *argNum >= hType.NumIn() { 438 | return false 439 | } 440 | argType := hType.In(*argNum) 441 | if argType == appStateType { 442 | (*argNum)++ 443 | return true 444 | } 445 | return false 446 | } 447 | 448 | func makeHandlerInfo(rti runtimeImplIf, name string, handlerFn interface{}, opts HandlerOpts) (*runtimeHandlerInfo, error) { 449 | rtn := &runtimeHandlerInfo{ 450 | Name: name, 451 | Pure: opts.PureHandler, 452 | Hidden: opts.Hidden, 453 | Display: opts.Display, 454 | FormDisplay: opts.FormDisplay, 455 | ResultsDisplay: opts.ResultsDisplay, 456 | } 457 | var err error 458 | hType := reflect.TypeOf(handlerFn) 459 | rtn.RtnType, err = makeTypeInfoFromReturn(hType) 460 | if err != nil { 461 | return nil, err 462 | } 463 | argNum := 0 464 | rtn.ContextParam = checkContextArg(hType, &argNum) 465 | rtn.ReqParam = checkReqArg(hType, &argNum) 466 | rtn.AppStateParam = checkAppStateArg(hType, &argNum, rti.getStateType()) 467 | rtn.ParamsType = []runtimeTypeInfo{} 468 | for ; argNum < hType.NumIn(); argNum++ { 469 | typeInfo, err := makeTypeInfo(hType.In(argNum)) 470 | if err != nil { 471 | return nil, err 472 | } 473 | rtn.ParamsType = append(rtn.ParamsType, *typeInfo) 474 | } 475 | return rtn, nil 476 | } 477 | 478 | func convertRtnVals(hType reflect.Type, rtnVals []reflect.Value) (interface{}, error) { 479 | if len(rtnVals) == 0 { 480 | return nil, nil 481 | } else if len(rtnVals) == 1 { 482 | if checkOutput(hType, errType) { 483 | if rtnVals[0].IsNil() { 484 | return nil, nil 485 | } 486 | return nil, rtnVals[0].Interface().(error) 487 | } 488 | return rtnVals[0].Interface(), nil 489 | } else { 490 | // (interface{}, error) 491 | var errRtn error 492 | if !rtnVals[1].IsNil() { 493 | errRtn = rtnVals[1].Interface().(error) 494 | } 495 | return rtnVals[0].Interface(), errRtn 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /pkg/dash/dbclient.go: -------------------------------------------------------------------------------- 1 | package dash 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "os" 14 | "reflect" 15 | "runtime/debug" 16 | "strconv" 17 | "strings" 18 | "sync" 19 | "sync/atomic" 20 | "time" 21 | 22 | "github.com/google/uuid" 23 | "github.com/sawka/dashborg-go-sdk/pkg/dasherr" 24 | "github.com/sawka/dashborg-go-sdk/pkg/dashproto" 25 | "github.com/sawka/dashborg-go-sdk/pkg/dashutil" 26 | "google.golang.org/grpc" 27 | "google.golang.org/grpc/backoff" 28 | "google.golang.org/grpc/connectivity" 29 | "google.golang.org/grpc/credentials" 30 | "google.golang.org/grpc/keepalive" 31 | "google.golang.org/grpc/metadata" 32 | ) 33 | 34 | // must be divisible by 3 (for base64 encoding) 35 | const blobReadSize = 3 * 340 * 1024 36 | 37 | const maxRRABlobSize = 3 * 1024 * 1024 // 3M 38 | 39 | const grpcServerPath = "/grpc-server" 40 | const mbConst = 1000000 41 | const uploadTimeout = 5 * time.Minute 42 | 43 | const stdGrpcTimeout = 10 * time.Second 44 | const streamGrpcTimeout = 0 45 | 46 | const maxBlobBytes = 5000000 47 | 48 | const ( 49 | mdConnIdKey = "dashborg-connid" 50 | mdClientVersionKey = "dashborg-clientversion" 51 | ) 52 | 53 | var NotConnectedErr = dasherr.ErrWithCodeStr(dasherr.ErrCodeNotConnected, "DashborgCloudClient is not Connected") 54 | 55 | type accInfoType struct { 56 | AccType string `json:"acctype"` 57 | AccName string `json:"accname"` 58 | AccCName string `json:"acccname"` 59 | AccJWTEnabled bool `json:"accjwtenabled"` 60 | NewAccount bool `json:"newaccount"` 61 | BlobSizeLimitMB float64 `json:"blobsizelimitmb"` 62 | HtmlSizeLimitMB float64 `json:"htmlsizelimitmb"` 63 | } 64 | 65 | type DashCloudClient struct { 66 | Lock *sync.Mutex 67 | StartTime time.Time 68 | ProcRunId string 69 | Config *Config 70 | Conn *grpc.ClientConn 71 | DBService dashproto.DashborgServiceClient 72 | ConnId *atomic.Value 73 | LinkRtMap map[string]LinkRuntime 74 | DoneCh chan bool 75 | PermErr bool 76 | ExitErr error 77 | AccInfo accInfoType 78 | } 79 | 80 | func makeCloudClient(config *Config) *DashCloudClient { 81 | rtn := &DashCloudClient{ 82 | Lock: &sync.Mutex{}, 83 | StartTime: time.Now(), 84 | ProcRunId: uuid.New().String(), 85 | Config: config, 86 | ConnId: &atomic.Value{}, 87 | LinkRtMap: make(map[string]LinkRuntime), 88 | DoneCh: make(chan bool), 89 | } 90 | rtn.ConnId.Store("") 91 | return rtn 92 | } 93 | 94 | type grpcConfig struct { 95 | GrpcServer string `json:"grpcserver"` 96 | GrpcPort int `json:"grpcport"` 97 | } 98 | 99 | type grpcServerRtn struct { 100 | Success bool `json:"success"` 101 | Error string `json:"error"` 102 | Data grpcConfig `json:"data"` 103 | } 104 | 105 | func (pc *DashCloudClient) getGrpcServer() (*grpcConfig, error) { 106 | urlVal := fmt.Sprintf("https://%s%s?accid=%s", pc.Config.ConsoleHost, grpcServerPath, pc.Config.AccId) 107 | resp, err := http.Get(urlVal) 108 | if err != nil { 109 | return nil, fmt.Errorf("Cannot get gRPC Server Host: %w", err) 110 | } 111 | defer resp.Body.Close() 112 | bodyContent, err := ioutil.ReadAll(resp.Body) 113 | var grpcRtn grpcServerRtn 114 | err = json.Unmarshal(bodyContent, &grpcRtn) 115 | if err != nil { 116 | return nil, fmt.Errorf("Cannot get gRPC Server Host (decoding response): %w", err) 117 | } 118 | if !grpcRtn.Success { 119 | return nil, fmt.Errorf("Cannot get gRPC Server Host (error response): %s", grpcRtn.Error) 120 | } 121 | if grpcRtn.Data.GrpcServer == "" || grpcRtn.Data.GrpcPort == 0 { 122 | return nil, fmt.Errorf("Cannot get gRPC Server Host (bad response)") 123 | } 124 | return &grpcRtn.Data, nil 125 | } 126 | 127 | func (pc *DashCloudClient) startClient() error { 128 | if pc.Config.GrpcHost == "" { 129 | grpcConfig, err := pc.getGrpcServer() 130 | if err != nil { 131 | pc.logV("DashborgCloudClient error starting: %v\n", err) 132 | return err 133 | } 134 | pc.Config.GrpcHost = grpcConfig.GrpcServer 135 | pc.Config.GrpcPort = grpcConfig.GrpcPort 136 | if pc.Config.Verbose { 137 | pc.log("Dashborg Using gRPC host %s:%d\n", pc.Config.GrpcHost, pc.Config.GrpcPort) 138 | } 139 | } 140 | err := pc.connectGrpc() 141 | if err != nil { 142 | pc.logV("DashborgCloudClient ERROR connecting gRPC client: %v\n", err) 143 | } 144 | if pc.Config.Verbose { 145 | pc.log("Dashborg Initialized CloudClient AccId:%s Zone:%s ProcName:%s ProcRunId:%s\n", pc.Config.AccId, pc.Config.ZoneName, pc.Config.ProcName, pc.ProcRunId) 146 | } 147 | if pc.Config.ShutdownCh != nil { 148 | go func() { 149 | <-pc.Config.ShutdownCh 150 | pc.externalShutdown() 151 | }() 152 | } 153 | err = pc.sendConnectClientMessage(false) 154 | if err != nil && !dasherr.CanRetry(err) { 155 | pc.setExitError(err) 156 | return err 157 | } 158 | go pc.runRequestStreamLoop() 159 | return nil 160 | } 161 | 162 | func (pc *DashCloudClient) ctxWithMd(timeout time.Duration) (context.Context, context.CancelFunc) { 163 | var ctx context.Context 164 | var cancelFn context.CancelFunc 165 | if timeout == 0 { 166 | ctx = context.Background() 167 | cancelFn = func() {} 168 | } else { 169 | ctx, cancelFn = context.WithTimeout(context.Background(), timeout) 170 | } 171 | connId := pc.ConnId.Load().(string) 172 | ctx = metadata.AppendToOutgoingContext(ctx, mdConnIdKey, connId, mdClientVersionKey, ClientVersion) 173 | return ctx, cancelFn 174 | } 175 | 176 | func (pc *DashCloudClient) externalShutdown() { 177 | if pc.Conn == nil { 178 | pc.logV("DashborgCloudClient ERROR shutting down, gRPC connection is not initialized\n") 179 | return 180 | } 181 | pc.setExitError(fmt.Errorf("ShutdownCh channel closed")) 182 | err := pc.Conn.Close() 183 | if err != nil { 184 | pc.logV("DashborgCloudClient ERROR closing gRPC connection: %v\n", err) 185 | } 186 | } 187 | 188 | func makeHostData() map[string]string { 189 | hostname, err := os.Hostname() 190 | if err != nil { 191 | hostname = "unknown" 192 | } 193 | hostData := map[string]string{ 194 | "HostName": hostname, 195 | "Pid": strconv.Itoa(os.Getpid()), 196 | } 197 | return hostData 198 | } 199 | 200 | func (pc *DashCloudClient) sendConnectClientMessage(isReconnect bool) error { 201 | // only allow one proc message at a time (synchronize) 202 | hostData := makeHostData() 203 | m := &dashproto.ConnectClientMessage{ 204 | Ts: dashutil.Ts(), 205 | ProcRunId: pc.ProcRunId, 206 | AccId: pc.Config.AccId, 207 | ZoneName: pc.Config.ZoneName, 208 | AnonAcc: pc.Config.AnonAcc, 209 | ProcName: pc.Config.ProcName, 210 | ProcIKey: pc.Config.ProcIKey, 211 | ProcTags: pc.Config.ProcTags, 212 | HostData: hostData, 213 | StartTs: dashutil.DashTime(pc.StartTime), 214 | } 215 | ctx, cancelFn := pc.ctxWithMd(stdGrpcTimeout) 216 | defer cancelFn() 217 | resp, respErr := pc.DBService.ConnectClient(ctx, m) 218 | dashErr := pc.handleStatusErrors("ConnectClient", resp, respErr, true) 219 | var accInfo accInfoType 220 | if dashErr == nil { 221 | jsonErr := json.Unmarshal([]byte(resp.AccInfoJson), &accInfo) 222 | if jsonErr != nil { 223 | dashErr = dasherr.JsonUnmarshalErr("AccInfo", jsonErr) 224 | } 225 | if accInfo.AccType == "" { 226 | dashErr = dasherr.JsonUnmarshalErr("AccInfo", fmt.Errorf("No AccType in AccInfo")) 227 | } 228 | } 229 | if dashErr != nil { 230 | pc.ConnId.Store("") 231 | if !dasherr.CanRetry(dashErr) { 232 | pc.Lock.Lock() 233 | pc.PermErr = true 234 | pc.Lock.Unlock() 235 | } 236 | return dashErr 237 | } 238 | pc.ConnId.Store(resp.ConnId) 239 | pc.Lock.Lock() 240 | pc.AccInfo = accInfo 241 | pc.Lock.Unlock() 242 | if !isReconnect { 243 | if accInfo.NewAccount { 244 | pc.printNewAccMessage() 245 | } else if accInfo.AccType == "anon" { 246 | pc.printAnonAccMessage() 247 | } 248 | if pc.Config.Verbose { 249 | pc.log("DashborgCloudClient Connected, AccId:%s Zone:%s ConnId:%s AccType:%v\n", pc.Config.AccId, pc.Config.ZoneName, resp.ConnId, accInfo.AccType) 250 | } else { 251 | pc.log("DashborgCloudClient Connected, AccId:%s Zone:%s\n", pc.Config.AccId, pc.Config.ZoneName) 252 | } 253 | } else { 254 | if pc.Config.Verbose { 255 | pc.log("DashborgCloudClient ReConnected, AccId:%s Zone:%s ConnId:%s\n", pc.Config.AccId, pc.Config.ZoneName, resp.ConnId) 256 | } 257 | pc.reconnectLinks() 258 | } 259 | return nil 260 | } 261 | 262 | func (pc *DashCloudClient) getLinkPaths() []string { 263 | pc.Lock.Lock() 264 | defer pc.Lock.Unlock() 265 | var paths []string 266 | for path, _ := range pc.LinkRtMap { 267 | paths = append(paths, path) 268 | } 269 | return paths 270 | } 271 | 272 | func (pc *DashCloudClient) connectLinkRpc(path string) error { 273 | if !pc.IsConnected() { 274 | return NotConnectedErr 275 | } 276 | err := dashutil.ValidateFullPath(path, false) 277 | if err != nil { 278 | return dasherr.ValidateErr(err) 279 | } 280 | m := &dashproto.ConnectLinkMessage{ 281 | Ts: dashutil.Ts(), 282 | Path: path, 283 | } 284 | ctx, cancelFn := pc.ctxWithMd(stdGrpcTimeout) 285 | defer cancelFn() 286 | resp, respErr := pc.DBService.ConnectLink(ctx, m) 287 | dashErr := pc.handleStatusErrors(fmt.Sprintf("ConnectLink(%s)", path), resp, respErr, false) 288 | if dashErr != nil { 289 | return dashErr 290 | } 291 | return nil 292 | } 293 | 294 | func (pc *DashCloudClient) reconnectLinks() { 295 | linkPaths := pc.getLinkPaths() 296 | for _, linkPath := range linkPaths { 297 | err := pc.connectLinkRpc(linkPath) 298 | if err != nil { 299 | pc.log("DashborgCloudClient %v\n", err) 300 | } else { 301 | pc.logV("DashborgCloudClient ReConnected link:%s\n", dashutil.SimplifyPath(linkPath, nil)) 302 | } 303 | } 304 | } 305 | 306 | func (pc *DashCloudClient) printNewAccMessage() { 307 | pc.log("Welcome to Dashborg! Your new account has been provisioned. AccId:%s\n", pc.Config.AccId) 308 | pc.log("You are currently using a free version of the Dashborg Service.\n") 309 | pc.log("Your use of this service is subject to the Dashborg Terms of Service - https://www.dashborg.net/static/tos.html\n") 310 | } 311 | 312 | func (pc *DashCloudClient) printAnonAccMessage() { 313 | pc.log("You are currently using a free version of the Dashborg Service.\n") 314 | pc.log("Your use of this service is subject to the Dashborg Terms of Service - https://www.dashborg.net/static/tos.html\n") 315 | } 316 | 317 | func (pc *DashCloudClient) handleStatusErrors(fnName string, resp interface{}, respErr error, forceLog bool) error { 318 | var rtnErr error 319 | if respErr != nil { 320 | rtnErr = dasherr.RpcErr(fnName, respErr) 321 | } else { 322 | respV := reflect.ValueOf(resp).Elem() 323 | rtnStatus := respV.FieldByName("Status").Interface().(*dashproto.RtnStatus) 324 | rtnErr = dasherr.FromRtnStatus(fnName, rtnStatus) 325 | } 326 | if rtnErr == nil { 327 | return nil 328 | } 329 | if forceLog || pc.Config.Verbose { 330 | pc.log("DashborgCloudClient %v\n", rtnErr) 331 | } 332 | pc.explainLimit(pc.AccInfo.AccType, rtnErr.Error()) 333 | return rtnErr 334 | } 335 | 336 | func (pc *DashCloudClient) connectGrpc() error { 337 | addr := pc.Config.GrpcHost + ":" + strconv.Itoa(pc.Config.GrpcPort) 338 | backoffConfig := backoff.Config{ 339 | BaseDelay: 1.0 * time.Second, 340 | Multiplier: 1.6, 341 | Jitter: 0.2, 342 | MaxDelay: 60 * time.Second, 343 | } 344 | connectParams := grpc.ConnectParams{MinConnectTimeout: time.Second, Backoff: backoffConfig} 345 | keepaliveParams := keepalive.ClientParameters{Time: 10 * time.Second, Timeout: 5 * time.Second, PermitWithoutStream: true} 346 | clientCert, err := tls.LoadX509KeyPair(pc.Config.CertFileName, pc.Config.KeyFileName) 347 | if err != nil { 348 | return fmt.Errorf("Cannot load keypair key:%s cert:%s err:%w", pc.Config.KeyFileName, pc.Config.CertFileName, err) 349 | } 350 | tlsConfig := &tls.Config{ 351 | MinVersion: tls.VersionTLS13, 352 | CurvePreferences: []tls.CurveID{tls.CurveP384}, 353 | PreferServerCipherSuites: true, 354 | Certificates: []tls.Certificate{clientCert}, 355 | CipherSuites: []uint16{ 356 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 357 | tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 358 | }, 359 | } 360 | tlsCreds := credentials.NewTLS(tlsConfig) 361 | conn, err := grpc.Dial( 362 | addr, 363 | grpc.WithConnectParams(connectParams), 364 | grpc.WithKeepaliveParams(keepaliveParams), 365 | grpc.WithTransportCredentials(tlsCreds), 366 | ) 367 | pc.Conn = conn 368 | pc.DBService = dashproto.NewDashborgServiceClient(conn) 369 | return err 370 | } 371 | 372 | func (pc *DashCloudClient) unlinkRuntime(path string) { 373 | pc.Lock.Lock() 374 | defer pc.Lock.Unlock() 375 | delete(pc.LinkRtMap, path) 376 | } 377 | 378 | func (pc *DashCloudClient) connectLinkRuntime(path string, rt LinkRuntime) { 379 | pc.Lock.Lock() 380 | defer pc.Lock.Unlock() 381 | pc.LinkRtMap[path] = rt 382 | } 383 | 384 | func (pc *DashCloudClient) removeAppPath(appName string) error { 385 | if !pc.IsConnected() { 386 | return NotConnectedErr 387 | } 388 | if !dashutil.IsAppNameValid(appName) { 389 | return dasherr.ValidateErr(fmt.Errorf("Invalid app name")) 390 | } 391 | appPath := AppPathFromName(appName) 392 | m := &dashproto.RemovePathMessage{ 393 | Ts: dashutil.Ts(), 394 | Path: appPath, 395 | RemoveFullApp: true, 396 | } 397 | ctx, cancelFn := pc.ctxWithMd(stdGrpcTimeout) 398 | defer cancelFn() 399 | resp, respErr := pc.DBService.RemovePath(ctx, m) 400 | dashErr := pc.handleStatusErrors(fmt.Sprintf("RemoveApp(%s)", appName), resp, respErr, true) 401 | if dashErr != nil { 402 | return dashErr 403 | } 404 | pc.log("DashborgCloudClient removed app %s\n", appName) 405 | return nil 406 | } 407 | 408 | func (pc *DashCloudClient) removePath(path string) error { 409 | if !pc.IsConnected() { 410 | return NotConnectedErr 411 | } 412 | err := dashutil.ValidateFullPath(path, false) 413 | if err != nil { 414 | return dasherr.ValidateErr(err) 415 | } 416 | m := &dashproto.RemovePathMessage{ 417 | Ts: dashutil.Ts(), 418 | Path: path, 419 | } 420 | ctx, cancelFn := pc.ctxWithMd(stdGrpcTimeout) 421 | defer cancelFn() 422 | resp, respErr := pc.DBService.RemovePath(ctx, m) 423 | dashErr := pc.handleStatusErrors(fmt.Sprintf("RemovePath(%s)", path), resp, respErr, true) 424 | if dashErr != nil { 425 | return dashErr 426 | } 427 | pc.log("DashborgCloudClient removed path %s\n", path) 428 | return nil 429 | } 430 | 431 | func (pc *DashCloudClient) fileInfo(path string, dirOpts *DirOpts, rtnContents bool) ([]*FileInfo, []byte, error) { 432 | if !pc.IsConnected() { 433 | return nil, nil, NotConnectedErr 434 | } 435 | err := dashutil.ValidateFullPath(path, false) 436 | if err != nil { 437 | return nil, nil, dasherr.ValidateErr(err) 438 | } 439 | m := &dashproto.FileInfoMessage{ 440 | Ts: dashutil.Ts(), 441 | Path: path, 442 | RtnContents: rtnContents, 443 | } 444 | if dirOpts != nil { 445 | jsonStr, err := dashutil.MarshalJson(dirOpts) 446 | if err != nil { 447 | return nil, nil, dasherr.JsonMarshalErr("DirOpts", err) 448 | } 449 | m.DirOptsJson = jsonStr 450 | } 451 | ctx, cancelFn := pc.ctxWithMd(stdGrpcTimeout) 452 | defer cancelFn() 453 | resp, respErr := pc.DBService.FileInfo(ctx, m) 454 | dashErr := pc.handleStatusErrors(fmt.Sprintf("FileInfo(%s)", path), resp, respErr, false) 455 | if dashErr != nil { 456 | return nil, nil, dashErr 457 | } 458 | if resp.FileInfoJson == "" { 459 | return nil, nil, nil 460 | } 461 | var rtn []*FileInfo 462 | err = json.Unmarshal([]byte(resp.FileInfoJson), &rtn) 463 | if err != nil { 464 | return nil, nil, dasherr.JsonUnmarshalErr("FileInfoJson", err) 465 | } 466 | return rtn, resp.FileContent, nil 467 | } 468 | 469 | func (pc *DashCloudClient) runRequestStreamLoop() { 470 | defer close(pc.DoneCh) 471 | 472 | w := &expoWait{CloudClient: pc} 473 | for { 474 | state := pc.Conn.GetState() 475 | if state == connectivity.Shutdown { 476 | pc.log("DashborgCloudClient RunRequestStreamLoop exiting - Conn Shutdown\n") 477 | pc.setExitError(fmt.Errorf("gRPC Connection Shutdown")) 478 | break 479 | } 480 | if state == connectivity.Connecting || state == connectivity.TransientFailure { 481 | time.Sleep(1 * time.Second) 482 | w.Reset() 483 | continue 484 | } 485 | okWait := w.Wait() 486 | if !okWait { 487 | continue 488 | } 489 | if pc.ConnId.Load().(string) == "" { 490 | err := pc.sendConnectClientMessage(true) 491 | if err != nil && !dasherr.CanRetry(err) { 492 | pc.log("DashborgCloudClient RunRequestStreamLoop exiting - Permanent Error: %v\n", err) 493 | pc.setExitError(err) 494 | break 495 | } 496 | if err != nil { 497 | continue 498 | } 499 | } 500 | ranOk, errCode := pc.runRequestStream() 501 | if ranOk { 502 | w.Reset() 503 | } 504 | if errCode == dasherr.ErrCodeBadConnId { 505 | pc.ConnId.Store("") 506 | continue 507 | } 508 | w.ForceWait = true 509 | } 510 | } 511 | 512 | func (pc *DashCloudClient) sendErrResponse(reqMsg *dashproto.RequestMessage, errMsg string) { 513 | m := &dashproto.SendResponseMessage{ 514 | Ts: dashutil.Ts(), 515 | ReqId: reqMsg.ReqId, 516 | RequestType: reqMsg.RequestType, 517 | Path: reqMsg.Path, 518 | FeClientId: reqMsg.FeClientId, 519 | ResponseDone: true, 520 | Err: &dashproto.ErrorType{Err: errMsg}, 521 | } 522 | ctx, cancelFn := pc.ctxWithMd(stdGrpcTimeout) 523 | defer cancelFn() 524 | resp, respErr := pc.DBService.SendResponse(ctx, m) 525 | dashErr := pc.handleStatusErrors("SendResponse", resp, respErr, false) 526 | if dashErr != nil { 527 | pc.logV("Error sending Error Response: %v\n", dashErr) 528 | } 529 | } 530 | 531 | func (pc *DashCloudClient) runRequestStream() (bool, dasherr.ErrCode) { 532 | m := &dashproto.RequestStreamMessage{Ts: dashutil.Ts()} 533 | pc.logV("Dashborg gRPC RequestStream starting\n") 534 | ctx, cancelFn := pc.ctxWithMd(streamGrpcTimeout) 535 | defer cancelFn() 536 | reqStreamClient, err := pc.DBService.RequestStream(ctx, m) 537 | if err != nil { 538 | pc.log("Dashborg Error setting up gRPC RequestStream: %v\n", err) 539 | return false, dasherr.ErrCodeRpc 540 | } 541 | startTime := time.Now() 542 | var reqCounter int64 543 | var endingErrCode dasherr.ErrCode 544 | for { 545 | reqMsg, err := reqStreamClient.Recv() 546 | if err == io.EOF { 547 | pc.logV("Dashborg gRPC RequestStream done: EOF\n") 548 | endingErrCode = dasherr.ErrCodeEof 549 | break 550 | } 551 | if err != nil { 552 | pc.logV("Dashborg %v\n", dasherr.RpcErr("RequestStream", err)) 553 | endingErrCode = dasherr.ErrCodeRpc 554 | break 555 | } 556 | if reqMsg.Status != nil { 557 | dashErr := dasherr.FromRtnStatus("RequestStream", reqMsg.Status) 558 | if dashErr != nil { 559 | pc.logV("Dashborg %v\n", dashErr) 560 | endingErrCode = dasherr.GetErrCode(dashErr) 561 | break 562 | } 563 | } 564 | pc.logV("Dashborg gRPC request %s\n", requestMsgStr(reqMsg)) 565 | go func() { 566 | atomic.AddInt64(&reqCounter, 1) 567 | timeoutMs := reqMsg.TimeoutMs 568 | if timeoutMs == 0 || timeoutMs > 60000 { 569 | timeoutMs = 60000 570 | } 571 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond) 572 | defer cancel() 573 | if reqMsg.Path == "" { 574 | pc.sendErrResponse(reqMsg, "Bad Request - No Path") 575 | return 576 | } 577 | if reqMsg.RequestType == "path" { 578 | fullPath, err := dashutil.PathNoFrag(reqMsg.Path) 579 | if err != nil { 580 | pc.sendErrResponse(reqMsg, fmt.Sprintf("Error parsing path: %v", err)) 581 | } 582 | pc.Lock.Lock() 583 | runtimeVal := pc.LinkRtMap[fullPath] 584 | pc.Lock.Unlock() 585 | if runtimeVal == nil { 586 | pc.sendErrResponse(reqMsg, "No Linked Runtime") 587 | return 588 | } 589 | pc.dispatchRtRequest(ctx, runtimeVal, reqMsg) 590 | return 591 | } else { 592 | pc.sendErrResponse(reqMsg, fmt.Sprintf("Invalid RequestType '%s'", reqMsg.RequestType)) 593 | return 594 | } 595 | }() 596 | } 597 | elapsed := time.Since(startTime) 598 | return (elapsed >= 5*time.Second), endingErrCode 599 | } 600 | 601 | func (pc *DashCloudClient) sendPathResponse(preq *AppRequest, rtnVal interface{}, appReq bool) { 602 | if preq.IsDone() { 603 | return 604 | } 605 | m := &dashproto.SendResponseMessage{ 606 | Ts: dashutil.Ts(), 607 | ReqId: preq.RequestInfo().ReqId, 608 | RequestType: preq.RequestInfo().RequestType, 609 | Path: preq.RequestInfo().Path, 610 | FeClientId: preq.RequestInfo().FeClientId, 611 | ResponseDone: true, 612 | } 613 | defer pc.sendResponseProtoRpc(m) 614 | rtnErr := preq.GetError() 615 | if rtnErr != nil { 616 | m.Err = dasherr.AsProtoErr(rtnErr) 617 | return 618 | } 619 | var rtnValRRA []*dashproto.RRAction 620 | if rtnVal != nil { 621 | var err error 622 | rtnValRRA, err = rtnValToRRA(rtnVal) 623 | if err != nil { 624 | m.Err = dasherr.AsProtoErr(err) 625 | return 626 | } 627 | } 628 | if appReq { 629 | m.Actions = preq.getRRA() 630 | } 631 | m.Actions = append(m.Actions, rtnValRRA...) 632 | return 633 | } 634 | 635 | func (pc *DashCloudClient) dispatchRtRequest(ctx context.Context, linkrt LinkRuntime, reqMsg *dashproto.RequestMessage) { 636 | var rtnVal interface{} 637 | preq := makeAppRequest(ctx, reqMsg, pc) 638 | defer func() { 639 | if panicErr := recover(); panicErr != nil { 640 | log.Printf("Dashborg PANIC in Handler %s | %v\n", requestMsgStr(reqMsg), panicErr) 641 | preq.SetError(fmt.Errorf("PANIC in handler %v", panicErr)) 642 | debug.PrintStack() 643 | } 644 | pc.sendPathResponse(preq, rtnVal, reqMsg.AppRequest) 645 | }() 646 | dataResult, err := linkrt.RunHandler(preq) 647 | if err != nil { 648 | preq.SetError(err) 649 | return 650 | } 651 | rtnVal = dataResult 652 | return 653 | } 654 | 655 | // returns the reason for shutdown (GetExitError()) 656 | func (pc *DashCloudClient) WaitForShutdown() error { 657 | <-pc.DoneCh 658 | return pc.GetExitError() 659 | } 660 | 661 | func (pc *DashCloudClient) setExitError(err error) { 662 | pc.Lock.Lock() 663 | defer pc.Lock.Unlock() 664 | if pc.ExitErr == nil { 665 | pc.ExitErr = err 666 | } 667 | } 668 | 669 | // Returns nil if client is still running. Returns error (reason for shutdown) if client has stopped. 670 | func (pc *DashCloudClient) GetExitError() error { 671 | pc.Lock.Lock() 672 | defer pc.Lock.Unlock() 673 | return pc.ExitErr 674 | } 675 | 676 | func (pc *DashCloudClient) IsConnected() bool { 677 | if pc == nil || pc.Config == nil { 678 | return false 679 | } 680 | pc.Lock.Lock() 681 | defer pc.Lock.Unlock() 682 | 683 | if pc.ExitErr != nil { 684 | return false 685 | } 686 | if pc.Conn == nil { 687 | return false 688 | } 689 | connId := pc.ConnId.Load().(string) 690 | if connId == "" { 691 | return false 692 | } 693 | return true 694 | } 695 | 696 | func (pc *DashCloudClient) getAccHost() string { 697 | if !pc.IsConnected() { 698 | panic("DashCloudClient is not connected") 699 | } 700 | pc.Lock.Lock() 701 | defer pc.Lock.Unlock() 702 | 703 | if pc.AccInfo.AccCName != "" { 704 | if pc.Config.Env != "prod" { 705 | return fmt.Sprintf("https://%s:8080", pc.AccInfo.AccCName) 706 | } 707 | return fmt.Sprintf("https://%s", pc.AccInfo.AccCName) 708 | } 709 | accId := pc.Config.AccId 710 | return fmt.Sprintf("https://acc-%s.%s", accId, pc.Config.ConsoleHost) 711 | } 712 | 713 | // SendResponseProtoRpc is for internal use by the Dashborg AppClient, not to be called by the end user. 714 | func (pc *DashCloudClient) sendResponseProtoRpc(m *dashproto.SendResponseMessage) (int, error) { 715 | if !pc.IsConnected() { 716 | return 0, NotConnectedErr 717 | } 718 | ctx, cancelFn := pc.ctxWithMd(stdGrpcTimeout) 719 | defer cancelFn() 720 | resp, respErr := pc.DBService.SendResponse(ctx, m) 721 | dashErr := pc.handleStatusErrors("SendResponse", resp, respErr, false) 722 | if dashErr != nil { 723 | pc.logV("Error sending response: %v\n", dashErr) 724 | return 0, dashErr 725 | } 726 | return int(resp.NumStreamClients), nil 727 | } 728 | 729 | func (pc *DashCloudClient) logV(fmtStr string, args ...interface{}) { 730 | if pc.Config.Verbose { 731 | pc.log(fmtStr, args...) 732 | } 733 | } 734 | 735 | func (pc *DashCloudClient) log(fmtStr string, args ...interface{}) { 736 | if pc.Config.Logger != nil { 737 | pc.Config.Logger.Printf(fmtStr, args...) 738 | } else { 739 | log.Printf(fmtStr, args...) 740 | } 741 | } 742 | 743 | func (opts *FileOpts) Validate() error { 744 | if opts == nil { 745 | return dasherr.ValidateErr(fmt.Errorf("FileOpts is nil")) 746 | } 747 | if !dashutil.IsFileTypeValid(opts.FileType) { 748 | return dasherr.ValidateErr(fmt.Errorf("Invalid FileType")) 749 | } 750 | if !dashutil.IsRoleListValid(strings.Join(opts.AllowedRoles, ",")) { 751 | return dasherr.ValidateErr(fmt.Errorf("Invalid AllowedRoles")) 752 | } 753 | if opts.Display != "" && !dashutil.IsFileDisplayValid(opts.Display) { 754 | return dasherr.ValidateErr(fmt.Errorf("Invalid Display")) 755 | } 756 | if !dashutil.IsDescriptionValid(opts.Description) { 757 | return dasherr.ValidateErr(fmt.Errorf("Invalid Description (too long)")) 758 | } 759 | if opts.FileType == FileTypeStatic { 760 | if !dashutil.IsSha256Base64HashValid(opts.Sha256) { 761 | return dasherr.ValidateErr(fmt.Errorf("Invalid SHA-256 hash value, must be a base64 encoded SHA-256 hash (44 characters), see dashutil.Sha256Base64()")) 762 | } 763 | if !dashutil.IsMimeTypeValid(opts.MimeType) { 764 | return dasherr.ValidateErr(fmt.Errorf("Invalid MimeType")) 765 | } 766 | if opts.Size <= 0 { 767 | return dasherr.ValidateErr(fmt.Errorf("Invalid Size (cannot be 0)")) 768 | } 769 | } 770 | if opts.FileType == FileTypeApp && opts.AppConfigJson == "" { 771 | return dasherr.ValidateErr(fmt.Errorf("FileType 'app' must have AppConfigJson set")) 772 | } 773 | if opts.FileType != FileTypeApp && opts.AppConfigJson != "" { 774 | return dasherr.ValidateErr(fmt.Errorf("FileType '%s' must not have AppConfigJson set", opts.FileType)) 775 | } 776 | if opts.AppConfigJson != "" { 777 | if len(opts.AppConfigJson) > dashutil.AppConfigJsonMax { 778 | return dasherr.ValidateErr(fmt.Errorf("AppConfig too large")) 779 | } 780 | var acfg AppConfig 781 | err := json.Unmarshal([]byte(opts.AppConfigJson), &acfg) 782 | if err != nil { 783 | return dasherr.JsonUnmarshalErr("AppConfig", err) 784 | } 785 | } 786 | if opts.MetadataJson != "" { 787 | if len(opts.MetadataJson) > dashutil.MetadataJsonMax { 788 | return dasherr.ValidateErr(fmt.Errorf("Metadata too large")) 789 | } 790 | var testJson interface{} 791 | err := json.Unmarshal([]byte(opts.MetadataJson), &testJson) 792 | if err != nil { 793 | return dasherr.JsonUnmarshalErr("Metadata", err) 794 | } 795 | } 796 | return nil 797 | } 798 | 799 | func (pc *DashCloudClient) setRawPath(fullPath string, r io.Reader, fileOpts *FileOpts, linkRt LinkRuntime) error { 800 | err := pc.setRawPathWrap(fullPath, r, fileOpts, linkRt) 801 | if err != nil { 802 | pc.logV("Dashborg SetPath ERROR %s => %s | %v\n", dashutil.SimplifyPath(fullPath, nil), shortFileOptsStr(fileOpts), err) 803 | return err 804 | } 805 | pc.logV("Dashborg SetPath %s => %s\n", dashutil.SimplifyPath(fullPath, nil), shortFileOptsStr(fileOpts)) 806 | return nil 807 | } 808 | 809 | func (pc *DashCloudClient) setRawPathWrap(fullPath string, r io.Reader, fileOpts *FileOpts, linkRt LinkRuntime) error { 810 | if !pc.IsConnected() { 811 | return NotConnectedErr 812 | } 813 | if fileOpts == nil { 814 | return dasherr.ValidateErr(fmt.Errorf("SetRawPath cannot receive nil *FileOpts")) 815 | } 816 | if !dashutil.IsFullPathValid(fullPath) { 817 | return dasherr.ValidateErr(fmt.Errorf("Invalid Path '%s'", fullPath)) 818 | } 819 | err := dashutil.ValidateFullPath(fullPath, false) 820 | if err != nil { 821 | return dasherr.ValidateErr(err) 822 | } 823 | if len(fileOpts.AllowedRoles) == 0 { 824 | fileOpts.AllowedRoles = []string{RoleUser} 825 | } 826 | err = fileOpts.Validate() 827 | if err != nil { 828 | return err 829 | } 830 | if fileOpts.FileType != FileTypeStatic && r != nil { 831 | return dasherr.ValidateErr(fmt.Errorf("SetRawPath no io.Reader allowed except for file-type:static")) 832 | } 833 | if !fileOpts.IsLinkType() && linkRt != nil { 834 | return dasherr.ValidateErr(fmt.Errorf("FileType is %s, no dash.LinkRuntime allowed", fileOpts.FileType)) 835 | } 836 | optsJson, err := dashutil.MarshalJson(fileOpts) 837 | if err != nil { 838 | return dasherr.JsonMarshalErr("FileOpts", err) 839 | } 840 | m := &dashproto.SetPathMessage{ 841 | Ts: dashutil.Ts(), 842 | Path: fullPath, 843 | HasBody: (r != nil), 844 | ConnectRuntime: (linkRt != nil), 845 | FileOptsJson: optsJson, 846 | } 847 | ctx, cancelFn := pc.ctxWithMd(stdGrpcTimeout) 848 | defer cancelFn() 849 | resp, respErr := pc.DBService.SetPath(ctx, m) 850 | dashErr := pc.handleStatusErrors("SetPath", resp, respErr, false) 851 | if dashErr != nil { 852 | return dashErr 853 | } 854 | if resp.BlobFound || !m.HasBody { 855 | if fileOpts.IsLinkType() && linkRt != nil { 856 | pc.connectLinkRuntime(fullPath, linkRt) 857 | } 858 | return nil 859 | } 860 | if resp.BlobUploadId == "" || resp.BlobUploadKey == "" { 861 | return dasherr.NoRetryErrWithCode(dasherr.ErrCodeProtocol, fmt.Errorf("Invalid Server Response, no UploadId/UploadKey specified")) 862 | } 863 | if r == nil { 864 | return dasherr.ValidateErr(fmt.Errorf("Nil Reader passed to SetPath")) 865 | } 866 | uploadCtx, uploadCancelFn := context.WithTimeout(context.Background(), uploadTimeout) 867 | defer uploadCancelFn() 868 | err = pc.UploadFile(uploadCtx, r, pc.Config.AccId, resp.BlobUploadId, resp.BlobUploadKey) 869 | if err != nil { 870 | return err 871 | } 872 | return nil 873 | } 874 | 875 | func (pc *DashCloudClient) GlobalFSClient() *DashFSClient { 876 | return &DashFSClient{client: pc} 877 | } 878 | 879 | func (pc *DashCloudClient) FSClientAtRoot(rootPath string) (*DashFSClient, error) { 880 | if rootPath != "" { 881 | _, _, _, err := dashutil.ParseFullPath(rootPath, false) 882 | if err != nil { 883 | return nil, err 884 | } 885 | // remove trailing slash 886 | if rootPath[len(rootPath)-1] == '/' { 887 | rootPath = rootPath[0 : len(rootPath)-1] 888 | } 889 | } 890 | return &DashFSClient{client: pc, rootPath: rootPath}, nil 891 | } 892 | 893 | func (pc *DashCloudClient) AppClient() *DashAppClient { 894 | return &DashAppClient{pc} 895 | } 896 | 897 | func requestMsgStr(reqMsg *dashproto.RequestMessage) string { 898 | if reqMsg.Path == "" { 899 | return fmt.Sprintf("[no-path]") 900 | } 901 | return fmt.Sprintf("%4s %s", reqMsg.RequestMethod, dashutil.SimplifyPath(reqMsg.Path, nil)) 902 | } 903 | 904 | func rtnValToRRA(rtnVal interface{}) ([]*dashproto.RRAction, error) { 905 | if blobRtn, ok := rtnVal.(BlobReturn); ok { 906 | return blobToRRA(blobRtn.MimeType, blobRtn.Reader) 907 | } 908 | if blobRtn, ok := rtnVal.(*BlobReturn); ok { 909 | return blobToRRA(blobRtn.MimeType, blobRtn.Reader) 910 | } 911 | jsonData, err := dashutil.MarshalJson(rtnVal) 912 | if err != nil { 913 | return nil, dasherr.JsonMarshalErr("HandlerReturnValue", err) 914 | } 915 | rrAction := &dashproto.RRAction{ 916 | Ts: dashutil.Ts(), 917 | ActionType: "setdata", 918 | Selector: RtnSetDataPath, 919 | JsonData: jsonData, 920 | } 921 | return []*dashproto.RRAction{rrAction}, nil 922 | } 923 | 924 | // convert to streaming 925 | func blobToRRA(mimeType string, reader io.Reader) ([]*dashproto.RRAction, error) { 926 | if !dashutil.IsMimeTypeValid(mimeType) { 927 | return nil, dasherr.ValidateErr(fmt.Errorf("Invalid Mime-Type passed to SetBlobData mime-type=%s", mimeType)) 928 | } 929 | first := true 930 | totalSize := 0 931 | var rra []*dashproto.RRAction 932 | for { 933 | buffer := make([]byte, blobReadSize) 934 | n, err := io.ReadFull(reader, buffer) 935 | if err == io.EOF { 936 | break 937 | } 938 | totalSize += n 939 | if (err == nil || err == io.ErrUnexpectedEOF) && n > 0 { 940 | // write 941 | rrAction := &dashproto.RRAction{ 942 | Ts: dashutil.Ts(), 943 | Selector: RtnSetDataPath, 944 | BlobBytes: buffer[0:n], 945 | } 946 | if first { 947 | rrAction.ActionType = "blob" 948 | rrAction.BlobMimeType = mimeType 949 | first = false 950 | } else { 951 | rrAction.ActionType = "blobext" 952 | } 953 | rra = append(rra, rrAction) 954 | } 955 | if err == io.ErrUnexpectedEOF { 956 | break 957 | } 958 | if err != nil { 959 | return nil, err 960 | } 961 | } 962 | if totalSize > maxRRABlobSize { 963 | return nil, dasherr.ValidateErr(fmt.Errorf("BLOB too large, max-size:%d, blob-size:%d", maxRRABlobSize, totalSize)) 964 | } 965 | return rra, nil 966 | } 967 | 968 | func shortFileOptsStr(fileOpts *FileOpts) string { 969 | if fileOpts == nil { 970 | return "[null]" 971 | } 972 | mimeType := "" 973 | if fileOpts.MimeType != "" { 974 | mimeType = ":" + fileOpts.MimeType 975 | } 976 | return fmt.Sprintf("%s%s", fileOpts.FileType, mimeType) 977 | } 978 | 979 | type uploadRespType struct { 980 | Success bool `json:"success"` 981 | Error string `json:"error"` 982 | ErrCode string `json:"errcode"` 983 | PermErr bool `json:"permerr"` 984 | } 985 | 986 | func (c *Config) getRawUploadUrl() string { 987 | return fmt.Sprintf("https://%s/api2/raw-upload", c.ConsoleHost) 988 | } 989 | 990 | func (pc *DashCloudClient) UploadFile(ctx context.Context, r io.Reader, accId string, uploadId string, uploadKey string) error { 991 | if !dashutil.IsUUIDValid(accId) { 992 | return dasherr.ValidateErr(fmt.Errorf("Invalid AccId")) 993 | } 994 | if !dashutil.IsUUIDValid(uploadId) || uploadKey == "" { 995 | return dasherr.ValidateErr(fmt.Errorf("Invalid UploadId/UploadKey")) 996 | } 997 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, pc.Config.getRawUploadUrl(), r) 998 | if err != nil { 999 | return err 1000 | } 1001 | req.Header.Set("Content-Type", "application/octet-stream") 1002 | req.Header.Set("X-Dashborg-AccId", accId) 1003 | req.Header.Set("X-Dashborg-UploadId", uploadId) 1004 | req.Header.Set("X-Dashborg-UploadKey", uploadKey) 1005 | resp, err := http.DefaultClient.Do(req) 1006 | if err != nil { 1007 | return dasherr.ErrWithCode(dasherr.ErrCodeUpload, err) 1008 | } 1009 | defer resp.Body.Close() 1010 | bodyContent, err := ioutil.ReadAll(resp.Body) 1011 | var uploadResp uploadRespType 1012 | err = json.Unmarshal(bodyContent, &uploadResp) 1013 | if err != nil { 1014 | return dasherr.JsonUnmarshalErr("UploadResponse", err) 1015 | } 1016 | if !uploadResp.Success { 1017 | errMsg := uploadResp.Error 1018 | if errMsg == "" { 1019 | return errors.New("Unknown Error") 1020 | } 1021 | if uploadResp.ErrCode != "" { 1022 | if uploadResp.PermErr { 1023 | return dasherr.NoRetryErrWithCode(dasherr.ErrCode(uploadResp.ErrCode), errors.New(errMsg)) 1024 | } else { 1025 | return dasherr.ErrWithCode(dasherr.ErrCode(uploadResp.ErrCode), errors.New(errMsg)) 1026 | } 1027 | } 1028 | return errors.New(errMsg) 1029 | } 1030 | return nil 1031 | } 1032 | -------------------------------------------------------------------------------- /pkg/dash/expowait.go: -------------------------------------------------------------------------------- 1 | package dash 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type expoWait struct { 8 | ForceWait bool 9 | InitialWait time.Time 10 | CurWaitDeadline time.Time 11 | LastOkMs int64 12 | WaitTimes int 13 | CloudClient *DashCloudClient 14 | } 15 | 16 | func (w *expoWait) Wait() bool { 17 | hasInitialWait := !w.InitialWait.IsZero() 18 | if w.InitialWait.IsZero() { 19 | w.InitialWait = time.Now() 20 | } 21 | if w.ForceWait || hasInitialWait { 22 | time.Sleep(1 * time.Second) 23 | w.WaitTimes++ 24 | w.ForceWait = false 25 | } 26 | msWait := int64(time.Since(w.InitialWait)) / int64(time.Millisecond) 27 | if !hasInitialWait { 28 | w.LastOkMs = msWait 29 | return true 30 | } 31 | diffWait := msWait - w.LastOkMs 32 | var rtnOk bool 33 | switch { 34 | case msWait < 4000: 35 | w.LastOkMs = msWait 36 | rtnOk = true 37 | 38 | case msWait < 60000 && diffWait > 4800: 39 | w.LastOkMs = msWait 40 | rtnOk = true 41 | 42 | case diffWait > 29500: 43 | w.LastOkMs = msWait 44 | rtnOk = true 45 | } 46 | if rtnOk { 47 | w.CloudClient.logV("DashborgCloudClient RunRequestStreamLoop trying to connect %0.1fs\n", float64(msWait)/1000) 48 | } 49 | return rtnOk 50 | } 51 | 52 | func (w *expoWait) Reset() { 53 | *w = expoWait{CloudClient: w.CloudClient} 54 | } 55 | -------------------------------------------------------------------------------- /pkg/dash/limits.go: -------------------------------------------------------------------------------- 1 | package dash 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var limitErrorRE = regexp.MustCompile("DashborgLimitError limit:([a-zA-Z0-9]+)(?:\\.([a-zA-Z0-9]+))?") 9 | 10 | type limitKey struct { 11 | AccType string 12 | LimitName string 13 | SubLimitName string 14 | } 15 | 16 | var limitExplanations map[limitKey]string 17 | 18 | func init() { 19 | limitExplanations = make(map[limitKey]string) 20 | 21 | limitExplanations[limitKey{AccTypeAnon, "MaxApps", ""}] = "Anonymous Dashborg accounts only support one app. Register your account for free to enable up to 5 apps." 22 | limitExplanations[limitKey{AccTypeAnon, "MaxAppsPerZone", ""}] = "Anonymous Dashborg accounts only support one app. Register your account for free to enable up to 5 apps." 23 | limitExplanations[limitKey{AccTypeAnon, "MaxZones", ""}] = "Anonymous Dashborg accounts only support a single zone. Register and upgrade to a PRO account to enable multiple zones" 24 | limitExplanations[limitKey{AccTypeAnon, "AppBlobs", ""}] = "Anonymous Dashborg accounts have very limited BLOB storage limits (to prevent abuse). Register your account for free to enable larger BLOB sizes and storage limits" 25 | limitExplanations[limitKey{AccTypeAnon, "AllBlobs", ""}] = "Anonymous Dashborg accounts have very limited BLOB storage limits (to prevent abuse). Register your account for free to enable larger BLOB sizes and storage limits" 26 | limitExplanations[limitKey{AccTypeAnon, "HtmlSizeMB", ""}] = "Anonymous Dashborg accounts have a limit on HTML size. Register your account for free enable a larger HTML size" 27 | limitExplanations[limitKey{AccTypeAnon, "BackendTransferMB", ""}] = "Anonymous Dashborg accounts have a very limited data transfer allowance (to prevent abuse). Register your account for free to enable much higher transfer limits" 28 | 29 | limitExplanations[limitKey{AccTypeFree, "MaxApps", ""}] = "Free Dashborg accounts only support up to 5 apps. Upgrade to a PRO account to enable up to 20 apps per zone, a staging zone, increased storage limits, and more user accounts." 30 | limitExplanations[limitKey{AccTypeFree, "MaxAppsPerZone", ""}] = "Free Dashborg accounts only support up to 5 apps. Upgrade to a PRO account to enable up to 20 apps per zone, a staging zone, increased storage limits, and more user accounts." 31 | limitExplanations[limitKey{AccTypeFree, "MaxZones", ""}] = "Free Dashborg accounts only support a single zone. Register and upgrade to a PRO account to enable multiple zones, more apps, increased storage limits, and more user accounts." 32 | limitExplanations[limitKey{AccTypeFree, "AppBlobs", ""}] = "Free Dashborg accounts have limited BLOB storage limits. Upgrade to a PRO account for much larger BLOB storage limits (up to 10G)" 33 | limitExplanations[limitKey{AccTypeFree, "HtmlSizeMB", ""}] = "Free Dashborg accounts have a limit on HTML size. Upgrade to a PRO account for a larger HTML limit" 34 | limitExplanations[limitKey{AccTypeAnon, "BackendTransferMB", ""}] = "Free Dashborg accounts have a limited data transfer allowance. Upgrade to a PRO account to increase your transfer limit" 35 | 36 | } 37 | 38 | func (pc *DashCloudClient) explainLimit(accType string, errMsg string) { 39 | if accType != AccTypeAnon && accType != AccTypeFree { 40 | return 41 | } 42 | if strings.Index(errMsg, "DashborgLimitError") == -1 { 43 | return 44 | } 45 | match := limitErrorRE.FindStringSubmatch(errMsg) 46 | if match == nil { 47 | return 48 | } 49 | limitName := match[1] 50 | explanation := limitExplanations[limitKey{accType, limitName, ""}] 51 | if explanation != "" { 52 | pc.log("%s\n", explanation) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/dash/request.go: -------------------------------------------------------------------------------- 1 | package dash 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "sync" 11 | "time" 12 | 13 | "github.com/sawka/dashborg-go-sdk/pkg/dasherr" 14 | "github.com/sawka/dashborg-go-sdk/pkg/dashproto" 15 | "github.com/sawka/dashborg-go-sdk/pkg/dashutil" 16 | ) 17 | 18 | const htmlPagePath = "$state.dashborg.htmlpage" 19 | const pageNameKey = "apppage" 20 | 21 | type RequestInfo struct { 22 | StartTime time.Time 23 | ReqId string // unique request id 24 | RequestType string // "data", "handler", or "stream" 25 | RequestMethod string // GET or POST 26 | Path string // request path 27 | AppName string // app name 28 | FeClientId string // unique id for client 29 | } 30 | 31 | type RawRequestData struct { 32 | DataJson string 33 | AppStateJson string 34 | AuthDataJson string 35 | } 36 | 37 | // For LinkRuntime requests and PureRequets, those functions get a Request interface 38 | // not an *AppRequest. Pure requests cannot call SetData, NavTo, etc. or any method 39 | // that would cause side effects for the application UI outside of the return value. 40 | type Request interface { 41 | Context() context.Context 42 | AuthData() *AuthAtom 43 | RequestInfo() RequestInfo 44 | RawData() RawRequestData 45 | BindData(obj interface{}) error 46 | BindAppState(obj interface{}) error 47 | } 48 | 49 | type dashborgState struct { 50 | UrlParams map[string]interface{} `json:"urlparams"` 51 | PostParams map[string]interface{} `json:"postparams"` 52 | Dashborg map[string]interface{} `json:"dashborg"` 53 | } 54 | 55 | // The full app request object. All of the information about the request is 56 | // encapsulated in this struct. Note that "pure" requests and link runtime requests 57 | // cannot access all of the functionality of the AppRequest (sepecifically the 58 | // parts that cause side effects in the UI). The limited API for those requests 59 | // is encapsulated in the Request interface. 60 | type AppRequest struct { 61 | lock *sync.Mutex // synchronizes RRActions 62 | ctx context.Context // gRPC context / streaming context 63 | info RequestInfo 64 | rawData RawRequestData 65 | client *DashCloudClient 66 | appState interface{} // json-unmarshaled app state for this request 67 | authData *AuthAtom // authentication tokens associated with this request 68 | err error // set if an error occured (when set, RRActions are not sent) 69 | rrActions []*dashproto.RRAction // output, these are the actions that will be returned 70 | isDone bool // set after Done() is called and response has been sent to server 71 | infoMsgs []string // debugging information 72 | } 73 | 74 | func (req *AppRequest) canSetHtml() bool { 75 | return req.info.RequestType == requestTypeHandler || req.info.RequestType == requestTypeHtml 76 | } 77 | 78 | // Returns RequestInfo which contains basic information about this request (StartTime, Path, AppName, FeClientId, etc.) 79 | func (req *AppRequest) RequestInfo() RequestInfo { 80 | return req.info 81 | } 82 | 83 | // Returns a context that controls this request. This context comes from the initiating gRPC request. When the 84 | // gRPC request times out, this context will expire. 85 | func (req *AppRequest) Context() context.Context { 86 | return req.ctx 87 | } 88 | 89 | // Returns the authentication (AuthAtom) attached to this request. 90 | func (req *AppRequest) AuthData() *AuthAtom { 91 | return req.authData 92 | } 93 | 94 | // Binds a Go struct to the data passed in this request. Used for special cases or when 95 | // the func reflection binding is not sufficient. Used just like json.Unmarshal(). 96 | func (req *AppRequest) BindData(obj interface{}) error { 97 | if req.rawData.DataJson == "" { 98 | return nil 99 | } 100 | err := json.Unmarshal([]byte(req.rawData.DataJson), obj) 101 | return err 102 | } 103 | 104 | // Binds a Go struct to the application state passed in this request. Used for special 105 | // cases when the Runtime's AppState is not sufficient. 106 | func (req *AppRequest) BindAppState(obj interface{}) error { 107 | if req.rawData.AppStateJson == "" { 108 | return nil 109 | } 110 | err := json.Unmarshal([]byte(req.rawData.AppStateJson), obj) 111 | return err 112 | } 113 | 114 | func (req *AppRequest) appendRR(rrAction *dashproto.RRAction) { 115 | req.lock.Lock() 116 | defer req.lock.Unlock() 117 | req.rrActions = append(req.rrActions, rrAction) 118 | } 119 | 120 | func (req *AppRequest) clearActions() []*dashproto.RRAction { 121 | req.lock.Lock() 122 | defer req.lock.Unlock() 123 | rtn := req.rrActions 124 | req.rrActions = nil 125 | return rtn 126 | } 127 | 128 | // SetBlobData sets blob data at a particular FE path. Often calling SetBlob can be 129 | // easier than creating a separate handler that returns BlobData -- e.g. getting 130 | // a data-table and a graph image. 131 | func (req *AppRequest) SetBlob(path string, mimeType string, reader io.Reader) error { 132 | if req.isDone { 133 | return fmt.Errorf("Cannot call SetBlob(), path=%s, Request is already done", path) 134 | } 135 | actions, err := blobToRRA(mimeType, reader) 136 | if err != nil { 137 | return err 138 | } 139 | for _, rrAction := range actions { 140 | req.appendRR(rrAction) 141 | } 142 | return nil 143 | } 144 | 145 | // Calls SetBlobData with the the contents of fileName. Do not confuse path with fileName. 146 | // path is the location in the FE data model to set the data. fileName is the local fileName 147 | // to read blob data from. 148 | func (req *AppRequest) SetBlobFromFile(path string, mimeType string, fileName string) error { 149 | fd, err := os.Open(fileName) 150 | if err != nil { 151 | return err 152 | } 153 | defer fd.Close() 154 | return req.SetBlob(path, mimeType, fd) 155 | } 156 | 157 | func (req *AppRequest) reqInfoStr() string { 158 | return fmt.Sprintf("%s://%s", req.info.RequestType, req.info.Path) 159 | } 160 | 161 | // AddDataOp is a more generic form of SetData. It allows for more advanced setting of data in 162 | // the frontend data model -- like "append" or "setunless". 163 | func (req *AppRequest) AddDataOp(op string, path string, data interface{}) error { 164 | if req.isDone { 165 | return fmt.Errorf("Cannot call SetData(), reqinfo=%s data-path=%s, Request is already done", req.reqInfoStr()) 166 | } 167 | jsonData, err := dashutil.MarshalJson(data) 168 | if err != nil { 169 | return fmt.Errorf("Error marshaling json for SetData, path:%s, err:%v\n", path, err) 170 | } 171 | rrAction := &dashproto.RRAction{ 172 | Ts: dashutil.Ts(), 173 | ActionType: "setdata", 174 | JsonData: jsonData, 175 | } 176 | if op == "" || op == "set" { 177 | rrAction.Selector = path 178 | } else { 179 | rrAction.Selector = op + ":" + path 180 | } 181 | req.appendRR(rrAction) 182 | return nil 183 | } 184 | 185 | // SetData is used to return data to the client. Will replace the contents of path with data. 186 | // Calls AddDataOp with the op "set". 187 | func (req *AppRequest) SetData(path string, data interface{}) error { 188 | return req.AddDataOp("set", path, data) 189 | } 190 | 191 | // SetHtml returns html to be rendered by the client. Only valid for root handler requests (path = "/") 192 | func (req *AppRequest) setHtml(html string) error { 193 | if req.isDone { 194 | return fmt.Errorf("Cannot call SetHtml(), Request is already done") 195 | } 196 | if !req.canSetHtml() { 197 | return fmt.Errorf("Cannot call SetHtml() for request-type=%s", req.info.RequestType) 198 | } 199 | ts := dashutil.Ts() 200 | htmlAction := &dashproto.RRAction{ 201 | Ts: ts, 202 | ActionType: "html", 203 | Html: html, 204 | } 205 | req.appendRR(htmlAction) 206 | return nil 207 | } 208 | 209 | // Convience wrapper over SetHtml that returns the contents of a file. 210 | func (req *AppRequest) setHtmlFromFile(fileName string) error { 211 | fd, err := os.Open(fileName) 212 | if err != nil { 213 | return err 214 | } 215 | defer fd.Close() 216 | htmlBytes, err := ioutil.ReadAll(fd) 217 | if err != nil { 218 | return err 219 | } 220 | return req.setHtml(string(htmlBytes)) 221 | } 222 | 223 | // Call from a handler to force the client to invalidate and re-pull data that matches path. 224 | // Path is a regular expression. If pathRegexp is set to empty string, it will invalidate 225 | // all frontend data (equivalent to ".*"). 226 | func (req *AppRequest) InvalidateData(pathRegexp string) error { 227 | if req.isDone { 228 | return fmt.Errorf("Cannot call InvalidateData(), path=%s, Request is already done", pathRegexp) 229 | } 230 | if pathRegexp == "" { 231 | pathRegexp = ".*" 232 | } 233 | rrAction := &dashproto.RRAction{ 234 | Ts: dashutil.Ts(), 235 | ActionType: "invalidate", 236 | Selector: pathRegexp, 237 | } 238 | req.appendRR(rrAction) 239 | return nil 240 | } 241 | 242 | func (req *AppRequest) setAuthData(aa *AuthAtom) { 243 | if aa == nil { 244 | return 245 | } 246 | if aa.Ts == 0 { 247 | aa.Ts = dashutil.Ts() + int64(MaxAuthExp/time.Millisecond) 248 | } 249 | if aa.Type == "" { 250 | panic(fmt.Sprintf("Dashborg Invalid AuthAtom, no Type specified")) 251 | } 252 | jsonAa, _ := json.Marshal(aa) 253 | rr := &dashproto.RRAction{ 254 | Ts: dashutil.Ts(), 255 | ActionType: "panelauth", 256 | JsonData: string(jsonAa), 257 | } 258 | req.appendRR(rr) 259 | } 260 | 261 | func (req *AppRequest) isStream() bool { 262 | return req.info.RequestType == requestTypeStream 263 | } 264 | 265 | // Returns the raw JSON request data (auth, app state, and parameter data). Used when 266 | // you require special/custom JSON handling that the other API functions cannot handle. 267 | func (req *AppRequest) RawData() RawRequestData { 268 | return req.rawData 269 | } 270 | 271 | // Returns true once the response has already been sent back to the Dashborg service. 272 | // Most methods will return errors (or have no effect) once the request is done. 273 | func (req *AppRequest) IsDone() bool { 274 | return req.isDone 275 | } 276 | 277 | func makeAppRequest(ctx context.Context, reqMsg *dashproto.RequestMessage, client *DashCloudClient) *AppRequest { 278 | preq := &AppRequest{ 279 | info: RequestInfo{ 280 | StartTime: time.Now(), 281 | ReqId: reqMsg.ReqId, 282 | RequestType: reqMsg.RequestType, 283 | RequestMethod: reqMsg.RequestMethod, 284 | Path: reqMsg.Path, 285 | FeClientId: reqMsg.FeClientId, 286 | }, 287 | rawData: RawRequestData{ 288 | DataJson: reqMsg.JsonData, 289 | AuthDataJson: reqMsg.AuthData, 290 | AppStateJson: reqMsg.AppStateData, 291 | }, 292 | ctx: ctx, 293 | lock: &sync.Mutex{}, 294 | client: client, 295 | } 296 | preq.info.AppName = dashutil.AppNameFromPath(reqMsg.Path) 297 | if !dashutil.IsRequestTypeValid(reqMsg.RequestType) { 298 | preq.err = fmt.Errorf("Invalid RequestMessage.RequestType [%s]", reqMsg.RequestType) 299 | return preq 300 | } 301 | if reqMsg.AuthData != "" { 302 | var authData AuthAtom 303 | err := json.Unmarshal([]byte(reqMsg.AuthData), &authData) 304 | if err != nil { 305 | preq.err = dasherr.JsonUnmarshalErr("AuthData", err) 306 | return preq 307 | } 308 | preq.authData = &authData 309 | } 310 | if reqMsg.AppStateData != "" { 311 | var pstate interface{} 312 | err := json.Unmarshal([]byte(reqMsg.AppStateData), &pstate) 313 | if err != nil { 314 | preq.err = fmt.Errorf("Cannot unmarshal AppStateData: %v", err) 315 | return preq 316 | } 317 | preq.appState = pstate 318 | } 319 | return preq 320 | } 321 | 322 | func (req *AppRequest) getRRA() []*dashproto.RRAction { 323 | return req.rrActions 324 | } 325 | 326 | // Returns the error (if any) that has been set on this request. 327 | func (req *AppRequest) GetError() error { 328 | return req.err 329 | } 330 | 331 | // Sets an error to be returned from this request. Normally you can just return the 332 | // error from your top-level handler function. This method is for special cases 333 | // where that's not possible. 334 | func (req *AppRequest) SetError(err error) { 335 | req.err = err 336 | } 337 | 338 | // Should normally call NavToPage (if PagesEnabled). This call is a low-level call 339 | // that swaps out the HTML view on the frontend. It does not update the URL. 340 | func (req *AppRequest) SetHtmlPage(htmlPage string) error { 341 | _, _, err := dashutil.ParseHtmlPage(htmlPage) 342 | if err != nil { 343 | return err 344 | } 345 | req.SetData(htmlPagePath, htmlPage) 346 | return nil 347 | } 348 | 349 | // Returns the current frontend page name that generated this request. 350 | func (req *AppRequest) GetPageName() string { 351 | var state dashborgState 352 | err := req.BindAppState(&state) 353 | if err != nil { 354 | return "" 355 | } 356 | strRtn, ok := state.Dashborg[pageNameKey].(string) 357 | if !ok { 358 | return "" 359 | } 360 | return strRtn 361 | } 362 | 363 | // Navigates the application to the given pageName with the given parameters. 364 | // Should only be called for apps that have PagesEnabled. 365 | func (req *AppRequest) NavToPage(pageName string, params interface{}) error { 366 | rrAction := &dashproto.RRAction{ 367 | Ts: dashutil.Ts(), 368 | ActionType: "navto", 369 | Selector: pageName, 370 | } 371 | if params != nil { 372 | jsonData, err := dashutil.MarshalJson(params) 373 | if err != nil { 374 | return fmt.Errorf("Error marshaling json for NavToPage, pageName:%s, err:%v\n", pageName, err) 375 | } 376 | rrAction.JsonData = jsonData 377 | } 378 | req.appendRR(rrAction) 379 | return nil 380 | } 381 | -------------------------------------------------------------------------------- /pkg/dash/runtime.go: -------------------------------------------------------------------------------- 1 | package dash 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sort" 7 | "sync" 8 | 9 | "github.com/sawka/dashborg-go-sdk/pkg/dasherr" 10 | "github.com/sawka/dashborg-go-sdk/pkg/dashutil" 11 | ) 12 | 13 | const ( 14 | pathFragDefault = "@default" 15 | pathFragInit = "@init" 16 | pathFragHtml = "@html" 17 | pathFragTypeInfo = "@typeinfo" 18 | pathFragDyn = "@dyn" 19 | pathFragPageInit = "@pageinit" 20 | ) 21 | 22 | type handlerType struct { 23 | HandlerFn func(req *AppRequest) (interface{}, error) 24 | HandlerInfo *runtimeHandlerInfo 25 | Opts HandlerOpts 26 | } 27 | 28 | // type = any, bool, int, float, string, map, array, struct, blob 29 | type runtimeTypeInfo struct { 30 | Type string `json:"type"` 31 | Strict bool `json:"strict"` 32 | Name string `json:"name,omitempty"` 33 | MimeType string `json:"mimetype,omitempty"` 34 | ElemType *runtimeTypeInfo `json:"elemtype,omitempty"` 35 | FieldTypes []*runtimeTypeInfo `json:"structtype,omitempty"` 36 | } 37 | 38 | type runtimeHandlerInfo struct { 39 | Name string `json:"name"` 40 | Display string `json:"display,omitempty"` 41 | FormDisplay string `json:"formdisplay,omitempty"` 42 | ResultsDisplay string `json:"resultsdisplay,omitempty"` 43 | Description string `json:"description,omitempty"` 44 | Hidden bool `json:"hidden,omitempty"` 45 | Pure bool `json:"pure,omitempty"` 46 | AutoCall bool `json:"autocall,omitempty"` 47 | ContextParam bool `json:"contextparam,omitempty"` 48 | ReqParam bool `json:"reqparam,omitempty"` 49 | AppStateParam bool `json:"appstateparam,omitempty"` 50 | RtnType *runtimeTypeInfo `json:"rtntype"` 51 | ParamsType []runtimeTypeInfo `json:"paramstype"` 52 | } 53 | 54 | type LinkRuntime interface { 55 | RunHandler(req *AppRequest) (interface{}, error) 56 | } 57 | 58 | type HandlerOpts struct { 59 | Hidden bool 60 | PureHandler bool 61 | Display string 62 | FormDisplay string 63 | ResultsDisplay string 64 | } 65 | 66 | type LinkRuntimeImpl struct { 67 | lock *sync.Mutex 68 | middlewares []middlewareType 69 | handlers map[string]handlerType 70 | errs []error 71 | } 72 | 73 | type handlerFuncType = func(req *AppRequest) (interface{}, error) 74 | 75 | type AppRuntimeImpl struct { 76 | lock *sync.Mutex 77 | appStateType reflect.Type 78 | handlers map[string]handlerType 79 | pageHandlers map[string]handlerFuncType 80 | middlewares []middlewareType 81 | errs []error 82 | } 83 | 84 | type runtimeImplIf interface { 85 | addError(err error) 86 | setHandler(path string, handler handlerType) 87 | getStateType() reflect.Type 88 | } 89 | 90 | type HasErr interface { 91 | Err() error 92 | } 93 | 94 | // Creates an app runtime. Normally you should 95 | // use the App class to manage applications which creates 96 | // an AppRuntime automatically. This is for special low-level use cases. 97 | func MakeAppRuntime() *AppRuntimeImpl { 98 | rtn := &AppRuntimeImpl{ 99 | lock: &sync.Mutex{}, 100 | handlers: make(map[string]handlerType), 101 | pageHandlers: make(map[string]handlerFuncType), 102 | } 103 | rtn.SetInitHandler(func() {}, &HandlerOpts{Hidden: true}) 104 | rtn.Handler(pathFragPageInit, rtn.pageInitHandler, &HandlerOpts{Hidden: true}) 105 | rtn.PureHandler(pathFragTypeInfo, rtn.getHandlerInfo, &HandlerOpts{Hidden: true}) 106 | return rtn 107 | } 108 | 109 | func (apprt *AppRuntimeImpl) setHandler(path string, handler handlerType) { 110 | apprt.lock.Lock() 111 | defer apprt.lock.Unlock() 112 | apprt.handlers[path] = handler 113 | } 114 | 115 | func (apprt *AppRuntimeImpl) getStateType() reflect.Type { 116 | return apprt.appStateType 117 | } 118 | 119 | func (apprt *AppRuntimeImpl) getHandlerInfo() (interface{}, error) { 120 | apprt.lock.Lock() 121 | defer apprt.lock.Unlock() 122 | var rtn []*runtimeHandlerInfo 123 | for _, hval := range apprt.handlers { 124 | if hval.HandlerInfo.Hidden { 125 | continue 126 | } 127 | rtn = append(rtn, hval.HandlerInfo) 128 | } 129 | sort.Slice(rtn, func(i int, j int) bool { 130 | return rtn[i].Name < rtn[j].Name 131 | }) 132 | return rtn, nil 133 | } 134 | 135 | func (apprt *AppRuntimeImpl) pageInitHandler(req *AppRequest, pageName string) (interface{}, error) { 136 | handlerFn := apprt.pageHandlers[pageName] 137 | if handlerFn == nil { 138 | return nil, nil 139 | } 140 | return handlerFn(req) 141 | } 142 | 143 | // Set the init function for an application page. Only used when the app has PagesEnabled. 144 | func (apprt *AppRuntimeImpl) SetPageHandler(pageName string, handlerFn interface{}) { 145 | hfn, err := convertHandlerFn(apprt, handlerFn, true, HandlerOpts{}) 146 | if err != nil { 147 | apprt.addError(fmt.Errorf("Error in SetPageHandler(%s): %v", pageName, err)) 148 | return 149 | } 150 | apprt.pageHandlers[pageName] = hfn 151 | } 152 | 153 | // Runs an application handler given an AppRequest. This method is not normally used by end users, 154 | // it is used by the Dashborg runtime to dispatch requests to this runtime. 155 | func (apprt *AppRuntimeImpl) RunHandler(req *AppRequest) (interface{}, error) { 156 | _, _, pathFrag, err := dashutil.ParseFullPath(req.info.Path, true) 157 | if err != nil { 158 | return nil, dasherr.ValidateErr(fmt.Errorf("Invalid Path: %w", err)) 159 | } 160 | if pathFrag == "" { 161 | pathFrag = pathFragDefault 162 | } 163 | apprt.lock.Lock() 164 | hval, ok := apprt.handlers[pathFrag] 165 | mws := apprt.middlewares 166 | apprt.lock.Unlock() 167 | if !ok { 168 | return nil, dasherr.ErrWithCode(dasherr.ErrCodeNoHandler, fmt.Errorf("No handler found for %s", dashutil.SimplifyPath(req.RequestInfo().Path, nil))) 169 | } 170 | if req.info.RequestMethod == RequestMethodGet && !hval.Opts.PureHandler { 171 | return nil, dasherr.ValidateErr(fmt.Errorf("GET/data request to non-pure handler '%s'", pathFrag)) 172 | } 173 | rtn, err := mwHelper(req, hval, mws, 0) 174 | if err != nil { 175 | return nil, err 176 | } 177 | return rtn, nil 178 | } 179 | 180 | func mwHelper(outerReq *AppRequest, hval handlerType, mws []middlewareType, mwPos int) (interface{}, error) { 181 | if mwPos >= len(mws) { 182 | return hval.HandlerFn(outerReq) 183 | } 184 | mw := mws[mwPos] 185 | return mw.Fn(outerReq, func(innerReq *AppRequest) (interface{}, error) { 186 | if innerReq == nil { 187 | panic("No Request Passed to middleware nextFn") 188 | } 189 | return mwHelper(innerReq, hval, mws, mwPos+1) 190 | }) 191 | } 192 | 193 | // Sets the type to unmarshal the application state into. Must be set before the app 194 | // is connected to the Dashborg service. 195 | func (apprt *AppRuntimeImpl) SetAppStateType(appStateType reflect.Type) { 196 | if appStateType != nil { 197 | isStruct := appStateType.Kind() == reflect.Struct 198 | isStructPtr := appStateType.Kind() == reflect.Ptr && appStateType.Elem().Kind() == reflect.Struct 199 | if !isStruct && !isStructPtr { 200 | apprt.addError(fmt.Errorf("AppStateType must be a struct or pointer to struct")) 201 | } 202 | } 203 | apprt.appStateType = appStateType 204 | } 205 | 206 | func addMiddlewares(mws []middlewareType, mw middlewareType) []middlewareType { 207 | newmws := make([]middlewareType, len(mws)+1) 208 | copy(newmws, mws) 209 | newmws[len(mws)] = mw 210 | sort.Slice(newmws, func(i int, j int) bool { 211 | mw1 := newmws[i] 212 | mw2 := newmws[j] 213 | return mw1.Priority > mw2.Priority 214 | }) 215 | return newmws 216 | } 217 | 218 | func removeMiddleware(mws []middlewareType, name string) []middlewareType { 219 | newmws := make([]middlewareType, 0) 220 | for _, mw := range mws { 221 | if mw.Name == name { 222 | continue 223 | } 224 | newmws = append(newmws, mw) 225 | } 226 | return newmws 227 | } 228 | 229 | // Adds a middleware function to this runtime. 230 | func (apprt *AppRuntimeImpl) AddRawMiddleware(name string, mwFunc MiddlewareFuncType, priority float64) { 231 | apprt.RemoveMiddleware(name) 232 | apprt.lock.Lock() 233 | defer apprt.lock.Unlock() 234 | newmw := middlewareType{Name: name, Fn: mwFunc, Priority: priority} 235 | apprt.middlewares = addMiddlewares(apprt.middlewares, newmw) 236 | } 237 | 238 | // Removes a middleware function from this runtime 239 | func (apprt *AppRuntimeImpl) RemoveMiddleware(name string) { 240 | apprt.lock.Lock() 241 | defer apprt.lock.Unlock() 242 | apprt.middlewares = removeMiddleware(apprt.middlewares, name) 243 | } 244 | 245 | // Sets a raw handler. Normal code should use Handler() or PureHandler() which internally calls this method. 246 | func (apprt *AppRuntimeImpl) SetRawHandler(handlerName string, handlerFn func(req *AppRequest) (interface{}, error), opts *HandlerOpts) error { 247 | if opts == nil { 248 | opts = &HandlerOpts{} 249 | } 250 | if !dashutil.IsPathFragValid(handlerName) { 251 | return fmt.Errorf("Invalid handler name") 252 | } 253 | hinfo, err := makeHandlerInfo(apprt, handlerName, handlerFn, *opts) 254 | if err != nil { 255 | return err 256 | } 257 | apprt.setHandler(handlerName, handlerType{HandlerFn: handlerFn, Opts: *opts, HandlerInfo: hinfo}) 258 | return nil 259 | } 260 | 261 | // Set the init handler. Only called if InitRequired is set to true in the application. 262 | // Init handlers run before the application loads, and can set up the internal application state 263 | // or perform validation. If an error is returned the app will not load. The init handler is 264 | // often used to validate url parameters and convert them to application state. 265 | func (apprt *AppRuntimeImpl) SetInitHandler(handlerFn interface{}, opts ...*HandlerOpts) { 266 | apprt.Handler(pathFragInit, handlerFn, opts...) 267 | } 268 | 269 | // Set the application's dynamic HTML handler. Only used if app SetHtmlFromRuntime() has 270 | // been called. Should return a BlobReturn struct with mime type of text/html. 271 | func (apprt *AppRuntimeImpl) SetHtmlHandler(handlerFn interface{}, opts ...*HandlerOpts) { 272 | apprt.Handler(pathFragHtml, handlerFn, opts...) 273 | } 274 | 275 | // Creates a LinkRuntime structure. 276 | func MakeRuntime() *LinkRuntimeImpl { 277 | rtn := &LinkRuntimeImpl{ 278 | lock: &sync.Mutex{}, 279 | handlers: make(map[string]handlerType), 280 | } 281 | rtn.PureHandler(pathFragTypeInfo, rtn.getHandlerInfo) 282 | return rtn 283 | } 284 | 285 | // Creates a LinkRuntime structure with a single function. This lets the handler 286 | // act like a dynamic file. So if the application requests a path (without a fragment), 287 | // the handlerFn can return the result. 288 | func MakeSingleFnRuntime(handlerFn interface{}, opts ...*HandlerOpts) *LinkRuntimeImpl { 289 | rtn := &LinkRuntimeImpl{ 290 | lock: &sync.Mutex{}, 291 | handlers: make(map[string]handlerType), 292 | } 293 | rtn.Handler(pathFragDefault, handlerFn, opts...) 294 | return rtn 295 | } 296 | 297 | func (linkrt *LinkRuntimeImpl) getHandlerInfo() (interface{}, error) { 298 | linkrt.lock.Lock() 299 | defer linkrt.lock.Unlock() 300 | var rtn []*runtimeHandlerInfo 301 | for _, hval := range linkrt.handlers { 302 | if hval.HandlerInfo.Hidden { 303 | continue 304 | } 305 | rtn = append(rtn, hval.HandlerInfo) 306 | } 307 | sort.Slice(rtn, func(i int, j int) bool { 308 | return rtn[i].Name < rtn[j].Name 309 | }) 310 | return rtn, nil 311 | } 312 | 313 | func (linkrt *LinkRuntimeImpl) setHandler(name string, fn handlerType) { 314 | linkrt.lock.Lock() 315 | defer linkrt.lock.Unlock() 316 | linkrt.handlers[name] = fn 317 | } 318 | 319 | // Runs an application handler given an AppRequest. This method is not normally used by end users, 320 | // it is used by the Dashborg runtime to dispatch requests to this runtime. 321 | func (linkrt *LinkRuntimeImpl) RunHandler(req *AppRequest) (interface{}, error) { 322 | info := req.RequestInfo() 323 | if info.RequestType != requestTypePath { 324 | return nil, dasherr.ValidateErr(fmt.Errorf("Invalid RequestType for linked runtime")) 325 | } 326 | _, _, pathFrag, err := dashutil.ParseFullPath(req.info.Path, true) 327 | if err != nil { 328 | return nil, dasherr.ValidateErr(fmt.Errorf("Invalid Path: %w", err)) 329 | } 330 | if pathFrag == "" { 331 | pathFrag = pathFragDefault 332 | } 333 | linkrt.lock.Lock() 334 | hval, ok := linkrt.handlers[pathFrag] 335 | mws := linkrt.middlewares 336 | linkrt.lock.Unlock() 337 | if !ok { 338 | return nil, dasherr.ErrWithCode(dasherr.ErrCodeNoHandler, fmt.Errorf("No handler found for %s", dashutil.SimplifyPath(info.Path, nil))) 339 | } 340 | if req.info.RequestMethod == RequestMethodGet && !hval.Opts.PureHandler { 341 | return nil, dasherr.ValidateErr(fmt.Errorf("GET/Data request to non-pure handler")) 342 | } 343 | rtn, err := mwHelper(req, hval, mws, 0) 344 | if err != nil { 345 | return nil, err 346 | } 347 | return rtn, nil 348 | } 349 | 350 | // Sets a raw handler. Normal code should use Handler() or PureHandler() which internally calls this method. 351 | func (linkrt *LinkRuntimeImpl) SetRawHandler(handlerName string, handlerFn func(req Request) (interface{}, error), opts *HandlerOpts) error { 352 | if opts == nil { 353 | opts = &HandlerOpts{} 354 | } 355 | if !dashutil.IsPathFragValid(handlerName) { 356 | return fmt.Errorf("Invalid handler name") 357 | } 358 | whfn := func(req *AppRequest) (interface{}, error) { 359 | return handlerFn(req) 360 | } 361 | linkrt.setHandler(handlerName, handlerType{HandlerFn: whfn, Opts: *opts}) 362 | return nil 363 | } 364 | 365 | func (apprt *AppRuntimeImpl) addError(err error) { 366 | apprt.errs = append(apprt.errs, err) 367 | } 368 | 369 | // Returns any setup errors that the runtime encountered. 370 | func (apprt *AppRuntimeImpl) Err() error { 371 | return dashutil.ConvertErrArray(apprt.errs) 372 | } 373 | 374 | func (linkrt *LinkRuntimeImpl) addError(err error) { 375 | linkrt.errs = append(linkrt.errs, err) 376 | } 377 | 378 | // Returns any setup errors that the runtime encountered. 379 | func (linkrt *LinkRuntimeImpl) Err() error { 380 | return dashutil.ConvertErrArray(linkrt.errs) 381 | } 382 | 383 | // Adds a middleware function to this runtime. 384 | func (linkrt *LinkRuntimeImpl) AddRawMiddleware(name string, mwFunc MiddlewareFuncType, priority float64) { 385 | linkrt.RemoveMiddleware(name) 386 | linkrt.lock.Lock() 387 | defer linkrt.lock.Unlock() 388 | newmw := middlewareType{Name: name, Fn: mwFunc, Priority: priority} 389 | linkrt.middlewares = addMiddlewares(linkrt.middlewares, newmw) 390 | } 391 | 392 | // Removes a middleware function from this runtime 393 | func (linkrt *LinkRuntimeImpl) RemoveMiddleware(name string) { 394 | linkrt.lock.Lock() 395 | defer linkrt.lock.Unlock() 396 | linkrt.middlewares = removeMiddleware(linkrt.middlewares, name) 397 | } 398 | 399 | func (linkrt *LinkRuntimeImpl) getStateType() reflect.Type { 400 | return nil 401 | } 402 | -------------------------------------------------------------------------------- /pkg/dasherr/dasherr.go: -------------------------------------------------------------------------------- 1 | // Structured errors for Dashborg providing a wrapped error with error codes. 2 | package dasherr 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/sawka/dashborg-go-sdk/pkg/dashproto" 10 | ) 11 | 12 | type ErrCode string 13 | 14 | const ( 15 | ErrCodeNone ErrCode = "" 16 | ErrCodeEof ErrCode = "EOF" 17 | ErrCodeUnknown ErrCode = "UNKNOWN" 18 | ErrCodeBadConnId ErrCode = "BADCONNID" 19 | ErrCodeAccAccess ErrCode = "ACCACCESS" 20 | ErrCodeNoHandler ErrCode = "NOHANDLER" 21 | ErrCodeBadAuth ErrCode = "BADAUTH" 22 | ErrCodeRoleAuth ErrCode = "BADROLE" 23 | ErrCodeBadZone ErrCode = "BADZONE" 24 | ErrCodeNoAcc ErrCode = "NOACC" 25 | ErrCodeOffline ErrCode = "OFFLINE" 26 | ErrCodePanic ErrCode = "PANIC" 27 | ErrCodeJson ErrCode = "JSON" 28 | ErrCodeRpc ErrCode = "RPC" 29 | ErrCodeUpload ErrCode = "UPLOAD" 30 | ErrCodeLimit ErrCode = "LIMIT" 31 | ErrCodeNotConnected ErrCode = "NOCONN" 32 | ErrCodeValidation ErrCode = "NOTVALID" 33 | ErrCodeQueueFull ErrCode = "QUEUE" 34 | ErrCodeTimeout ErrCode = "TIMEOUT" 35 | ErrCodeNotImpl ErrCode = "NOTIMPL" 36 | ErrCodePathNotFound ErrCode = "NOTFOUND" 37 | ErrCodeBadPath ErrCode = "BADPATH" 38 | ErrCodeNoApp ErrCode = "NOAPP" 39 | ErrCodeProtocol ErrCode = "PROTOCOL" 40 | ErrCodeInitErr ErrCode = "INITERR" 41 | ) 42 | 43 | type DashErr struct { 44 | apiName string 45 | err error 46 | code ErrCode 47 | permanent bool 48 | } 49 | 50 | func (e *DashErr) Error() string { 51 | codeStr := "" 52 | if e.code != "" { 53 | codeStr = fmt.Sprintf("[%s] ", e.code) 54 | } 55 | if e.apiName == "" { 56 | return fmt.Sprintf("%s%v", codeStr, e.err) 57 | } 58 | return fmt.Sprintf("Error calling %s: %s%v", e.apiName, codeStr, e.err) 59 | } 60 | 61 | func (e *DashErr) Unwrap() error { 62 | return e.err 63 | } 64 | 65 | func (e *DashErr) ErrCode() ErrCode { 66 | return e.code 67 | } 68 | 69 | func (e *DashErr) CanRetry() bool { 70 | return !e.permanent 71 | } 72 | 73 | // If err is a DashErr, returns CanRetry(), otherwise returns true. 74 | func CanRetry(err error) bool { 75 | var dashErr *DashErr 76 | if errors.As(err, &dashErr) { 77 | return dashErr.CanRetry() 78 | } 79 | return true 80 | } 81 | 82 | // If err is a DashErr, unwraps it to return the inner error message, otherwise 83 | // just calls Error(). 84 | func GetMessage(err error) string { 85 | var dashErr *DashErr 86 | if errors.As(err, &dashErr) { 87 | return dashErr.err.Error() 88 | } 89 | return err.Error() 90 | } 91 | 92 | // If err is a DashErr returns the error code, otherwise returns "" for non-DashErr errors. 93 | func GetErrCode(err error) ErrCode { 94 | var dashErr *DashErr 95 | if errors.As(err, &dashErr) { 96 | return dashErr.ErrCode() 97 | } 98 | return ErrCodeNone 99 | } 100 | 101 | // If err is a DashErr, returns it, otherwise wraps it into a DashErr struct. 102 | func AsDashErr(err error) *DashErr { 103 | var dashErr *DashErr 104 | if errors.As(err, &dashErr) { 105 | return dashErr 106 | } 107 | return &DashErr{err: err} 108 | } 109 | 110 | // Wraps err into a DashErr with the given error code 111 | func ErrWithCode(code ErrCode, err error) error { 112 | return &DashErr{err: err, code: code} 113 | } 114 | 115 | // Wraps errStr into a DashErr with the given error code 116 | func ErrWithCodeStr(code ErrCode, errStr string) error { 117 | return &DashErr{err: errors.New(errStr), code: code} 118 | } 119 | 120 | // Wraps err into a DashErr with no-retry set. 121 | func NoRetryErr(err error) error { 122 | return &DashErr{err: err, permanent: true} 123 | } 124 | 125 | // Wraps err into a DashErr with the given error code and no-retry set. 126 | func NoRetryErrWithCode(code ErrCode, err error) error { 127 | return &DashErr{code: code, err: err, permanent: true} 128 | } 129 | 130 | // Creates a DashErr 131 | func MakeDashErr(code ErrCode, isPermanent bool, err error) error { 132 | return &DashErr{code: code, err: err, permanent: isPermanent} 133 | } 134 | 135 | // Creates a DashErr from a gRPC RtnStatus 136 | func FromRtnStatus(apiName string, rtnStatus *dashproto.RtnStatus) error { 137 | if rtnStatus == nil { 138 | return &DashErr{apiName: apiName, err: errors.New("No Return Status"), permanent: true} 139 | } 140 | if rtnStatus.Success { 141 | return nil 142 | } 143 | var statusErr error 144 | if rtnStatus.Err != "" { 145 | statusErr = errors.New(rtnStatus.Err) 146 | } else { 147 | statusErr = errors.New("Unspecified Error") 148 | } 149 | rtnErr := &DashErr{ 150 | apiName: apiName, 151 | err: statusErr, 152 | code: ErrCode(rtnStatus.ErrCode), 153 | permanent: rtnStatus.PermErr, 154 | } 155 | return rtnErr 156 | } 157 | 158 | // Creates a dashproto.ErrorType from the fields of DashErr 159 | func AsProtoErr(err error) *dashproto.ErrorType { 160 | if err == nil { 161 | return nil 162 | } 163 | return &dashproto.ErrorType{ 164 | Err: GetMessage(err), 165 | ErrCode: string(GetErrCode(err)), 166 | PermErr: !CanRetry(err), 167 | } 168 | } 169 | 170 | // Turns a dashproto.ErrorType into a DashErr 171 | func FromProtoErr(perr *dashproto.ErrorType) error { 172 | if perr == nil { 173 | return nil 174 | } 175 | var statusErr error 176 | if perr.Err != "" { 177 | statusErr = errors.New(perr.Err) 178 | } else { 179 | statusErr = errors.New("Unspecified Error") 180 | } 181 | return &DashErr{ 182 | err: statusErr, 183 | code: ErrCode(perr.ErrCode), 184 | permanent: perr.PermErr, 185 | } 186 | } 187 | 188 | // Creates a DashErr from a gRPC error 189 | func RpcErr(apiName string, respErr error) error { 190 | if respErr == nil { 191 | return nil 192 | } 193 | rtnErr := &DashErr{ 194 | apiName: apiName, 195 | err: respErr, 196 | code: ErrCodeRpc, 197 | } 198 | return rtnErr 199 | } 200 | 201 | // Creates a DashErr from a json.Marshal error 202 | func JsonMarshalErr(thing string, err error) error { 203 | return &DashErr{ 204 | err: fmt.Errorf("Error Marshaling %s to JSON: %w", thing, err), 205 | code: ErrCodeJson, 206 | permanent: true, 207 | } 208 | } 209 | 210 | // Creates a DashErr from a json.Unmarshal error 211 | func JsonUnmarshalErr(thing string, err error) error { 212 | return &DashErr{ 213 | err: fmt.Errorf("Error Unmarshaling %s from JSON: %w", thing, err), 214 | code: ErrCodeJson, 215 | permanent: true, 216 | } 217 | } 218 | 219 | // Creates a validation DashErr 220 | func ValidateErr(err error) error { 221 | if GetErrCode(err) == ErrCodeValidation { 222 | return err 223 | } 224 | return &DashErr{ 225 | err: err, 226 | code: ErrCodeValidation, 227 | permanent: true, 228 | } 229 | } 230 | 231 | // Creates a DashErr around an exceeded account limit. 232 | func LimitErr(message string, limitName string, limitMax float64) error { 233 | limitUnit := "" 234 | if strings.HasSuffix(limitName, "MB") { 235 | limitUnit = "MB" 236 | } 237 | return &DashErr{ 238 | err: fmt.Errorf("DashborgLimitError limit:%s exceeded, max=%0.1f%s - %s", limitName, limitMax, limitUnit, message), 239 | code: ErrCodeLimit, 240 | permanent: true, 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /pkg/dashproto/dashproto.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package dashborg.rpc1; 3 | option go_package = "github.com/sawka/dashborg-go-sdk/pkg/dashproto"; 4 | 5 | message RtnStatus { 6 | bool Success = 1; 7 | string Err = 2; 8 | string ErrCode = 3; 9 | bool PermErr = 4; 10 | } 11 | 12 | message ErrorType { 13 | string Err = 1; 14 | string ErrCode = 2; 15 | bool PermErr = 3; 16 | } 17 | 18 | message RRAction { 19 | int64 Ts = 1; 20 | string ActionType = 2; 21 | string Selector = 3; // path, selector, controlid 22 | string JsonData = 4; 23 | string OpType = 5; 24 | string Html = 6; 25 | ErrorType Err = 7; 26 | bytes BlobBytes = 8; // for "blob" / "blobext" ActionType 27 | string BlobMimeType = 9; // for "blob" ActionType 28 | string ReqId = 10; // transient for LocalServer 29 | } 30 | 31 | message SendResponseMessage { 32 | int64 Ts = 1; 33 | string ReqId = 2; 34 | string RequestType = 3; 35 | string Path = 4; 36 | string FeClientId = 5; 37 | bool ResponseDone = 6; 38 | repeated RRAction Actions = 7; 39 | ErrorType Err = 8; 40 | } 41 | 42 | message SendResponseResponse { 43 | RtnStatus Status = 1; 44 | int32 NumStreamClients = 2; 45 | } 46 | 47 | message RequestMessage { 48 | int64 Ts = 1; 49 | string AccId = 2; 50 | string ZoneName = 3; 51 | string RequestType = 4; 52 | string Path = 5; 53 | string ReqId = 6; 54 | string FeClientId = 7; 55 | string JsonData = 8; 56 | string AuthData = 9; 57 | string AppStateData = 10; 58 | RtnStatus Status = 11; // only used for server-side errors 59 | int64 TimeoutMs = 12; 60 | bool IsBackendCall = 13; 61 | bool AppRequest = 14; 62 | string RequestMethod = 15; 63 | } 64 | 65 | message ConnectClientMessage { 66 | int64 Ts = 1; 67 | string ProcRunId = 2; 68 | string AccId = 3; 69 | string ZoneName = 4; 70 | bool AnonAcc = 5; 71 | string ProcName = 6; 72 | string ProcIKey = 7; 73 | map ProcTags = 8; 74 | map HostData = 9; 75 | int64 StartTs = 10; 76 | } 77 | 78 | message ConnectClientResponse { 79 | RtnStatus Status = 1; 80 | string ConnId = 2; 81 | string AccInfoJson = 3; 82 | } 83 | 84 | message RequestStreamMessage { 85 | int64 Ts = 1; 86 | } 87 | 88 | message SetPathMessage { 89 | int64 Ts = 1; 90 | string Path = 2; 91 | string TxId = 3; 92 | bool HasBody = 4; 93 | bool ConnectRuntime = 5; 94 | string FileOptsJson = 6; 95 | } 96 | 97 | message SetPathResponse { 98 | RtnStatus Status = 1; 99 | bool BlobFound = 2; 100 | string BlobUploadId = 3; 101 | string BlobUploadKey = 4; 102 | } 103 | 104 | message RemovePathMessage { 105 | int64 Ts = 1; 106 | string Path = 2; 107 | bool RemoveFullApp = 3; 108 | } 109 | 110 | message RemovePathResponse { 111 | RtnStatus Status = 1; 112 | } 113 | 114 | message FileInfoMessage { 115 | int64 Ts = 1; 116 | string Path = 2; 117 | string DirOptsJson = 3; 118 | bool RtnContents = 4; 119 | } 120 | 121 | message FileInfoResponse { 122 | RtnStatus Status = 1; 123 | string FileInfoJson = 2; 124 | bytes FileContent = 3; 125 | bool FileContentRtn = 4; 126 | } 127 | 128 | message ConnectLinkMessage { 129 | int64 Ts = 1; 130 | string Path = 2; 131 | } 132 | 133 | message ConnectLinkResponse { 134 | RtnStatus Status = 1; 135 | } 136 | 137 | service DashborgService { 138 | rpc ConnectClient(ConnectClientMessage) returns (ConnectClientResponse) {} 139 | rpc RequestStream(RequestStreamMessage) returns (stream RequestMessage) {} // backwards 140 | rpc SendResponse(SendResponseMessage) returns (SendResponseResponse) {} 141 | rpc SetPath(SetPathMessage) returns (SetPathResponse) {} 142 | rpc RemovePath(RemovePathMessage) returns (RemovePathResponse) {} 143 | rpc FileInfo(FileInfoMessage) returns (FileInfoResponse) {} 144 | rpc ConnectLink(ConnectLinkMessage) returns (ConnectLinkResponse) {} 145 | } 146 | 147 | -------------------------------------------------------------------------------- /pkg/dashproto/doc.go: -------------------------------------------------------------------------------- 1 | // Generated gRPC types for communicating with Dashborg service 2 | package dashproto 3 | -------------------------------------------------------------------------------- /pkg/dashutil/path.go: -------------------------------------------------------------------------------- 1 | package dashutil 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // keep in sync with dash consts 10 | const ( 11 | rootAppPath = "/_/apps" 12 | appRuntimeSubPath = "/_/runtime" 13 | appPathNs = "app" 14 | ) 15 | 16 | func PathNoFrag(fullPath string) (string, error) { 17 | pathNs, path, _, err := ParseFullPath(fullPath, true) 18 | if err != nil { 19 | return "", err 20 | } 21 | pathNsStr := "" 22 | if pathNs != "" { 23 | pathNsStr = "/@" + pathNs 24 | } 25 | return pathNsStr + path, nil 26 | } 27 | 28 | var pathAppNameRe = regexp.MustCompile("^" + rootAppPath + "/([a-zA-Z][a-zA-Z0-9_.-]*)(?:/|$)") 29 | 30 | func AppNameFromPath(path string) string { 31 | matches := pathAppNameRe.FindStringSubmatch(path) 32 | if matches == nil { 33 | return "" 34 | } 35 | return matches[1] 36 | } 37 | 38 | type FormatPathOpts struct { 39 | AppName string 40 | FsPath string 41 | } 42 | 43 | func appPathFromName(appName string) string { 44 | return rootAppPath + "/" + appName 45 | } 46 | 47 | func CanonicalizePath(fullPath string, opts *FormatPathOpts) (string, error) { 48 | if opts == nil { 49 | opts = &FormatPathOpts{} 50 | } 51 | pathNs, path, pathFrag, err := ParseFullPath(fullPath, true) 52 | if err != nil { 53 | return "", err 54 | } 55 | if pathNs == "" { 56 | return fullPath, nil 57 | } 58 | fragStr := "" 59 | if pathFrag != "" { 60 | fragStr = ":" + pathFrag 61 | } 62 | if pathNs == "app" { 63 | if opts.AppName == "" { 64 | return "", fmt.Errorf("Cannot canonicalize /@app path without an app context (use canonical path or /@app=[appname]/ format)") 65 | } 66 | if path == "/" && pathFrag != "" { 67 | return fmt.Sprintf("/_/apps/%s/_/runtime:%s", opts.AppName, pathFrag), nil 68 | } 69 | return fmt.Sprintf("/_/apps/%s%s%s", opts.AppName, path, fragStr), nil 70 | } 71 | if pathNs == "self" { 72 | if opts.FsPath == "" { 73 | return "", fmt.Errorf("Cannot canonicalize /@self path without an FS path (use canonical path format)") 74 | } 75 | if path != "/" { 76 | return "", fmt.Errorf("Cannot /@self path cannot have sub-path") 77 | } 78 | return CanonicalizePath(fmt.Sprintf("%s%s", opts.FsPath, fragStr), opts) 79 | } 80 | if strings.HasPrefix(pathNs, "app=") { 81 | appName := pathNs[4:] 82 | if !IsAppNameValid(appName) { 83 | return "", fmt.Errorf("Cannot canonicalize /%s path, invalid app name", pathNs) 84 | } 85 | if path == "/" && pathFrag != "" { 86 | return fmt.Sprintf("/_/apps/%s/_/runtime:%s", appName, pathFrag), nil 87 | } 88 | return fmt.Sprintf("/_/apps/%s%s%s", appName, path, fragStr), nil 89 | } 90 | return fullPath, nil 91 | } 92 | 93 | func GetPathNs(fullPath string) string { 94 | pathNs, _, _, _ := ParseFullPath(fullPath, true) 95 | return pathNs 96 | } 97 | 98 | func SimplifyPath(fullPath string, opts *FormatPathOpts) string { 99 | if opts == nil { 100 | opts = &FormatPathOpts{} 101 | } 102 | pathNs, path, pathFrag, err := ParseFullPath(fullPath, true) 103 | if err != nil || pathNs != "" { 104 | return fullPath 105 | } 106 | pathAppName := AppNameFromPath(path) 107 | if pathAppName == "" { 108 | return fullPath 109 | } 110 | rtnPath := "" 111 | appPath := appPathFromName(pathAppName) 112 | if pathAppName == opts.AppName { 113 | path = strings.Replace(path, appPath, "", 1) 114 | rtnPath = "/@app" 115 | } else { 116 | path = strings.Replace(path, appPath, "", 1) 117 | rtnPath = fmt.Sprintf("/@app=%s", pathAppName) 118 | } 119 | fragStr := "" 120 | if pathFrag != "" { 121 | fragStr = ":" + pathFrag 122 | } 123 | if path == appRuntimeSubPath && pathFrag != "" { 124 | return rtnPath + fragStr 125 | } 126 | return rtnPath + path + fragStr 127 | } 128 | 129 | func ParseFullPath(fullPath string, allowFrag bool) (string, string, string, error) { 130 | if fullPath == "" { 131 | return "", "", "", fmt.Errorf("Path cannot be empty") 132 | } 133 | if len(fullPath) > FullPathMax { 134 | return "", "", "", fmt.Errorf("Path too long") 135 | } 136 | if fullPath[0] != '/' { 137 | return "", "", "", fmt.Errorf("Path must begin with '/'") 138 | } 139 | match := fullPathRe.FindStringSubmatch(fullPath) 140 | if match == nil { 141 | return "", "", "", fmt.Errorf("Invalid Path '%s'", fullPath) 142 | } 143 | path := match[2] 144 | if path == "" { 145 | path = "/" 146 | } 147 | if match[3] != "" && !allowFrag { 148 | return "", "", "", fmt.Errorf("Path does not allow path-fragment") 149 | } 150 | if strings.Index(path, "//") != -1 { 151 | return "", "", "", fmt.Errorf("Path '%s' contains empty part, '//' in path pos=%d", path, strings.Index(path, "//")) 152 | } 153 | return match[1], path, match[3], nil 154 | } 155 | 156 | func ValidateFullPath(fullPath string, allowFrag bool) error { 157 | _, _, _, err := ParseFullPath(fullPath, allowFrag) 158 | return err 159 | } 160 | 161 | func GetFileName(fullPath string) (string, error) { 162 | _, pathMain, _, err := ParseFullPath(fullPath, true) 163 | if err != nil { 164 | return "", err 165 | } 166 | if pathMain == "" { 167 | return "", fmt.Errorf("Invalid path") 168 | } 169 | if pathMain == "/" { 170 | return pathMain, nil 171 | } 172 | lastIndex := -1 173 | if pathMain[len(pathMain)-1] == '/' { 174 | lastIndex = strings.LastIndex(pathMain[0:len(pathMain)-1], "/") 175 | } else { 176 | lastIndex = strings.LastIndex(pathMain, "/") 177 | } 178 | if lastIndex == -1 { 179 | return "", fmt.Errorf("Invalid path") 180 | } 181 | return pathMain[lastIndex+1:], nil 182 | } 183 | 184 | func GetParentDirectory(fullPath string) (string, error) { 185 | pathNs, pathMain, _, err := ParseFullPath(fullPath, true) 186 | if err != nil { 187 | return "", err 188 | } 189 | if pathMain == "/" { 190 | return "", fmt.Errorf("Root directory does not have parent") 191 | } 192 | if pathMain[len(pathMain)-1] == '/' { 193 | // remove trailing '/' (directory) 194 | pathMain = pathMain[0 : len(pathMain)-1] 195 | } 196 | lastIndex := strings.LastIndex(pathMain, "/") 197 | if lastIndex == -1 { 198 | return "", fmt.Errorf("Invalid Path '%s' (does not start with /)", fullPath) 199 | } 200 | parentPath := pathMain[0 : lastIndex+1] 201 | nsStr := "" 202 | if pathNs != "" { 203 | nsStr = "/@" + pathNs 204 | } 205 | return nsStr + parentPath, nil 206 | } 207 | 208 | func GetPathDepth(fullPath string) int { 209 | if fullPath == "" { 210 | return 0 211 | } 212 | pathDepth := strings.Count(fullPath, "/") 213 | if fullPath[len(fullPath)-1] == '/' { 214 | pathDepth = pathDepth - 1 215 | } 216 | return pathDepth 217 | } 218 | -------------------------------------------------------------------------------- /pkg/dashutil/util.go: -------------------------------------------------------------------------------- 1 | // Dashborg utility functions. 2 | package dashutil 3 | 4 | import ( 5 | "bytes" 6 | "crypto/sha256" 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "time" 14 | "unicode" 15 | ) 16 | 17 | // Utility type for the SortSpec generated by a d-table node. 18 | type SortSpec struct { 19 | Column string `json:"column"` 20 | Asc bool `json:"asc"` 21 | } 22 | 23 | // Parsed ClientVersion struct (e.g. go-0.7.0). 24 | type ClientVersion struct { 25 | Valid bool 26 | ClientType string 27 | MajorVersion int 28 | MinorVersion int 29 | PatchVersion int 30 | } 31 | 32 | // Dashborg timestamp (epoch milliseconds) 33 | func Ts() int64 { 34 | return time.Now().UnixNano() / 1000000 35 | } 36 | 37 | // Converts a time.Time to Dashborg timestamp 38 | func DashTime(t time.Time) int64 { 39 | return t.UnixNano() / 1000000 40 | } 41 | 42 | // Converts a Dashborg timestamp to Golang time.Time 43 | func GoTime(ts int64) time.Time { 44 | return time.Unix(ts/1000, (ts%1000)*1000000) 45 | } 46 | 47 | // Marshal json helper 48 | func MarshalJson(val interface{}) (string, error) { 49 | var jsonBuf bytes.Buffer 50 | enc := json.NewEncoder(&jsonBuf) 51 | enc.SetEscapeHTML(false) 52 | err := enc.Encode(val) 53 | if err != nil { 54 | return "", err 55 | } 56 | return jsonBuf.String(), nil 57 | } 58 | 59 | // Marshal json helper (adding indentation) 60 | func MarshalJsonIndent(val interface{}) (string, error) { 61 | var jsonBuf bytes.Buffer 62 | enc := json.NewEncoder(&jsonBuf) 63 | enc.SetEscapeHTML(false) 64 | enc.SetIndent("", " ") 65 | err := enc.Encode(val) 66 | if err != nil { 67 | return "", err 68 | } 69 | return jsonBuf.String(), nil 70 | } 71 | 72 | // Marshal json helper, no error returned (no panic either) useful 73 | // when a structure should never have a json encoding error. 74 | func MarshalJsonNoError(val interface{}) string { 75 | var jsonBuf bytes.Buffer 76 | enc := json.NewEncoder(&jsonBuf) 77 | enc.SetEscapeHTML(false) 78 | err := enc.Encode(val) 79 | if err != nil { 80 | return "\"error marshaling json\"" 81 | } 82 | return jsonBuf.String() 83 | } 84 | 85 | // Creates a Dashborg compatible double quoted string for pure ASCII printable strings (+ tab, newline, linefeed). 86 | // Not a general purpose string quoter, but will work for most simple keys. 87 | func QuoteString(str string) string { 88 | var buf bytes.Buffer 89 | buf.WriteByte('"') 90 | for i := 0; i < len(str); i++ { 91 | ch := str[i] 92 | if ch > unicode.MaxASCII || ch == 127 { 93 | buf.WriteByte('_') 94 | continue 95 | } 96 | switch ch { 97 | case '\t': 98 | buf.WriteByte('\\') 99 | buf.WriteByte('t') 100 | continue 101 | 102 | case '\n': 103 | buf.WriteByte('\\') 104 | buf.WriteByte('n') 105 | continue 106 | 107 | case '\r': 108 | buf.WriteByte('\\') 109 | buf.WriteByte('r') 110 | continue 111 | 112 | case '\\': 113 | buf.WriteByte('\\') 114 | buf.WriteByte('\\') 115 | continue 116 | } 117 | if !strconv.IsPrint(rune(ch)) { 118 | buf.WriteByte('_') 119 | continue 120 | } 121 | buf.WriteByte(ch) 122 | } 123 | buf.WriteByte('"') 124 | return buf.String() 125 | } 126 | 127 | // Add a string to a string array. Does not add duplicates. 128 | func AddToStringArr(arr []string, val string) []string { 129 | for _, s := range arr { 130 | if s == val { 131 | return arr 132 | } 133 | } 134 | return append(arr, val) 135 | } 136 | 137 | // Removes a string from a string array. 138 | func RemoveFromStringArr(arr []string, val string) []string { 139 | pos := -1 140 | for idx, v := range arr { 141 | if v == val { 142 | pos = idx 143 | break 144 | } 145 | } 146 | if pos == -1 { 147 | return arr 148 | } 149 | arr[pos] = arr[len(arr)-1] 150 | return arr[:len(arr)-1] 151 | } 152 | 153 | // Returns the first string that has length > 0. 154 | func DefaultString(opts ...string) string { 155 | for _, s := range opts { 156 | if s != "" { 157 | return s 158 | } 159 | } 160 | return "" 161 | } 162 | 163 | // Used to get the value for boolean environment variables. Everything except for "0" and "" is true. 164 | func EnvOverride(val bool, varName string) bool { 165 | envVal := os.Getenv(varName) 166 | if envVal == "0" { 167 | return false 168 | } 169 | if envVal == "" { 170 | return val 171 | } 172 | return true 173 | } 174 | 175 | // Converts []byte, string, and all the integer/floating point golang primitive types to a string. 176 | func ConvertToString(valArg interface{}) (string, error) { 177 | if valArg == nil { 178 | return "", nil 179 | } 180 | switch val := valArg.(type) { 181 | case []byte: 182 | return string(val), nil 183 | 184 | case string: 185 | return val, nil 186 | 187 | case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: 188 | return fmt.Sprintf("%v", val), nil 189 | 190 | default: 191 | return "", fmt.Errorf("Invalid base type: %T", val) 192 | } 193 | } 194 | 195 | // Creates an frontend app path given a zoneName and appName. 196 | func MakeAppPath(zoneName string, appName string) string { 197 | if zoneName == "default" && appName == "default" { 198 | return "/" 199 | } 200 | if zoneName == "default" { 201 | return fmt.Sprintf("/app/%s", appName) 202 | } 203 | if appName == "default" { 204 | return fmt.Sprintf("/zone/%s", zoneName) 205 | } 206 | return fmt.Sprintf("/zone/%s/app/%s", zoneName, appName) 207 | } 208 | 209 | // Parsing a ClientVersion struct 210 | func ParseClientVersion(version string) ClientVersion { 211 | match := clientVersionRe.FindStringSubmatch(version) 212 | if match == nil { 213 | return ClientVersion{} 214 | } 215 | rtn := ClientVersion{Valid: true} 216 | rtn.ClientType = match[1] 217 | rtn.MajorVersion, _ = strconv.Atoi(match[2]) 218 | rtn.MinorVersion, _ = strconv.Atoi(match[3]) 219 | rtn.PatchVersion, _ = strconv.Atoi(match[4]) 220 | return rtn 221 | } 222 | 223 | func (v ClientVersion) String() string { 224 | if !v.Valid { 225 | return "" 226 | } 227 | return fmt.Sprintf("%s-%d.%d.%d", v.ClientType, v.MajorVersion, v.MinorVersion, v.PatchVersion) 228 | } 229 | 230 | // Returns the SHA-256 Base64 encoded hash of passed bytes. 231 | func Sha256Base64(barr []byte) string { 232 | hashVal := sha256.Sum256(barr) 233 | hashValStr := base64.StdEncoding.EncodeToString(hashVal[:]) 234 | return hashValStr 235 | } 236 | 237 | // Encapsulates an array of errors 238 | type MultiErr struct { 239 | Errs []error 240 | } 241 | 242 | func (merr MultiErr) Error() string { 243 | errStrs := make([]string, len(merr.Errs)) 244 | for idx, err := range merr.Errs { 245 | errStrs[idx] = err.Error() 246 | } 247 | return fmt.Sprintf("Multiple Errors (%d): [%s]", len(merr.Errs), strings.Join(errStrs, " | ")) 248 | } 249 | 250 | func ConvertErrArray(errs []error) error { 251 | if len(errs) == 0 { 252 | return nil 253 | } 254 | if len(errs) == 1 { 255 | return errs[0] 256 | } 257 | return MultiErr{Errs: errs} 258 | } 259 | 260 | // Helper to make Dashborg "html page" string from a path and pageName. 261 | func MakeHtmlPage(path string, pageName string) string { 262 | if path == "" { 263 | return pageName 264 | } 265 | if pageName == "" { 266 | return path 267 | } 268 | return fmt.Sprintf("%s | %s", path, pageName) 269 | } 270 | 271 | // Parses Dashborg "html page" string, returns (path, pageName, error) 272 | func ParseHtmlPage(displayStr string) (string, string, error) { 273 | fields := strings.Split(displayStr, "|") 274 | if len(fields) != 1 && len(fields) != 2 { 275 | return "", "", fmt.Errorf("Invalid display, format as either [path], [pagename], or [path]|[pagename]") 276 | } 277 | var pathStr, pageName string 278 | if len(fields) == 1 { 279 | f1 := strings.TrimSpace(fields[0]) 280 | if strings.HasPrefix(f1, "/") { 281 | pathStr = f1 282 | pageName = "default" 283 | } else { 284 | pageName = f1 285 | } 286 | } else { 287 | pathStr = strings.TrimSpace(fields[0]) 288 | pageName = strings.TrimSpace(fields[1]) 289 | } 290 | if pathStr != "" { 291 | err := ValidateFullPath(pathStr, true) 292 | if err != nil { 293 | return "", "", err 294 | } 295 | } 296 | if !IsSimpleIdValid(pageName) { 297 | return "", "", fmt.Errorf("Invalid page name '%s'", pageName) 298 | } 299 | return pathStr, pageName, nil 300 | } 301 | -------------------------------------------------------------------------------- /pkg/dashutil/validators.go: -------------------------------------------------------------------------------- 1 | package dashutil 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | ZoneNameMax = 20 10 | PageNameMax = 20 11 | ZoneAccessMax = 50 12 | ControlNameMax = 30 13 | AppNameMax = 50 14 | AppTitleMax = 80 15 | ProcNameMax = 20 16 | ProcIKeyMax = 50 17 | FileNameMax = 80 18 | EmailMax = 80 19 | PasswordMax = 80 20 | PasswordMin = 8 21 | MimeTypeMax = 80 22 | Sha256HexLen = 64 23 | Sha256Base64Len = 44 24 | UuidLen = 36 25 | DescriptionMax = 100 26 | FileDisplayMax = 100 27 | HandlerPathMax = 100 28 | DataPathMax = 200 29 | PathMax = 100 30 | PathFragMax = 30 31 | FullPathMax = 120 32 | TagMax = 50 33 | RoleMax = 12 34 | RoleListMax = 50 35 | ClientVersionMax = 20 36 | ProcTagValMax = 200 37 | HostDataValMax = 100 38 | SimpleIdMax = 30 39 | UserIdMax = 100 40 | AppConfigJsonMax = 2000 41 | MetadataJsonMax = 1000 42 | ) 43 | 44 | var ( 45 | zoneNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.-]*$") 46 | controlNameRe = regexp.MustCompile("^[a-zA-Z0-9_.:#/-]+$") 47 | appNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.-]*$") 48 | pageNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.-]*$") 49 | procNameRe = regexp.MustCompile("^[a-zA-Z0-9_.-]+$") 50 | procIKeyRe = regexp.MustCompile("^[a-zA-Z0-9_.-]+$") 51 | uuidRe = regexp.MustCompile("^[a-fA-F0-9-]{36}$") 52 | handlerPathRe = regexp.MustCompile("^/@?[a-zA-Z0-9_-][a-zA-Z0-9_/-]*$") 53 | fileDisplayRe = regexp.MustCompile("^@[a-zA-Z0-9_-]+$") 54 | base64Re = regexp.MustCompile("^[a-zA-Z0-9/+=]+$") 55 | hexRe = regexp.MustCompile("^[a-f0-9]+$") 56 | imageMimeTypeRe = regexp.MustCompile("^image/[a-zA-Z0-9._+-]+$") 57 | mimeTypeRe = regexp.MustCompile("^[a-z0-9.-]+/[a-zA-Z0-9._+-]+$") 58 | simpleFileNameRe = regexp.MustCompile("^[a-zA-Z0-9._-]+$") 59 | pathRe = regexp.MustCompile("^/[a-zA-Z0-9._/-]*$") 60 | pathFragRe = regexp.MustCompile("^@?[a-zA-Z_][a-zA-Z0-9_-]*$") 61 | fullPathRe = regexp.MustCompile("^(?:/@([a-zA-Z_][a-zA-Z0-9=_.-]*))?(/[a-zA-Z0-9._/-]*)?(?:[:](@?[a-zA-Z][a-zA-Z0-9_-]*))?$") 62 | tagRe = regexp.MustCompile("^[a-zA-Z0-9._:/-]+$") 63 | roleRe = regexp.MustCompile("^(\\*|-|[a-z][a-z0-9-]+)$") 64 | simpleIdRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_-]*") 65 | clientVersionRe = regexp.MustCompile("^([a-z][a-z0-9_]*)-(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,4})$") 66 | zoneAccessRe = regexp.MustCompile("^[a-zA-Z0-9_.*-]+$") 67 | 68 | // https://www.w3.org/TR/2016/REC-html51-20161101/sec-forms.html#email-state-typeemali 69 | emailRe = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") 70 | 71 | userIdRe = regexp.MustCompile("^[a-z0-9A-Z_.@#-]+$") 72 | passwordRe = regexp.MustCompile("^[a-zA-Z0-9]+$") 73 | ) 74 | 75 | var ValidRequestType = map[string]bool{"data": true, "handler": true, "stream": true, "auth": true, "html": true, "init": true, "path": true} 76 | var ValidActionType = map[string]bool{"setdata": true, "event": true, "invalidate": true, "html": true, "panelauth": true, "panelauthchallenge": true, "error": true, "blob": true, "blobext": true, "streamopen": true, "backendpush": true, "navto": true} 77 | var ValidFileType = map[string]bool{"static": true, "dir": true, "rt-link": true, "rt-app-link": true, "app": true} 78 | var ValidRequestMethod = map[string]bool{"GET": true, "POST": true} 79 | 80 | func IsZoneNameValid(zoneName string) bool { 81 | if len(zoneName) > ZoneNameMax { 82 | return false 83 | } 84 | return zoneNameRe.MatchString(zoneName) 85 | } 86 | 87 | func IsPageNameValid(pageName string) bool { 88 | if len(pageName) > PageNameMax { 89 | return false 90 | } 91 | return pageNameRe.MatchString(pageName) 92 | } 93 | 94 | func IsZoneAccessValid(zoneAccess string) bool { 95 | if len(zoneAccess) > ZoneAccessMax { 96 | return false 97 | } 98 | return zoneAccessRe.MatchString(zoneAccess) 99 | } 100 | 101 | func IsAppNameValid(appName string) bool { 102 | if len(appName) > AppNameMax { 103 | return false 104 | } 105 | return appNameRe.MatchString(appName) 106 | } 107 | 108 | func IsSimpleFileNameValid(fileName string) bool { 109 | if len(fileName) > FileNameMax { 110 | return false 111 | } 112 | return simpleFileNameRe.MatchString(fileName) 113 | } 114 | 115 | func IsControlNameValid(controlName string) bool { 116 | if len(controlName) > ControlNameMax { 117 | return false 118 | } 119 | return controlNameRe.MatchString(controlName) 120 | } 121 | 122 | func IsProcNameValid(procName string) bool { 123 | if len(procName) > ProcNameMax { 124 | return false 125 | } 126 | return procNameRe.MatchString(procName) 127 | } 128 | 129 | func IsProcIKeyValid(procIKey string) bool { 130 | if len(procIKey) > ProcIKeyMax { 131 | return false 132 | } 133 | return procIKeyRe.MatchString(procIKey) 134 | } 135 | 136 | func IsUUIDValid(uuid string) bool { 137 | if len(uuid) != UuidLen { 138 | return false 139 | } 140 | return uuidRe.MatchString(uuid) 141 | } 142 | 143 | func IsHandlerPathValid(handler string) bool { 144 | if len(handler) > HandlerPathMax { 145 | return false 146 | } 147 | return handlerPathRe.MatchString(handler) 148 | } 149 | 150 | func IsPathValid(path string) bool { 151 | if len(path) > PathMax { 152 | return false 153 | } 154 | return pathRe.MatchString(path) 155 | } 156 | 157 | func IsFullPathValid(path string) bool { 158 | err := ValidateFullPath(path, true) 159 | return err == nil 160 | } 161 | 162 | func IsPathFragValid(pathFrag string) bool { 163 | if len(pathFrag) > PathFragMax { 164 | return false 165 | } 166 | return pathFragRe.MatchString(pathFrag) 167 | } 168 | 169 | func IsPublicKeyValid(publicKey string) bool { 170 | if len(publicKey) < 20 || len(publicKey) > 1000 { 171 | return false 172 | } 173 | return base64Re.MatchString(publicKey) 174 | } 175 | 176 | func IsSha256HexHashValid(s string) bool { 177 | if len(s) != Sha256HexLen { 178 | return false 179 | } 180 | return hexRe.MatchString(s) 181 | } 182 | 183 | func IsSha256Base64HashValid(s string) bool { 184 | if len(s) != Sha256Base64Len { 185 | return false 186 | } 187 | return base64Re.MatchString(s) 188 | } 189 | 190 | func IsMimeTypeValid(s string) bool { 191 | if len(s) == 0 || len(s) > MimeTypeMax { 192 | return false 193 | } 194 | return mimeTypeRe.MatchString(s) 195 | } 196 | 197 | func IsImageMimeTypeValid(s string) bool { 198 | if len(s) == 0 || len(s) > MimeTypeMax { 199 | return false 200 | } 201 | return imageMimeTypeRe.MatchString(s) 202 | } 203 | 204 | func IsEmailValid(s string) bool { 205 | if len(s) == 0 || len(s) > EmailMax { 206 | return false 207 | } 208 | return emailRe.MatchString(s) 209 | } 210 | 211 | func IsPasswordValid(s string) bool { 212 | if len(s) == 0 || len(s) > PasswordMax { 213 | return false 214 | } 215 | if len(s) < PasswordMin { 216 | return false 217 | } 218 | return true 219 | } 220 | 221 | func IsRequestTypeValid(s string) bool { 222 | return ValidRequestType[s] 223 | } 224 | 225 | func IsActionTypeValid(s string) bool { 226 | return ValidActionType[s] 227 | } 228 | 229 | func IsTagValid(s string) bool { 230 | if len(s) == 0 || len(s) > TagMax { 231 | return false 232 | } 233 | return tagRe.MatchString(s) 234 | } 235 | 236 | func IsRoleValid(s string) bool { 237 | if len(s) == 0 || len(s) > RoleMax { 238 | return false 239 | } 240 | return roleRe.MatchString(s) 241 | } 242 | 243 | func IsClientVersionValid(s string) bool { 244 | if len(s) == 0 || len(s) > ClientVersionMax { 245 | return false 246 | } 247 | return clientVersionRe.MatchString(s) 248 | } 249 | 250 | func IsSimpleIdValid(s string) bool { 251 | if len(s) == 0 || len(s) > SimpleIdMax { 252 | return false 253 | } 254 | return simpleIdRe.MatchString(s) 255 | } 256 | 257 | func IsRoleListValid(s string) bool { 258 | if len(s) == 0 || len(s) > RoleListMax { 259 | return false 260 | } 261 | list := strings.Split(s, ",") 262 | for _, role := range list { 263 | if !IsRoleValid(role) { 264 | return false 265 | } 266 | } 267 | return true 268 | } 269 | 270 | func IsUserIdValid(s string) bool { 271 | if len(s) == 0 || len(s) > UserIdMax { 272 | return false 273 | } 274 | return userIdRe.MatchString(s) 275 | } 276 | 277 | func IsFileTypeValid(s string) bool { 278 | return ValidFileType[s] 279 | } 280 | 281 | func IsFileDisplayValid(s string) bool { 282 | if IsPathValid(s) { 283 | return true 284 | } 285 | if len(s) == 0 || len(s) > FileDisplayMax { 286 | return false 287 | } 288 | return fileDisplayRe.MatchString(s) 289 | } 290 | 291 | func IsDescriptionValid(s string) bool { 292 | return len(s) < DescriptionMax 293 | } 294 | 295 | func IsRequestMethodValid(s string) bool { 296 | return ValidRequestMethod[s] 297 | } 298 | -------------------------------------------------------------------------------- /pkg/keygen/keygen.go: -------------------------------------------------------------------------------- 1 | // Utility functions for generating and reading public/private keypairs. 2 | package keygen 3 | 4 | import ( 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/base64" 11 | "encoding/pem" 12 | "fmt" 13 | "math/big" 14 | "os" 15 | "time" 16 | ) 17 | 18 | const p384Params = "BgUrgQQAIg==" 19 | 20 | // Creates a keypair with CN=[accId], private key at keyFileName, and 21 | // public key certificate at certFileName. 22 | func CreateKeyPair(keyFileName string, certFileName string, accId string) error { 23 | privateKey, err := CreatePrivateKey(keyFileName) 24 | if err != nil { 25 | return err 26 | } 27 | err = CreateCertificate(certFileName, privateKey, accId) 28 | if err != nil { 29 | return err 30 | } 31 | return nil 32 | } 33 | 34 | // Creates a private key at keyFileName (ECDSA, secp384r1 (P-384)), PEM format 35 | func CreatePrivateKey(keyFileName string) (*ecdsa.PrivateKey, error) { 36 | curve := elliptic.P384() // secp384r1 37 | privateKey, err := ecdsa.GenerateKey(curve, rand.Reader) 38 | if err != nil { 39 | return nil, fmt.Errorf("Error generating P-384 key err:%w", err) 40 | } 41 | keyFile, err := os.Create(keyFileName) 42 | if err != nil { 43 | return nil, fmt.Errorf("error opening file:%s err:%w", keyFileName, err) 44 | } 45 | defer keyFile.Close() 46 | pkBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) 47 | if err != nil { 48 | return nil, fmt.Errorf("Error MarshalPKCS8PrivateKey err:%w", err) 49 | } 50 | paramsBytes, err := base64.StdEncoding.DecodeString(p384Params) 51 | if err != nil { 52 | return nil, fmt.Errorf("Error decoding bytes for P-384 EC PARAMETERS err:%w", err) 53 | } 54 | var pemParamsBlock = &pem.Block{ 55 | Type: "EC PARAMETERS", 56 | Bytes: paramsBytes, 57 | } 58 | err = pem.Encode(keyFile, pemParamsBlock) 59 | if err != nil { 60 | return nil, fmt.Errorf("Error writing EC PARAMETERS pem block err:%w", err) 61 | } 62 | var pemPrivateBlock = &pem.Block{ 63 | Type: "EC PRIVATE KEY", 64 | Bytes: pkBytes, 65 | } 66 | err = pem.Encode(keyFile, pemPrivateBlock) 67 | if err != nil { 68 | return nil, fmt.Errorf("Error writing EC PRIVATE KEY pem block err:%w", err) 69 | } 70 | return privateKey, nil 71 | } 72 | 73 | // Creates a public key certificate at certFileName using privateKey with CN=[accId]. 74 | func CreateCertificate(certFileName string, privateKey *ecdsa.PrivateKey, accId string) error { 75 | serialNumber, err := rand.Int(rand.Reader, big.NewInt(1000000000000)) 76 | if err != nil { 77 | return fmt.Errorf("Cannot generate serial number err:%w", err) 78 | } 79 | notBefore, err := time.Parse("Jan 2 15:04:05 2006", "Jan 1 00:00:00 2020") 80 | if err != nil { 81 | return fmt.Errorf("Cannot Parse Date err:%w", err) 82 | } 83 | notAfter, err := time.Parse("Jan 2 15:04:05 2006", "Jan 1 00:00:00 2030") 84 | if err != nil { 85 | return fmt.Errorf("Cannot Parse Date err:%w", err) 86 | } 87 | template := x509.Certificate{ 88 | SerialNumber: serialNumber, 89 | Subject: pkix.Name{ 90 | CommonName: accId, 91 | }, 92 | NotBefore: notBefore, 93 | NotAfter: notAfter, 94 | KeyUsage: x509.KeyUsageDigitalSignature, 95 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 96 | BasicConstraintsValid: true, 97 | } 98 | certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) 99 | if err != nil { 100 | return fmt.Errorf("Error running x509.CreateCertificate err:%v\n", err) 101 | } 102 | certFile, err := os.Create(certFileName) 103 | if err != nil { 104 | return fmt.Errorf("Error opening file:%s err:%w", certFileName, err) 105 | } 106 | defer certFile.Close() 107 | err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) 108 | if err != nil { 109 | return fmt.Errorf("Error writing CERTIFICATE pem block err:%w", err) 110 | } 111 | return nil 112 | } 113 | --------------------------------------------------------------------------------