├── .github └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── archive ├── ChangeLog-0.0.1 ├── ChangeLog-0.0.2 ├── ChangeLog-1.0.0 ├── ChangeLog-1.0.1 ├── ChangeLog-1.0.2 ├── ChangeLog-1.0.3 ├── ChangeLog-1.0.4 ├── ChangeLog-1.0.5 ├── ChangeLog-1.0.6 └── ChangeLog-1.0.7 ├── csvhandler.go ├── docker └── Dockerfile ├── example.py ├── go.mod ├── go.sum ├── gobuild.py ├── handlers.go ├── handlersprod.go ├── handlerstest.go ├── siridb-http.conf ├── siridb-http.go ├── siridb-http.png ├── src ├── .eslintignore ├── .eslintrc.js ├── Actions │ ├── AppActions.js │ ├── AuthActions.js │ ├── DatabaseActions.js │ ├── InsertActions.js │ └── QueryActions.js ├── Components │ ├── App │ │ ├── App.js │ │ ├── InfoModal.js │ │ ├── PageDoesNotExist.js │ │ └── TopMenu.js │ ├── Auth │ │ └── Auth.js │ ├── Insert │ │ └── Insert.js │ ├── Query │ │ ├── AutoCompeteItem.js │ │ ├── AutoCompletePopup.js │ │ ├── ParseError.js │ │ ├── Query.js │ │ └── Result │ │ │ ├── QueryGroupLnk.js │ │ │ ├── QuerySeriesLnk.js │ │ │ ├── QueryTagLnk.js │ │ │ ├── Result.js │ │ │ ├── Series.js │ │ │ └── Table.js │ └── index.js ├── Stores │ ├── AppStore.js │ ├── AuthStore.js │ ├── BaseStore.js │ ├── DatabaseStore.js │ ├── InsertStore.js │ └── QueryStore.js ├── Utils │ ├── Chart.js │ ├── JsonRequest.js │ └── SiriGrammar.js ├── index.html ├── layout.less ├── package-lock.json ├── package.json ├── waiting.html └── webpack.config.js └── static ├── css ├── bootstrap.css ├── bootstrap.css.map ├── bootstrap.min.css ├── font-awesome.css └── font-awesome.min.css ├── favicon.ico ├── fonts ├── FontAwesome.otf ├── fontawesome-webfont.eot ├── fontawesome-webfont.svg ├── fontawesome-webfont.ttf ├── fontawesome-webfont.woff └── fontawesome-webfont.woff2 ├── img ├── loader.gif ├── siridb-large.png ├── siridb-medium.png └── siridb-small.png └── js └── libs └── less.min.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI Node.js" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: 'src' 18 | 19 | strategy: 20 | matrix: 21 | node-version: [16.x] 22 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | cache: 'npm' 31 | cache-dependency-path: '**/package-lock.json' 32 | - name: Install dependencies 33 | run: | 34 | npm install 35 | - name: Run lint 36 | run: | 37 | npm run lint 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .coverage.* 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | *,cover 44 | .hypothesis/ 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | local_settings.py 53 | 54 | # Flask stuff: 55 | instance/ 56 | .webassets-cache 57 | 58 | # Scrapy stuff: 59 | .scrapy 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # PyBuilder 65 | target/ 66 | 67 | # IPython Notebook 68 | .ipynb_checkpoints 69 | 70 | # pyenv 71 | .python-version 72 | 73 | # celery beat schedule file 74 | celerybeat-schedule 75 | 76 | # dotenv 77 | .env 78 | 79 | # virtualenv 80 | venv/ 81 | ENV/ 82 | 83 | # Spyder project settings 84 | .spyderproject 85 | 86 | # Rope project settings 87 | .ropeproject 88 | 89 | 90 | # dummy scripts 91 | test.py 92 | 93 | test/ 94 | 95 | app.min.js 96 | style.min.css 97 | 98 | # Compiled React 99 | /src/node_modules/ 100 | 101 | # Compiled jsx 102 | static/js/bundle.js 103 | static/js/bundle.min.js 104 | 105 | # Compiled less 106 | static/css/layout.css 107 | static/css/layout.min.css 108 | 109 | # Secret 110 | .secret 111 | 112 | # dummy config 113 | dummy.conf 114 | 115 | # build 116 | siridb-http 117 | 118 | # Go generated files 119 | file*.go 120 | 121 | # Certificates 122 | certificates/ 123 | 124 | # Binaries 125 | bin/ 126 | *.exe 127 | *.bin 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SiriDB HTTP 2 | ![alt SiriDB HTTP](/siridb-http.png?raw=true) 3 | 4 | SiriDB HTTP provides a HTTP API and optional web interface for [SiriDB](https://github.com/SiriDB/siridb-server). 5 | 6 | > Note: Since version 2.0.0 SiriDB HTTP is written in Go. For the 1.x version in Python you should go to 7 | > this [release tag](https://github.com/SiriDB/siridb-http/tree/1.0.7). 8 | 9 | --------------------------------------- 10 | * [Features](#features) 11 | * [Installation](#installation) 12 | * [Pre-compiled](#pre-compiled) 13 | * [Compile from source](#compile-from-source) 14 | * [Configuration](#configuration) 15 | * [Autorun on startup](#autorun-on-startup) 16 | * [Multi server support](#multi-server-support) 17 | * [HTTP API](#http-api) 18 | * [Content Types](#content-types) 19 | * [Database info](#database-info) 20 | * [Authentication](#authentication) 21 | * [Session authentication](#session-authentication) 22 | * [Fetch](#fetch) 23 | * [Login](#login) 24 | * [Logout](#logout) 25 | * [Basic authentication](#basic-authentication) 26 | * [Query](#query) 27 | * [Insert](#insert) 28 | * [JSON, MsgPack, QPack](#insert-json) 29 | * [CSV](#insert-csv) 30 | * [List format](#list-format) 31 | * [Table format](#table-format) 32 | * [Socket.io](#socketio) 33 | * [Web Interface](#web-interface) 34 | * [SSL (HTTPS)](#ssl-https) 35 | --------------------------------------- 36 | 37 | ## Features 38 | - Optional Web interface for sending queries and inserting data 39 | - SSL (HTTPS) support 40 | - Optional Basic Authentication 41 | - Support for JSON, MsgPack, QPack and CSV 42 | - IPv6 support 43 | - Socket.io support 44 | 45 | 46 | ## Installation 47 | SiriDB HTTP 2.x can be compiled from source or, for most systems, you can simply download a pre-compiled binary. 48 | 49 | ### Pre-compiled 50 | Go to https://github.com/SiriDB/siridb-http/releases/latest and download the binary for your system. 51 | In this documentation we refer to the binary as `siridb-http`. On Linux/OSX it might be required to set the execution flag: 52 | ``` 53 | $ chmod +x siridb-http_X.Y.Z_OS_ARCH.bin 54 | ``` 55 | 56 | You might want to copy the binary to /usr/local/bin and create a symlink like this: 57 | ``` 58 | $ sudo cp siridb-http_X.Y.Z_OS_ARCH.bin /usr/local/bin/ 59 | $ sudo ln -s /usr/local/bin/siridb-http_X.Y.Z_OS_ARCH.bin /usr/local/bin/siridb-http 60 | ``` 61 | > Note: replace `X.Y.Z_OS_ARCH` with your binary, for example `2.0.0_linux_amd64` 62 | 63 | ### Compile from source 64 | > Before compiling from source make sure **go**, **npm** and **git** are installed and your [$GOPATH](https://github.com/golang/go/wiki/GOPATH) is set. 65 | 66 | Clone the project using git. (we assume git is installed) 67 | ``` 68 | git clone https://github.com/SiriDB/siridb-http 69 | ``` 70 | 71 | Make sure less is installed: 72 | ``` 73 | $ sudo npm install -g less less-plugin-clean-css 74 | ``` 75 | 76 | The gobuild.py script can be used to build the binary: 77 | ``` 78 | $ ./gobuild.py -i -l -w -b -p 79 | ``` 80 | 81 | Or, if you want the development version which uses original files from /build and /static instead of build-in files: 82 | ``` 83 | $ ./gobuild.py -i -l -w -b -d 84 | ``` 85 | 86 | ## Configuration 87 | For running SiriDB HTTP a configuration file is required and should be provided using the `-c` or `--config` argument. The easiest way to create a configuration file is to save the output from 88 | siridb-http to a file: 89 | 90 | > Note: you might want to switch to root and later create a service to automatically start SiriDB HTTP at startup. 91 | 92 | Switch to root or skip this step if you want to save the configuration file with your current user. 93 | ``` 94 | $ sudo su - 95 | ``` 96 | 97 | Save a template configuration file to for example ~/.siridb-http.conf. 98 | ``` 99 | $ siridb-http > ~/.siridb-http.conf 100 | ``` 101 | 102 | Now edit the file with you favorite editor and at least set the `user`, `password` and `dbname`. 103 | When the configuration is saved you can start the server using: 104 | ``` 105 | $ siridb-http -c ~/.siridb-http.conf 106 | ``` 107 | 108 | ### Autorun on startup 109 | Depending on your OS and subsystem you can create a service to start SiriDB HTTP. 110 | This is an example of how to do this using systemd which is currently the default for Ubuntu: 111 | 112 | First create the service file: (you might need to change the ExecStart line) 113 | ``` 114 | $ sudo cat > /lib/systemd/system/siridb-http.service < Note: when a string in CSV contains a comma (,) then the string must be wrapped between double quotes. 177 | > If double quotes are also required in a string, the double quote should be escaped with a second double quote. 178 | 179 | ### Database info 180 | With the `/db-info` URI, database and version information can be asked. 181 | ``` 182 | type: GET or POST 183 | uri: /db-info 184 | header: Content-Type: 'application/json' 185 | ``` 186 | 187 | Response: 188 | ```json 189 | { 190 | "dbname": "my_database-name", 191 | "timePrecision": "database time precision: s, ms, us or ns", 192 | "version": "SiriDB Server version, for example: 2.0.18", 193 | "httpServer": "SiriDB HTTP version, for example: 2.0.0" 194 | } 195 | ``` 196 | 197 | > Note that `version` does not guarantee that each SiriDB server in a cluster is running the same version. 198 | 199 | ### Authentication 200 | Authentication is required when `require_authentication` is set to `True` in the configuration file. When authentication is not required, the `/insert` and `/query` URIs can be used directly without any authentication as long as the user configured in the configuration file has privileges to perform the request. 201 | 202 | #### Session authentication 203 | SiriDB HTTP has session support and exposes the following URIs for handling session authentication: 204 | - /auth/fetch 205 | - /auth/login 206 | - /auth/logout 207 | 208 | > Note: in the examples below we use 'application/json' as Content-Type but the following alternatives 209 | > are also allowed: 'application/x-msgpack', 'application/x-qpack' and 'application/csv'. 210 | 211 | ##### Fetch 212 | Fetch can be used to retrieve the current session user. 213 | ``` 214 | type: GET or POST 215 | uri: /auth/fetch 216 | header: Content-Type: 'application/json' 217 | ``` 218 | The response contains the current user and a boolean value to indicate if authentication is required. In case no user is logged on and authentication is required, the value for `user` will be `null`. 219 | 220 | Example response: 221 | ```json 222 | { 223 | "user": "logged_on_username_or_null", 224 | "authRequired": true 225 | } 226 | ``` 227 | 228 | ##### Login 229 | Login can be used to authenticate and create a SiriDB database user. If the option `enable_multi_user` in the configuration file is set to `True`, any database user can be used. In case multi user support is turned off, the only allowed user is the one configured in the configuration file. 230 | 231 | ``` 232 | type: POST 233 | uri: /auth/login 234 | header: Content-Type: 'application/json' 235 | body: {"username": , "password": } 236 | ``` 237 | 238 | Success response: 239 | ```json 240 | { 241 | "user": "logged_on_username" 242 | } 243 | ``` 244 | 245 | In case authentication has failed, error code 422 will be returned and the body will contain an appropriate error message. 246 | 247 | ##### Logout 248 | When calling this uri the current session will be cleared. 249 | ``` 250 | type: GET or POST 251 | uri: /auth/logout 252 | header: Content-Type: 'application/json' 253 | ``` 254 | Response: 255 | ```json 256 | { 257 | "user": null 258 | } 259 | ``` 260 | 261 | #### Basic authentication 262 | As an alternative to session authentication it is possible to use basic authentication. To allow basic authentication the option `enable_basic_auth` must be set to `True` in the configuration file. 263 | 264 | Example Authorization header for username *iris* with password *siri*: 265 | ``` 266 | Authorization: Basic aXJpczpzaXJp 267 | ``` 268 | 269 | ### Query 270 | The `/query` POST handler can be used for querying SiriDB. SiriDB HTTP supports multiple formats that can be used by setting the `Content-Type` in the header. 271 | ``` 272 | type: POST 273 | uri: /query 274 | header: Content-Type: 'application/json' 275 | body: {"query": , "timeout": } 276 | ``` 277 | 278 | Example body: 279 | ```json 280 | { 281 | "query": "select mean(1h) => difference() from 'my-series'" 282 | } 283 | ``` 284 | 285 | ### Insert 286 | The `/insert` POST handler can be used for inserting data into SiriDB. The same content types as for queries are supported. Both MsgPack and QPack are similar to JSON except that the data is packed to a byte string. Therefore we only explain JSON and CSV data here. *(Note: in the examples below we use a second time-precision)* 287 | 288 | #### Insert JSON 289 | The preferred json layout is as following: (this is the layout which is returned by SiriDB on a select query) 290 | ```json 291 | { 292 | "my-series-01": [[1493126582, 4.2], ...], 293 | ... 294 | } 295 | ``` 296 | 297 | Optionally the following format can be used: 298 | ```json 299 | [ 300 | { 301 | "name": "my-series-01", 302 | "points": [[1493126582, 4.2], ...] 303 | }, 304 | ... 305 | ] 306 | ``` 307 | 308 | #### Insert CSV 309 | CSV data is allowed in two formats which we call the list and table format. 310 | 311 | ##### List format 312 | When using the list format, each row in the csv should contain a series name, timestamp and value. 313 | 314 | Example list: 315 | ```csv 316 | Series 001,1440138931,100 317 | Series 003,1440138931,8.0 318 | Series 001,1440140932,40 319 | Series 002,1440140932,9.3 320 | ``` 321 | ##### Table format 322 | A table format is more compact, especially if multiple series share points with equal timestamps. The csv should start with an empty field that is indicated with the first comma. 323 | 324 | Example table: 325 | ```csv 326 | ,Series 001,Series 002,Series 003 327 | 1440138931,100,,8.0 328 | 1440140932,40,9.3, 329 | ``` 330 | 331 | ## Socket.io 332 | SiriDB HTTP has socket.io support when `enable_socket_io` is set to `True` in the configuration file. 333 | 334 | For Socket.io the following events are available: 335 | - `db-info` 336 | - `auth fetch` 337 | - `auth login` 338 | - `auth logout` 339 | - `query` 340 | - `insert` 341 | 342 | The result for an event contains a status code and data object. The status codes are equal to HTTP status codes. For example the success code is 200. 343 | When the status code is anything other than 200, the data object will be a string representing the error message. 344 | 345 | Example of using Socket.io with HTML/JavaScript: 346 | ```html 347 | 348 | 349 | 350 | 351 | 352 | 373 | 374 | 375 | ``` 376 | 377 | ## Web interface 378 | SiriDB has an optional web interface that can be enabled by setting `enable_web` to `True`. This web interface will ask for user authentication if `enable_authentication` is set to `True`. Only the `user` that is configured in the configuration file is allowed to login unless `enable_multi_user` is set to `True`. 379 | 380 | The Web interface allows you to run queries and insert data using JSON format. 381 | 382 | ## SSL (HTTPS) 383 | SSL (HTTPS) support can be enabled by setting `enable_ssl` to `True`. When enabled the `crt_file` and `key_file` in section `[SSL]` must be set. 384 | 385 | -------------------------------------------------------------------------------- /archive/ChangeLog-0.0.1: -------------------------------------------------------------------------------- 1 | * Initial version 2 | -------------------------------------------------------------------------------- /archive/ChangeLog-0.0.2: -------------------------------------------------------------------------------- 1 | * Fixed loading HTTP Server from relative path. 2 | 3 | * Fixed pressing CTRL+break while providing credentials to SiriDB HTTP. 4 | -------------------------------------------------------------------------------- /archive/ChangeLog-1.0.0: -------------------------------------------------------------------------------- 1 | * Rebuild website using react framework. 2 | 3 | * Rebuild style sheet using less. 4 | 5 | * Enable optional Token and Session Authentication. 6 | 7 | * Using a configuration file. 8 | 9 | * Added SSL (HTTPS) which optionally can be enabled. 10 | 11 | * Wait for a SiriDB connection at startup when not yet available. 12 | 13 | * Keep command history in localstorage. 14 | 15 | * Added multi user login support. (enabled by default) 16 | -------------------------------------------------------------------------------- /archive/ChangeLog-1.0.1: -------------------------------------------------------------------------------- 1 | * Added timeit response -------------------------------------------------------------------------------- /archive/ChangeLog-1.0.2: -------------------------------------------------------------------------------- 1 | * Refactor init time. (Index and Query pages) 2 | 3 | * Fixed showing shard size in list statement. 4 | 5 | * Removed console log for debugging __timeit__ response. 6 | 7 | * Fixed handling csv response data. -------------------------------------------------------------------------------- /archive/ChangeLog-1.0.3: -------------------------------------------------------------------------------- 1 | * Add version information for the HTTP Server to the /db-info handler. 2 | 3 | * Update grammar with limit support. (issue #6) -------------------------------------------------------------------------------- /archive/ChangeLog-1.0.4: -------------------------------------------------------------------------------- 1 | * Fixed sending float values as float. (issue #7) -------------------------------------------------------------------------------- /archive/ChangeLog-1.0.5: -------------------------------------------------------------------------------- 1 | * Updated siridb-connector and qpack to the latest version. 2 | 3 | * Updated node packages. -------------------------------------------------------------------------------- /archive/ChangeLog-1.0.6: -------------------------------------------------------------------------------- 1 | * Changed link location from /usr/sbin to /usr/local/bin in deb package. 2 | 3 | * Update grammar with list_limit and select_points_limit. (issue #9) -------------------------------------------------------------------------------- /archive/ChangeLog-1.0.7: -------------------------------------------------------------------------------- 1 | * Added IPv6 support. (issue #12) 2 | -------------------------------------------------------------------------------- /csvhandler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/csv" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func escapeCsv(s string) string { 13 | if strings.ContainsRune(s, '"') { 14 | return fmt.Sprintf("\"%s\"", strings.Replace(s, `"`, `""`, -1)) 15 | } 16 | if strings.ContainsRune(s, ',') { 17 | return fmt.Sprintf("\"%s\"", s) 18 | } 19 | return s 20 | } 21 | 22 | func toCsv(v interface{}) (string, error) { 23 | t := reflect.TypeOf(v) 24 | switch t.Kind() { 25 | case reflect.Struct: 26 | m := reflect.ValueOf(v) 27 | n := m.NumField() 28 | lines := make([]string, n) 29 | for i := 0; i < n; i++ { 30 | field := t.Field(i) 31 | fn := field.Tag.Get("csv") 32 | if len(fn) == 0 { 33 | fn = field.Name 34 | } 35 | 36 | val := m.Field(i) 37 | lines[i] = fmt.Sprintf("%s,%s", fn, val.String()) 38 | } 39 | return strings.Join(lines, "\n"), nil 40 | case reflect.Map: 41 | var lines []string 42 | if err := queryToCsv(&lines, v); err != nil { 43 | return "", err 44 | } 45 | return strings.Join(lines, "\n"), nil 46 | default: 47 | return "", fmt.Errorf("unexpected data type: %s", t.Kind()) 48 | } 49 | } 50 | 51 | func queryToCsv(lines *[]string, v interface{}) error { 52 | m, ok := v.(map[string]interface{}) 53 | if !ok { 54 | return fmt.Errorf("got an unexpected map") 55 | } 56 | 57 | if err := tryTimeIt(lines, m); err != nil { 58 | return err 59 | } 60 | 61 | if stop, err := tryList(lines, m); stop { 62 | return err 63 | } 64 | 65 | if stop, err := tryCount(lines, m); stop { 66 | return err 67 | } 68 | 69 | if stop, err := tryShow(lines, m); stop { 70 | return err 71 | } 72 | 73 | if stop, err := tryMsg(lines, m); stop { 74 | return err 75 | } 76 | 77 | if stop, err := trySelect(lines, m); stop { 78 | return err 79 | } 80 | 81 | return fmt.Errorf("cannot convert query data to csv") 82 | } 83 | 84 | func tryTimeIt(lines *[]string, m map[string]interface{}) error { 85 | if timeit, ok := m["__timeit__"]; ok { 86 | *lines = append(*lines, "server name,query time") 87 | 88 | if reflect.TypeOf(timeit).Kind() != reflect.Slice { 89 | return fmt.Errorf("timeit: __timeit__ not a slice") 90 | } 91 | 92 | slice := reflect.ValueOf(timeit) 93 | n := slice.Len() 94 | for i := 0; i < n; i++ { 95 | res := slice.Index(i).Interface() 96 | resmap, ok := res.(map[string]interface{}) 97 | if !ok { 98 | return fmt.Errorf("timeit: no map") 99 | } 100 | server, ok := resmap["server"] 101 | if !ok { 102 | return fmt.Errorf("timeit: no server") 103 | } 104 | time, ok := resmap["time"] 105 | if !ok { 106 | return fmt.Errorf("timeit: no time") 107 | } 108 | *lines = append(*lines, fmt.Sprintf( 109 | "%s,%s", 110 | escapeCsv(fmt.Sprint(server)), 111 | escapeCsv(fmt.Sprint(time)))) 112 | } 113 | *lines = append(*lines, "") 114 | 115 | delete(m, "__timeit__") 116 | } 117 | return nil 118 | } 119 | 120 | func trySelect(lines *[]string, m map[string]interface{}) (bool, error) { 121 | for sn, pnts := range m { 122 | 123 | if reflect.TypeOf(pnts).Kind() != reflect.Slice { 124 | return false, fmt.Errorf("select: points not a slice") 125 | } 126 | 127 | points := reflect.ValueOf(pnts) 128 | n := points.Len() 129 | for i := 0; i < n; i++ { 130 | pnt := points.Index(i).Interface() 131 | if reflect.TypeOf(pnt).Kind() != reflect.Slice { 132 | return false, fmt.Errorf("select: point not a slice") 133 | } 134 | point := reflect.ValueOf(pnt) 135 | if point.Len() != 2 { 136 | return false, fmt.Errorf("select: point should be of len 2") 137 | } 138 | *lines = append(*lines, fmt.Sprintf( 139 | `%s,%v,%s`, 140 | escapeCsv(sn), 141 | point.Index(0), 142 | escapeCsv(fmt.Sprint(point.Index(1))))) 143 | } 144 | } 145 | return true, nil 146 | } 147 | 148 | func tryMsg(lines *[]string, m map[string]interface{}) (bool, error) { 149 | options := [4]string{ 150 | "success_msg", 151 | "error_msg", 152 | "help", 153 | "motd"} 154 | for _, option := range options { 155 | if message, ok := m[option]; ok { 156 | msg, ok := message.(string) 157 | if ok { 158 | *lines = append(*lines, fmt.Sprintf(`%s,%s`, option, escapeCsv(msg))) 159 | return true, nil 160 | } 161 | } 162 | } 163 | return false, fmt.Errorf("no message found") 164 | } 165 | 166 | func tryShow(lines *[]string, m map[string]interface{}) (bool, error) { 167 | var data, name, value interface{} 168 | var item map[string]interface{} 169 | var ok bool 170 | 171 | if data, ok = m["data"]; !ok { 172 | return false, fmt.Errorf("show: data not found") 173 | } 174 | 175 | if reflect.TypeOf(data).Kind() != reflect.Slice { 176 | return false, fmt.Errorf("show: data not a slice") 177 | } 178 | 179 | slice := reflect.ValueOf(data) 180 | n := slice.Len() 181 | if n == 0 { 182 | return false, fmt.Errorf("zero data items found") 183 | } 184 | 185 | var temp = make([]string, n) 186 | for i := 0; i < n; i++ { 187 | v := slice.Index(i).Interface() 188 | if item, ok = v.(map[string]interface{}); !ok { 189 | return false, fmt.Errorf("show: data contains a type other than map") 190 | } 191 | if name, ok = item["name"]; !ok { 192 | return false, fmt.Errorf("show: name not fount in item") 193 | } 194 | 195 | if value, ok = item["value"]; !ok { 196 | return false, fmt.Errorf("show: value not fount in item") 197 | } 198 | 199 | temp[i] = fmt.Sprintf(`%v,%s`, name, escapeCsv(fmt.Sprint(value))) 200 | } 201 | for i := 0; i < n; i++ { 202 | *lines = append(*lines, temp[i]) 203 | } 204 | return true, nil 205 | } 206 | 207 | func tryCount(lines *[]string, m map[string]interface{}) (bool, error) { 208 | cols := [12]string{ 209 | "calc", 210 | "series", 211 | "servers", 212 | "groups", 213 | "shards", 214 | "pools", 215 | "tags", 216 | "users", 217 | "servers_received_points", 218 | "servers_selected_points", 219 | "series_length", 220 | "shards_size"} 221 | for _, col := range cols { 222 | if count, ok := m[col]; ok { 223 | i, ok := count.(int) 224 | if ok { 225 | *lines = append(*lines, fmt.Sprintf(`%s,%d`, col, i)) 226 | return true, nil 227 | } 228 | } 229 | } 230 | return false, fmt.Errorf("no counter key found") 231 | } 232 | 233 | func tryList(lines *[]string, m map[string]interface{}) (bool, error) { 234 | var columns interface{} 235 | var ok bool 236 | if columns, ok = m["columns"]; !ok { 237 | return false, fmt.Errorf("columns not found") 238 | } 239 | 240 | if reflect.TypeOf(columns).Kind() != reflect.Slice { 241 | return false, fmt.Errorf("columns not a slice") 242 | } 243 | 244 | slice := reflect.ValueOf(columns) 245 | n := slice.Len() 246 | if n == 0 { 247 | return false, fmt.Errorf("zero columns found") 248 | } 249 | 250 | var temp = make([]string, n) 251 | for i := 0; i < n; i++ { 252 | v := slice.Index(i).Interface() 253 | if s, ok := v.(string); ok { 254 | temp[i] = escapeCsv(s) 255 | } else { 256 | return false, fmt.Errorf("columns contains non string") 257 | } 258 | } 259 | *lines = append(*lines, strings.Join(temp, ",")) 260 | 261 | delete(m, "columns") 262 | 263 | for k, data := range m { 264 | if reflect.TypeOf(data).Kind() != reflect.Slice { 265 | return true, fmt.Errorf("%s not a slice", k) 266 | } 267 | rows := reflect.ValueOf(data) 268 | nrows := rows.Len() 269 | for r := 0; r < nrows; r++ { 270 | row := rows.Index(r).Interface() 271 | 272 | if reflect.TypeOf(row).Kind() != reflect.Slice { 273 | return true, fmt.Errorf("row not a slice") 274 | } 275 | cols := reflect.ValueOf(row) 276 | 277 | ncols := cols.Len() 278 | if n != ncols { 279 | return true, fmt.Errorf("number of columns does not equel values") 280 | } 281 | var temp = make([]string, n) 282 | for i := 0; i < ncols; i++ { 283 | temp[i] = escapeCsv(fmt.Sprint(cols.Index(i).Interface())) 284 | } 285 | *lines = append(*lines, strings.Join(temp, ",")) 286 | } 287 | } 288 | return true, nil 289 | } 290 | 291 | func parseCsv(r io.Reader) (map[string]interface{}, error) { 292 | 293 | data := make(map[string]interface{}) 294 | reader := csv.NewReader(r) 295 | 296 | record, err := reader.Read() 297 | if err == io.EOF { 298 | return nil, fmt.Errorf("no csv data found") 299 | } 300 | if err != nil { 301 | return nil, err 302 | } 303 | if record[0] == "" { 304 | err = readTable(&data, record, reader) 305 | } else if len(record) == 3 { 306 | err = readFlat(&data, record, reader) 307 | } else if len(record) == 2 { 308 | err = readAPI(&data, record, reader) 309 | } else { 310 | err = fmt.Errorf("unknown csv layout received") 311 | } 312 | return data, err 313 | } 314 | 315 | func parseCsvVal(inp string) interface{} { 316 | if i, err := strconv.Atoi(inp); err == nil { 317 | return i 318 | } 319 | if f, err := strconv.ParseFloat(inp, 64); err == nil { 320 | return f 321 | } 322 | return inp 323 | } 324 | 325 | func readAPI(data *map[string]interface{}, record []string, reader *csv.Reader) error { 326 | if err := appendAPIRecord(data, record); err != nil { 327 | return err 328 | } 329 | for { 330 | record, err := reader.Read() 331 | if err == io.EOF { 332 | break 333 | } 334 | if err != nil { 335 | return err 336 | } 337 | if err := appendAPIRecord(data, record); err != nil { 338 | return err 339 | } 340 | } 341 | return nil 342 | } 343 | 344 | func appendAPIRecord(data *map[string]interface{}, record []string) error { 345 | if val, ok := (*data)[record[0]]; ok { 346 | return fmt.Errorf("duplicated value for '%s'", val) 347 | } 348 | (*data)[record[0]] = parseCsvVal(record[1]) 349 | return nil 350 | } 351 | 352 | func readTable(data *map[string]interface{}, record []string, reader *csv.Reader) error { 353 | if len(record) < 2 { 354 | return fmt.Errorf("missing series in csv table") 355 | } 356 | 357 | arr := make([][][2]interface{}, len(record)-1) 358 | 359 | for n := 1; n < len(record); n++ { 360 | (*data)[record[n]] = &arr[n-1] 361 | } 362 | for n := 2; ; n++ { 363 | record, err := reader.Read() 364 | if err == io.EOF { 365 | break 366 | } 367 | if err != nil { 368 | return err 369 | } 370 | ts, err := strconv.ParseUint(record[0], 10, 64) 371 | if err != nil { 372 | return fmt.Errorf("expecting a time-stamp in column zero at line %d", n) 373 | } 374 | for i := 1; i < len(record); i++ { 375 | arr[i-1] = append(arr[i-1], [2]interface{}{ts, parseCsvVal(record[i])}) 376 | } 377 | } 378 | return nil 379 | } 380 | 381 | func readFlat(data *map[string]interface{}, record []string, reader *csv.Reader) error { 382 | appendFlatRecord(data, record, 1) 383 | for n := 2; ; n++ { 384 | record, err := reader.Read() 385 | if err == io.EOF { 386 | break 387 | } 388 | if err != nil { 389 | return err 390 | } 391 | if err := appendFlatRecord(data, record, n); err != nil { 392 | return err 393 | } 394 | 395 | } 396 | return nil 397 | } 398 | 399 | func appendFlatRecord(data *map[string]interface{}, record []string, n int) error { 400 | var points *[][2]interface{} 401 | p, ok := (*data)[record[0]] 402 | if ok { 403 | points = p.(*[][2]interface{}) 404 | } else { 405 | newPoints := make([][2]interface{}, 0) 406 | (*data)[record[0]] = &newPoints 407 | points = &newPoints 408 | } 409 | ts, err := strconv.ParseUint(record[1], 10, 64) 410 | if err != nil { 411 | return fmt.Errorf("expecting a time-stamp in column one at line %d", n) 412 | } 413 | *points = append(*points, [2]interface{}{ts, parseCsvVal(record[2])}) 414 | return nil 415 | } 416 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine 2 | RUN apk update && \ 3 | apk upgrade && \ 4 | apk add git python3 nodejs-npm && \ 5 | npm install -g less less-plugin-clean-css && \ 6 | git clone https://github.com/transceptor-technology/siridb-http.git /tmp/siridb-http && \ 7 | cd /tmp/siridb-http && ./gobuild.py -i -l -w -b -p -o siridb-http 8 | 9 | FROM alpine:latest 10 | COPY --from=0 /tmp/siridb-http/siridb-http /usr/local/bin/ 11 | # Client connections 12 | EXPOSE 5050 13 | ENTRYPOINT ["/usr/local/bin/siridb-http"] 14 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import asyncio 3 | import aiohttp 4 | import time 5 | import json 6 | import logging 7 | import argparse 8 | import msgpack 9 | import qpack 10 | 11 | 12 | class Auth: 13 | 14 | def __init__(self, secret, url, only_secret=False): 15 | self.url = url 16 | self._secret = secret 17 | self._token = None 18 | self._refresh_ts = None 19 | self._refresh_token = None 20 | self._only_secret = only_secret 21 | 22 | async def get_header(self, content_type='application/json'): 23 | if not self._secret: 24 | return { 25 | 'Content-Type': content_type 26 | } 27 | if self._only_secret: 28 | return { 29 | 'Authorization': 'Secret {}'.format(self._secret), 30 | 'Content-Type': content_type 31 | } 32 | if self._token is None: 33 | await self._get_token() 34 | elif time.time() > self._refresh_ts: 35 | await self._refresh() 36 | return { 37 | 'Authorization': 'Token {}'.format(self._token), 38 | 'Content-Type': content_type 39 | } 40 | 41 | def _update(self, content): 42 | self._refresh_token = content['refresh_token'] 43 | self._refresh_ts = \ 44 | int(time.time()) + content['expires_in'] // 2 45 | self._token = content['token'] 46 | 47 | async def _get_token(self): 48 | headers = { 49 | 'Authorization': 'Secret {}'.format(self._secret), 50 | 'Content-Type': 'application/json' 51 | } 52 | async with aiohttp.ClientSession() as session: 53 | async with session.post( 54 | '{}/get-token'.format(self.url), 55 | headers=headers) as resp: 56 | if resp.status == 200: 57 | self._update(await resp.json()) 58 | else: 59 | logging.error('Error getting token: {}'.format(resp.status)) 60 | 61 | async def _refresh(self): 62 | headers = { 63 | 'Authorization': 'Refresh {}'.format(self._refresh_token), 64 | 'Content-Type': 'application/json' 65 | } 66 | async with aiohttp.ClientSession() as session: 67 | async with session.post( 68 | '{}/refresh-token'.format(self.url), 69 | headers=headers) as resp: 70 | if resp.status == 200: 71 | self._update(await resp.json()) 72 | else: 73 | logging.error( 74 | 'Error getting token: {}' 75 | .format(resp.status)) 76 | 77 | 78 | async def _query(auth, data, headers): 79 | async with aiohttp.ClientSession() as session: 80 | async with session.post( 81 | '{}/query'.format(auth.url), 82 | data=data, 83 | headers=headers) as resp: 84 | status = resp.status 85 | res = await resp.read() 86 | 87 | return res, status 88 | 89 | 90 | async def query_json(auth, q): 91 | data = {'query': q} 92 | headers = await auth.get_header() 93 | res, status = await _query(auth, json.dumps(data), headers) 94 | return json.loads(res.decode('utf-8')), status 95 | 96 | 97 | async def query_csv(auth, q): 98 | data = '"query","{}"'.format(q.replace('"', '""')) 99 | headers = await auth.get_header(content_type='application/csv') 100 | res, status = await _query(auth, data, headers) 101 | return res.decode('utf-8'), status 102 | 103 | 104 | async def query_msgpack(auth, q): 105 | data = {'query': q} 106 | headers = await auth.get_header(content_type='application/x-msgpack') 107 | res, status = await _query(auth, msgpack.packb(data), headers) 108 | return msgpack.unpackb(res, encoding='utf-8'), status 109 | 110 | 111 | async def query_qpack(auth, q): 112 | data = {'query': q} 113 | headers = await auth.get_header(content_type='application/x-qpack') 114 | res, status = await _query(auth, qpack.packb(data), headers) 115 | return qpack.unpackb(res, decode='utf-8'), status 116 | 117 | 118 | async def example_show(args, auth, method='json'): 119 | methods = { 120 | 'json': query_json, 121 | 'msgpack': query_msgpack, 122 | 'qpack': query_qpack 123 | } 124 | res, status = await methods[method](auth, 'show') 125 | if status == 200: 126 | for item in res['data']: 127 | print('{name:.<20}: {value}'.format(**item)) 128 | else: 129 | print('Error: {}'.format(res.get('error_msg', status))) 130 | 131 | 132 | async def example_query(args, auth): 133 | res, status = await query_csv(auth, args.query) 134 | print(res) 135 | 136 | 137 | if __name__ == '__main__': 138 | parser = argparse.ArgumentParser() 139 | 140 | parser.add_argument( 141 | '-u', 142 | '--url', 143 | type=str, 144 | default='http://localhost:8080', 145 | help='SiriDB HTTP url') 146 | 147 | parser.add_argument( 148 | '-s', 149 | '--secret', 150 | type=str, 151 | default='', 152 | help='Authenticate using a secret') 153 | 154 | parser.add_argument( 155 | '-o', '--only-secret', 156 | action='store_true', 157 | help='Only authenticate using the secret. ' + 158 | '(can only be used if a token is not required)') 159 | 160 | parser.add_argument( 161 | '-q', 162 | '--query', 163 | type=str, 164 | default='', 165 | help='Send a query, output is parsed as csv') 166 | 167 | args = parser.parse_args() 168 | loop = asyncio.get_event_loop() 169 | auth = Auth(args.secret, args.url, args.only_secret) 170 | if args.query: 171 | loop.run_until_complete(example_query(args, auth)) 172 | else: 173 | loop.run_until_complete(example_show(args, auth)) 174 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module siridb-http 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/SiriDB/go-siridb-connector v1.0.14 7 | github.com/alecthomas/kingpin/v2 v2.3.1 8 | github.com/astaxie/beego v1.12.3 9 | github.com/googollee/go-socket.io v1.7.0 10 | github.com/transceptor-technology/go-qpack v1.0.3 11 | gopkg.in/ini.v1 v1.67.0 12 | gopkg.in/vmihailenco/msgpack.v2 v2.9.2 13 | ) 14 | 15 | require ( 16 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 17 | github.com/gofrs/uuid v4.4.0+incompatible // indirect 18 | github.com/golang/protobuf v1.5.2 // indirect 19 | github.com/gomodule/redigo v2.0.0+incompatible // indirect 20 | github.com/gorilla/websocket v1.5.0 // indirect 21 | github.com/xhit/go-str2duration v1.2.0 // indirect 22 | golang.org/x/net v0.23.0 // indirect 23 | google.golang.org/appengine v1.6.7 // indirect 24 | google.golang.org/protobuf v1.33.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 3 | github.com/SiriDB/go-siridb-connector v1.0.14 h1:e6c2nmwQCBDSeXoeHUNIeSjyOTXculh7uzYZsDlHk+k= 4 | github.com/SiriDB/go-siridb-connector v1.0.14/go.mod h1:htsX/ffU3ZCTnyfInRW1xvumK7565QFuJ+unMAgkH9Y= 5 | github.com/alecthomas/kingpin/v2 v2.3.1 h1:ANLJcKmQm4nIaog7xdr/id6FM6zm5hHnfZrvtKPxqGg= 6 | github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE= 7 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 8 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 9 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 10 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 11 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 12 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 13 | github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= 14 | github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= 15 | github.com/astaxie/beego v1.12.3 h1:SAQkdD2ePye+v8Gn1r4X6IKZM1wd28EyUOVQ3PDSOOQ= 16 | github.com/astaxie/beego v1.12.3/go.mod h1:p3qIm0Ryx7zeBHLljmd7omloyca1s4yu1a8kM1FkpIA= 17 | github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ= 18 | github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU= 19 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 20 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 21 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 22 | github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= 23 | github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE= 24 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 25 | github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= 26 | github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U= 27 | github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c= 28 | github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= 29 | github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY= 30 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 32 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 33 | github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 34 | github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= 35 | github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= 36 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 37 | github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw= 38 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 39 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 40 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 41 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 42 | github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 43 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 44 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 45 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 46 | github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= 47 | github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 48 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 49 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 50 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 51 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 52 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 53 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 54 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 55 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 56 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 57 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 58 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 59 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 60 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 61 | github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 62 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 63 | github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= 64 | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= 65 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 66 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 67 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 68 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 69 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 70 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 71 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 72 | github.com/googollee/go-socket.io v1.7.0 h1:ODcQSAvVIPvKozXtUGuJDV3pLwdpBLDs1Uoq/QHIlY8= 73 | github.com/googollee/go-socket.io v1.7.0/go.mod h1:0vGP8/dXR9SZUMMD4+xxaGo/lohOw3YWMh2WRiWeKxg= 74 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 75 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 76 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 77 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 78 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 79 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 80 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 81 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 82 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 83 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 84 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 85 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 86 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 87 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 88 | github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ= 89 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 90 | github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 91 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 92 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 93 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 94 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 95 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 96 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 97 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 98 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 99 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 100 | github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= 101 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 102 | github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 103 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 104 | github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= 105 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 106 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 107 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 108 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 109 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 110 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 111 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 112 | github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 113 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 114 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 115 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 116 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 117 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 118 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 119 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 120 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 121 | github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= 122 | github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= 123 | github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s= 124 | github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA= 125 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 126 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 127 | github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE= 128 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 129 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 130 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 131 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 132 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 133 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 134 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 135 | github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= 136 | github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= 137 | github.com/transceptor-technology/go-qpack v0.0.0-20190116123619-49a14b216a45/go.mod h1:7QhRKvAhSRfXDqhw+JG0vw3o7igpbPDGka/q1yQwo6o= 138 | github.com/transceptor-technology/go-qpack v1.0.3 h1:A8ZLxhs1C+YZEYGK1jqgPTl9Kb6IjT06nFDdhPSPBOI= 139 | github.com/transceptor-technology/go-qpack v1.0.3/go.mod h1:7QhRKvAhSRfXDqhw+JG0vw3o7igpbPDGka/q1yQwo6o= 140 | github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= 141 | github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc= 142 | github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc= 143 | github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4= 144 | github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU= 145 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 146 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 147 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 148 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 149 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 150 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 151 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 152 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 153 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 154 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 155 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 156 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 157 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 158 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 159 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 160 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 161 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 162 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 163 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 164 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 165 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 166 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 169 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 170 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 171 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 172 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 173 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 174 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 175 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 176 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 177 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 178 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 179 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 180 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 181 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 182 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 183 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 184 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 185 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 186 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 187 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 188 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 189 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 190 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 191 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 192 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 193 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 194 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 195 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 196 | gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= 197 | gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= 198 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 199 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 200 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 201 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 202 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 203 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 204 | -------------------------------------------------------------------------------- /gobuild.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | import sys 4 | import argparse 5 | import subprocess 6 | import base64 7 | 8 | 9 | template = '''// +build !debug 10 | 11 | package {package} 12 | 13 | import "encoding/base64" 14 | 15 | // {variable} is a byte representation for {fn} 16 | var {variable}, _ = base64.StdEncoding.DecodeString("{base64str}") 17 | ''' 18 | 19 | 20 | goosarchs = [ 21 | ('darwin', '386'), 22 | ('darwin', 'amd64'), 23 | # # ('darwin', 'arm'), // not compiling 24 | # # ('darwin', 'arm64'), // not compiling 25 | # ('dragonfly', 'amd64'), 26 | ('freebsd', '386'), 27 | ('freebsd', 'amd64'), 28 | ('freebsd', 'arm'), 29 | ('linux', '386'), 30 | ('linux', 'amd64'), 31 | ('linux', 'arm'), 32 | ('linux', 'arm64'), 33 | # ('linux', 'ppc64'), 34 | # ('linux', 'ppc64le'), 35 | # ('linux', 'mips'), 36 | # ('linux', 'mipsle'), 37 | # ('linux', 'mips64'), 38 | # ('linux', 'mips64le'), 39 | # ('netbsd', '386'), 40 | # ('netbsd', 'amd64'), 41 | # ('netbsd', 'arm'), 42 | # ('openbsd', '386'), 43 | # ('openbsd', 'amd64'), 44 | # ('openbsd', 'arm'), 45 | # ('plan9', '386'), 46 | # ('plan9', 'amd64'), 47 | # # ('solaris', 'amd64'), // not compiling 48 | ('windows', '386'), 49 | ('windows', 'amd64'), 50 | ] 51 | 52 | binfiles = [ 53 | ("./static/css/bootstrap.min.css", "FileBootstrapMinCSS"), 54 | ("./static/css/font-awesome.min.css", "FileFontAwesomeMinCSS"), 55 | ("./static/img/siridb-large.png", "FileSiriDBLargePNG"), 56 | ("./static/img/siridb-small.png", "FileSiriDBSmallPNG"), 57 | ("./static/img/loader.gif", "FileLoaderGIF"), 58 | ("./static/fonts/FontAwesome.otf", "FileFontAwesomeOTF"), 59 | ("./static/fonts/fontawesome-webfont.eot", "FileFontawesomeWebfontEOT"), 60 | ("./static/fonts/fontawesome-webfont.svg", "FileFontawesomeWebfontSVG"), 61 | ("./static/fonts/fontawesome-webfont.ttf", "FileFontawesomeWebfontTTF"), 62 | ("./static/fonts/fontawesome-webfont.woff", "FileFontawesomeWebfontWOFF"), 63 | ("./static/fonts/fontawesome-webfont.woff2", 64 | "FileFontawesomeWebfontWOFF2"), 65 | ("./static/favicon.ico", "FileFaviconICO"), 66 | ("./src/index.html", "FileIndexHTML"), 67 | ("./src/waiting.html", "FileWaitingHTML"), 68 | ("./build/bundle.min.js", "FileBundleMinJS"), 69 | ("./build/layout.min.css", "FileLayoutMinCSS"), 70 | ] 71 | 72 | 73 | GOFILE = 'siridb-http.go' 74 | TARGET = 'siridb-http' 75 | 76 | 77 | def get_version(path): 78 | version = None 79 | with open(os.path.join(path, GOFILE), 'r') as f: 80 | for line in f: 81 | if line.startswith('const AppVersion ='): 82 | version = line.split('"')[1] 83 | if version is None: 84 | raise Exception('Cannot find version in {}'.format(GOFILE)) 85 | return version 86 | 87 | 88 | def build_all(): 89 | path = os.path.abspath(os.path.dirname(__file__)) 90 | version = get_version(path) 91 | outpath = os.path.join(path, 'bin', version) 92 | if not os.path.exists(outpath): 93 | os.makedirs(outpath) 94 | 95 | for goos, goarch in goosarchs: 96 | tmp_env = os.environ.copy() 97 | tmp_env["GOOS"] = goos 98 | tmp_env["GOARCH"] = goarch 99 | outfile = os.path.join(outpath, '{}_{}_{}_{}.{}'.format( 100 | TARGET, 101 | version, 102 | goos, 103 | goarch, 104 | 'exe' if goos == 'windows' else 'bin')) 105 | with subprocess.Popen( 106 | ['go', 'build', '-o', outfile], 107 | env=tmp_env, 108 | cwd=path, 109 | stdout=subprocess.PIPE) as proc: 110 | print('Building {}/{}...'.format(goos, goarch)) 111 | 112 | 113 | def build(development=True, output=''): 114 | path = os.path.abspath(os.path.dirname(__file__)) 115 | version = get_version(path) 116 | outfile = output if output else os.path.join(path, '{}_{}.{}'.format( 117 | TARGET, version, 'exe' if sys.platform.startswith('win') else 'bin')) 118 | args = ['go', 'build', '-o', outfile] 119 | 120 | if development: 121 | args.extend(['--tags', 'debug']) 122 | 123 | with subprocess.Popen( 124 | args, 125 | cwd=path, 126 | stdout=subprocess.PIPE) as proc: 127 | print('Building {}...'.format(outfile)) 128 | 129 | 130 | def install_packages(): 131 | path = os.path.abspath(os.path.dirname(__file__)) 132 | with subprocess.Popen( 133 | ['npm', 'install'], 134 | cwd=os.path.join(path, 'src'), 135 | stdout=subprocess.PIPE) as proc: 136 | print( 137 | 'Installing required npm packages and dependencies.\n' 138 | '(be patient, this can take some time)...') 139 | with subprocess.Popen( 140 | ['go', 'get', '-d'], 141 | cwd=path, 142 | stdout=subprocess.PIPE) as proc: 143 | print( 144 | 'Downloading required go packages and dependencies.\n' 145 | '(be patient, this can take some time)...') 146 | 147 | 148 | def webpack(development=True): 149 | print('(be patient, this can take some time)...') 150 | path = os.path.abspath(os.path.dirname(__file__)) 151 | env = os.environ 152 | if not development: 153 | env['NODE_ENV'] = 'production' 154 | with subprocess.Popen([ 155 | os.path.join('.', 'node_modules', '.bin', 'webpack'), 156 | ], 157 | env=env, 158 | cwd=os.path.join(path, 'src'), 159 | stdout=subprocess.PIPE) as proc: 160 | print(proc.stdout.read().decode('utf-8')) 161 | 162 | 163 | def compile_less(development=True): 164 | path = os.path.abspath(os.path.dirname(__file__)) 165 | if development: 166 | subprocess.run([ 167 | 'lessc', 168 | os.path.join(path, 'src', 'layout.less'), 169 | os.path.join(path, 'build', 'layout.css')]) 170 | else: 171 | subprocess.run([ 172 | 'lessc', 173 | '--clean-css', 174 | os.path.join(path, 'src', 'layout.less'), 175 | os.path.join(path, 'build', 'layout.min.css')]) 176 | 177 | 178 | def compile(fn, variable, empty=False): 179 | if empty: 180 | data = b'' 181 | else: 182 | with open(fn, 'rb') as f: 183 | data = f.read() 184 | with open('{}.go'.format(variable.lower()), 'w', encoding='utf-8') as f: 185 | f.write(template.format( 186 | package='main', 187 | fn=fn, 188 | variable=variable, 189 | base64str=base64.b64encode(data).decode('utf-8') 190 | )) 191 | 192 | 193 | if __name__ == '__main__': 194 | 195 | parser = argparse.ArgumentParser() 196 | 197 | parser.add_argument( 198 | '-i', '--install-packages', 199 | action='store_true', 200 | help='install required go and npm packages including dependencies') 201 | 202 | parser.add_argument( 203 | '-l', '--less', 204 | action='store_true', 205 | help='compile less (requires -d or -p)') 206 | 207 | parser.add_argument( 208 | '-w', '--webpack', 209 | action='store_true', 210 | help='compile webpack (requires -d or -p)') 211 | 212 | parser.add_argument( 213 | '-p', '--production-go', 214 | action='store_true', 215 | help='prepare go files for production') 216 | 217 | parser.add_argument( 218 | '-d', '--development-go', 219 | action='store_true', 220 | help='prepare placeholder go files for development') 221 | 222 | parser.add_argument( 223 | '-b', '--build', 224 | action='store_true', 225 | help='build binary (requires -d or -p)') 226 | 227 | parser.add_argument( 228 | '-o', '--output', 229 | default='', 230 | help='alternative output filename (requires -b/--build)') 231 | 232 | parser.add_argument( 233 | '-a', '--build-all', 234 | action='store_true', 235 | help='build production binaries for all goos and goarchs') 236 | 237 | args = parser.parse_args() 238 | 239 | if args.production_go and args.development_go: 240 | print('Cannot use -d and -p at the same time') 241 | sys.exit(1) 242 | 243 | if args.build and not args.production_go and not args.development_go: 244 | print('Cannot use -b without -d or -p') 245 | sys.exit(1) 246 | 247 | if args.output and not args.build: 248 | print('Cannot use -o/--output without -b/--build') 249 | sys.exit(1) 250 | 251 | if args.webpack and not args.production_go and not args.development_go: 252 | print('Cannot use -w without -d or -p') 253 | sys.exit(1) 254 | 255 | if args.less and not args.production_go and not args.development_go: 256 | print('Cannot use -l without -d or -p') 257 | sys.exit(1) 258 | 259 | if args.install_packages: 260 | install_packages() 261 | print('Finished installing required packages and dependencies!') 262 | 263 | if args.less: 264 | if args.production_go: 265 | print('Compiling production css...') 266 | compile_less(development=False) 267 | elif args.development_go: 268 | print('Compiling development css...') 269 | compile_less(development=True) 270 | else: 271 | sys.exit('-d or -p must be used') 272 | print('Finished compiling less!') 273 | 274 | if args.webpack: 275 | if args.production_go: 276 | print('Compiling production js using webpack...') 277 | webpack(development=False) 278 | elif args.development_go: 279 | print('Compiling development js using webpack...') 280 | webpack(development=True) 281 | else: 282 | sys.exit('-d or -p must be used') 283 | print('Finished compiling js using webpack...') 284 | 285 | if args.production_go: 286 | print('Create production go files...') 287 | for bf in binfiles: 288 | compile(*bf) 289 | print('Finished creating production go files!') 290 | 291 | if args.development_go: 292 | print('Create development go files...') 293 | for bf in binfiles: 294 | compile(*bf, empty=True) 295 | print('Finished creating development go files!') 296 | 297 | if args.build: 298 | if args.production_go: 299 | print('Build production binary') 300 | build(development=False, output=args.output) 301 | elif args.development_go: 302 | print('Build develpment binary') 303 | build(development=True, output=args.output) 304 | else: 305 | sys.exit('-d or -p must be used') 306 | print('Finished build!') 307 | 308 | if args.build_all: 309 | build_all() 310 | print('Finished building binaries!') 311 | 312 | if not any([ 313 | args.install_packages, 314 | args.production_go, 315 | args.development_go, 316 | args.less, 317 | args.webpack, 318 | args.build, 319 | args.build_all]): 320 | parser.print_usage() 321 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | 12 | siridb "github.com/SiriDB/go-siridb-connector" 13 | socketio "github.com/googollee/go-socket.io" 14 | qpack "github.com/transceptor-technology/go-qpack" 15 | msgpack "gopkg.in/vmihailenco/msgpack.v2" 16 | ) 17 | 18 | // StatusUnprocessableEntity is not available in Go 1.6 and older 19 | const StatusUnprocessableEntity int = 422 20 | 21 | type tDb struct { 22 | Dbname string `json:"dbname" qp:"dbname" msgpack:"dbname" csv:"dbname"` 23 | TimePrecision string `json:"timePrecision" qp:"timePrecision" msgpack:"timePrecision" csv:"timePrecision"` 24 | Version string `json:"version" qp:"version" msgpack:"version" csv:"version"` 25 | HTTPServer string `json:"httpServer" qp:"httpServer" msgpack:"httpServer" csv:"httpServer"` 26 | } 27 | 28 | type tAuthFetch struct { 29 | User interface{} `json:"user" qp:"user" msgpack:"user" csv:"user"` 30 | AuthRequired bool `json:"authRequired" qp:"authRequired" msgpack:"authRequired" csv:"authRequired"` 31 | } 32 | 33 | type tAuthLoginReq struct { 34 | Username string `json:"username" qp:"username" msgpack:"username" csv:"username"` 35 | Password string `json:"password" qp:"password" msgpack:"password" csv:"password"` 36 | } 37 | 38 | type tAuthLoginRes struct { 39 | User string `json:"user" qp:"user" msgpack:"user" msgpack:"user" csv:"user"` 40 | } 41 | 42 | type tAuthLogoff struct { 43 | User interface{} `json:"user" qp:"user" msgpack:"user" csv:"user"` 44 | } 45 | 46 | type tQuery struct { 47 | Query string `json:"query" qp:"query" msgpack:"query" csv:"query"` 48 | Timeout interface{} `json:"timeout" qp:"timeout" msgpack:"timeout" csv:"timeout"` 49 | } 50 | 51 | func checkBasicAuth(r *http.Request) (conn *Conn) { 52 | if !base.enableBasicAuth { 53 | return nil 54 | } 55 | username, password, ok := r.BasicAuth() 56 | if !ok { 57 | return nil 58 | } 59 | conn = getConnByUser(username) 60 | if conn != nil { 61 | if password == conn.password { 62 | return conn 63 | } 64 | return nil 65 | } 66 | if base.multiUser { 67 | conn, _ = addConnection(username, password) 68 | } 69 | return conn 70 | } 71 | 72 | func getConnBySIO(so *socketio.Conn) (conn *Conn, err error) { 73 | if user, ok := base.ssessions[(*so).ID()]; ok { 74 | conn = getConnByUser(user) 75 | if conn == nil { 76 | err = fmt.Errorf("no connection for user '%s' found, please try to login again", user) 77 | } 78 | } else if base.reqAuth { 79 | err = fmt.Errorf("not authenticated") 80 | } else { 81 | conn = &base.connections[0] 82 | } 83 | return conn, err 84 | } 85 | 86 | func getConnByHTTP(w http.ResponseWriter, r *http.Request) *Conn { 87 | var conn *Conn 88 | 89 | if conn = checkBasicAuth(r); conn != nil { 90 | return conn 91 | } 92 | sess, err := base.gsessions.SessionStart(w, r) 93 | 94 | if err != nil { 95 | sendError(w, err.Error(), http.StatusInternalServerError) 96 | } else { 97 | user, ok := sess.Get("user").(string) 98 | if ok { 99 | conn = getConnByUser(user) 100 | if conn == nil { 101 | sendError( 102 | w, 103 | fmt.Sprintf("no connection for user '%s' found, please try to login again", user), 104 | http.StatusUnauthorized) 105 | } 106 | } else if base.reqAuth { 107 | sendError(w, "not authenticated", http.StatusUnauthorized) 108 | } else { 109 | conn = &base.connections[0] 110 | } 111 | } 112 | return conn 113 | } 114 | 115 | func sendError(w http.ResponseWriter, err string, code int) { 116 | w.Header().Set("Access-Control-Allow-Origin", "*") 117 | http.Error(w, err, code) 118 | } 119 | 120 | func sendCSV(w http.ResponseWriter, data interface{}) { 121 | if s, err := toCsv(data); err != nil { 122 | sendError(w, err.Error(), http.StatusInternalServerError) 123 | } else { 124 | w.Header().Set("Access-Control-Allow-Origin", "*") 125 | w.Header().Set("Content-Type", "application/csv; charset=UTF-8") 126 | w.Write([]byte(s)) 127 | } 128 | } 129 | 130 | func sendJSON(w http.ResponseWriter, data interface{}) { 131 | if b, err := json.Marshal(data); err != nil { 132 | sendError(w, err.Error(), http.StatusInternalServerError) 133 | } else { 134 | w.Header().Set("Access-Control-Allow-Origin", "*") 135 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 136 | w.Write(b) 137 | } 138 | } 139 | 140 | func sendQPack(w http.ResponseWriter, data interface{}) { 141 | if b, err := qpack.Pack(data); err != nil { 142 | sendError(w, err.Error(), http.StatusInternalServerError) 143 | } else { 144 | w.Header().Set("Access-Control-Allow-Origin", "*") 145 | w.Header().Set("Content-Type", "application/x-qpack; charset=UTF-8") 146 | w.Write(b) 147 | } 148 | } 149 | 150 | func sendMsgPack(w http.ResponseWriter, data interface{}) { 151 | if b, err := msgpack.Marshal(data); err != nil { 152 | sendError(w, err.Error(), http.StatusInternalServerError) 153 | } else { 154 | w.Header().Set("Access-Control-Allow-Origin", "*") 155 | w.Header().Set("Content-Type", "application/x-msgpack; charset=UTF-8") 156 | w.Write(b) 157 | } 158 | } 159 | 160 | func getConnByUser(user string) *Conn { 161 | for _, conn := range base.connections { 162 | if conn.user == user { 163 | return &conn 164 | } 165 | } 166 | return nil 167 | } 168 | 169 | func addConnection(username, password string) (*Conn, error) { 170 | var conn Conn 171 | conn.user = username 172 | conn.password = password 173 | conn.client = siridb.NewClient( 174 | conn.user, // user 175 | conn.password, // password 176 | base.dbname, // database 177 | serversToInterface(base.servers), // siridb server(s) 178 | base.logCh, // optional log channel 179 | ) 180 | conn.client.Connect() 181 | if conn.client.IsConnected() && conn.client.IsAvailable() { 182 | base.connections = append(base.connections, conn) 183 | } else { 184 | conn.client.Close() 185 | return nil, fmt.Errorf("Cannot login using the username and password") 186 | } 187 | return &conn, nil 188 | } 189 | 190 | func handlerNotFound(w http.ResponseWriter, r *http.Request) { 191 | sendError(w, "404 not found", http.StatusNotFound) 192 | } 193 | 194 | func onDbInfo(so *socketio.Conn) (int, interface{}) { 195 | db := tDb{ 196 | Dbname: base.dbname, 197 | TimePrecision: base.timePrecision, 198 | Version: base.version, 199 | HTTPServer: AppVersion} 200 | return http.StatusOK, db 201 | } 202 | 203 | func handlerDbInfo(w http.ResponseWriter, r *http.Request) { 204 | db := tDb{ 205 | Dbname: base.dbname, 206 | TimePrecision: base.timePrecision, 207 | Version: base.version, 208 | HTTPServer: AppVersion} 209 | sendData(w, r, db) 210 | } 211 | 212 | func sendData(w http.ResponseWriter, r *http.Request, data interface{}) { 213 | contentType := r.Header.Get("Content-type") 214 | 215 | switch strings.ToLower(contentType) { 216 | case "application/csv": 217 | sendCSV(w, data) 218 | case "application/json": 219 | sendJSON(w, data) 220 | case "application/x-qpack": 221 | sendQPack(w, data) 222 | case "application/x-msgpack": 223 | sendMsgPack(w, data) 224 | default: 225 | sendError(w, fmt.Sprintf("unsupported content-type: %s", contentType), http.StatusUnsupportedMediaType) 226 | } 227 | } 228 | 229 | func readBody(w http.ResponseWriter, r *http.Request, v interface{}) error { 230 | contentType := r.Header.Get("Content-type") 231 | 232 | switch strings.ToLower(contentType) { 233 | case "application/csv": 234 | return readCSV(w, r, &v) 235 | case "application/json": 236 | return readJSON(w, r, &v) 237 | case "application/x-qpack": 238 | return readQPack(w, r, &v) 239 | case "application/x-msgpack": 240 | return readMsgPack(w, r, &v) 241 | default: 242 | err := fmt.Errorf("unsupported content-type: %s", contentType) 243 | sendError(w, err.Error(), http.StatusUnsupportedMediaType) 244 | return err 245 | } 246 | } 247 | 248 | func onAuthFetch(so *socketio.Conn) (int, interface{}) { 249 | authFetch := tAuthFetch{User: nil, AuthRequired: base.reqAuth} 250 | 251 | if user, ok := base.ssessions[(*so).ID()]; ok && getConnByUser(user) != nil { 252 | authFetch.User = user 253 | } else if !base.reqAuth { 254 | authFetch.User = base.connections[0].user 255 | } 256 | return http.StatusOK, authFetch 257 | } 258 | 259 | func handlerAuthFetch(w http.ResponseWriter, r *http.Request) { 260 | 261 | authFetch := tAuthFetch{User: nil, AuthRequired: base.reqAuth} 262 | sess, err := base.gsessions.SessionStart(w, r) 263 | 264 | if err != nil { 265 | sendError(w, err.Error(), http.StatusInternalServerError) 266 | return 267 | } 268 | 269 | if user, ok := sess.Get("user").(string); ok && getConnByUser(user) != nil { 270 | authFetch.User = user 271 | } else if !base.reqAuth { 272 | authFetch.User = base.connections[0].user 273 | } 274 | sendData(w, r, authFetch) 275 | } 276 | 277 | func onAuthLogin(so *socketio.Conn, req *tAuthLoginReq) (int, interface{}) { 278 | if conn := getConnByUser(req.Username); conn != nil { 279 | if req.Password != conn.password { 280 | return StatusUnprocessableEntity, "Username or password incorrect" 281 | } 282 | } else if base.multiUser { 283 | if _, err := addConnection(req.Username, req.Password); err != nil { 284 | return StatusUnprocessableEntity, err.Error() 285 | } 286 | } else { 287 | return StatusUnprocessableEntity, "Multiple user login is not allowed" 288 | } 289 | 290 | base.ssessions[(*so).ID()] = req.Username 291 | authLoginRes := tAuthLoginRes{User: req.Username} 292 | 293 | return http.StatusOK, authLoginRes 294 | } 295 | 296 | func handlerAuthLogin(w http.ResponseWriter, r *http.Request) { 297 | 298 | var authLoginReq tAuthLoginReq 299 | 300 | sess, err := base.gsessions.SessionStart(w, r) 301 | if err != nil { 302 | sendError(w, err.Error(), http.StatusInternalServerError) 303 | return 304 | } 305 | 306 | if err := readBody(w, r, &authLoginReq); err != nil { 307 | return // error is send by the readBody function 308 | } 309 | 310 | if conn := getConnByUser(authLoginReq.Username); conn != nil { 311 | if authLoginReq.Password != conn.password { 312 | sendError(w, "Username or password incorrect", StatusUnprocessableEntity) 313 | return 314 | } 315 | } else if base.multiUser { 316 | if _, err := addConnection(authLoginReq.Username, authLoginReq.Password); err != nil { 317 | sendError(w, err.Error(), StatusUnprocessableEntity) 318 | return 319 | } 320 | } else { 321 | sendError(w, "Multiple user login is not allowed", StatusUnprocessableEntity) 322 | return 323 | } 324 | 325 | sess.Set("user", authLoginReq.Username) 326 | authLoginRes := tAuthLoginRes{User: authLoginReq.Username} 327 | sendData(w, r, authLoginRes) 328 | } 329 | 330 | func onAuthLogout(so *socketio.Conn) (int, interface{}) { 331 | authLogoff := tAuthLogoff{User: nil} 332 | delete(base.ssessions, (*so).ID()) 333 | return http.StatusOK, authLogoff 334 | } 335 | 336 | func handlerAuthLogout(w http.ResponseWriter, r *http.Request) { 337 | authLogoff := tAuthLogoff{User: nil} 338 | 339 | sess, err := base.gsessions.SessionStart(w, r) 340 | if err != nil { 341 | sendError(w, err.Error(), http.StatusInternalServerError) 342 | return 343 | } 344 | 345 | if err = sess.Flush(); err != nil { 346 | sendError(w, err.Error(), http.StatusInternalServerError) 347 | return 348 | } 349 | 350 | sendData(w, r, authLogoff) 351 | } 352 | 353 | func onQuery(so *socketio.Conn, req *tQuery) (int, interface{}) { 354 | conn, err := getConnBySIO(so) 355 | if err != nil { 356 | return http.StatusUnauthorized, err.Error() 357 | } 358 | 359 | var timeout uint64 360 | timeout = 30 361 | 362 | if req.Timeout != nil { 363 | if timeout, err = strconv.ParseUint(fmt.Sprint(req.Timeout), 10, 16); err != nil { 364 | timeout = 30 365 | } 366 | } 367 | 368 | res, err := conn.client.Query(req.Query, uint16(timeout)) 369 | if err != nil { 370 | return http.StatusInternalServerError, err.Error() 371 | } 372 | 373 | return http.StatusOK, res 374 | } 375 | 376 | func handlerQuery(w http.ResponseWriter, r *http.Request) { 377 | if conn := getConnByHTTP(w, r); conn != nil { 378 | var query tQuery 379 | 380 | if err := readBody(w, r, &query); err != nil { 381 | return // error is send by the readBody function 382 | } 383 | 384 | timeout, ok := query.Timeout.(uint16) 385 | if !ok { 386 | timeout = 30 387 | } 388 | 389 | res, err := conn.client.Query(query.Query, timeout) 390 | if err != nil { 391 | sendError(w, err.Error(), http.StatusInternalServerError) 392 | return 393 | } 394 | 395 | sendData(w, r, res) 396 | } 397 | } 398 | 399 | func onInsert(so *socketio.Conn, insert *interface{}) (int, interface{}) { 400 | conn, err := getConnBySIO(so) 401 | if err != nil { 402 | return http.StatusUnauthorized, err.Error() 403 | } 404 | 405 | res, err := conn.client.Insert(*insert, base.insertTimeout) 406 | if err != nil { 407 | return http.StatusInternalServerError, err.Error() 408 | } 409 | 410 | return http.StatusOK, res 411 | } 412 | 413 | func readJSON(w http.ResponseWriter, r *http.Request, v *interface{}) error { 414 | decoder := json.NewDecoder(r.Body) 415 | decoder.UseNumber() 416 | if err := decoder.Decode(v); err != nil { 417 | sendError(w, err.Error(), http.StatusInternalServerError) 418 | return err 419 | } 420 | return nil 421 | } 422 | 423 | func readMsgPack(w http.ResponseWriter, r *http.Request, v *interface{}) error { 424 | decoder := msgpack.NewDecoder(r.Body) 425 | if err := decoder.Decode(v); err != nil { 426 | sendError(w, err.Error(), http.StatusInternalServerError) 427 | return err 428 | } 429 | return nil 430 | } 431 | 432 | func resToPlan(w http.ResponseWriter, res interface{}, v *interface{}, ft string) error { 433 | iface, ok := (*v).(*interface{}) 434 | if ok { 435 | *iface = res 436 | return nil 437 | } 438 | 439 | m, ok := res.(map[string]interface{}) 440 | if !ok { 441 | err := fmt.Errorf("expecting a map for a non interface{} value") 442 | sendError(w, err.Error(), http.StatusInternalServerError) 443 | return err 444 | } 445 | 446 | e := reflect.ValueOf(*v).Elem() 447 | t := e.Type() 448 | n := t.NumField() 449 | 450 | for i := 0; i < n; i++ { 451 | field := t.Field(i) 452 | fn := field.Tag.Get(ft) 453 | if len(fn) == 0 { 454 | fn = field.Name 455 | } 456 | val, ok := m[fn] 457 | if ok { 458 | if e.Field(i).Type() != reflect.TypeOf(val) { 459 | err := fmt.Errorf("unexpected type") 460 | sendError(w, err.Error(), http.StatusInternalServerError) 461 | return err 462 | } 463 | e.Field(i).Set(reflect.ValueOf(val)) 464 | } 465 | } 466 | return nil 467 | } 468 | 469 | func readQPack(w http.ResponseWriter, r *http.Request, v *interface{}) error { 470 | b, err := ioutil.ReadAll(r.Body) 471 | 472 | if err != nil { 473 | sendError(w, err.Error(), http.StatusInternalServerError) 474 | return err 475 | } 476 | 477 | res, err := qpack.Unpack(b, qpack.QpFlagStringKeysOnly) 478 | if err != nil { 479 | sendError(w, err.Error(), http.StatusInternalServerError) 480 | return err 481 | } 482 | 483 | return resToPlan(w, res, v, "qp") 484 | } 485 | 486 | func readCSV(w http.ResponseWriter, r *http.Request, v *interface{}) error { 487 | res, err := parseCsv(r.Body) 488 | if err != nil { 489 | sendError(w, err.Error(), http.StatusInternalServerError) 490 | return err 491 | } 492 | return resToPlan(w, res, v, "csv") 493 | } 494 | 495 | func handlerInsert(w http.ResponseWriter, r *http.Request) { 496 | if conn := getConnByHTTP(w, r); conn != nil { 497 | var insert interface{} 498 | if err := readBody(w, r, &insert); err != nil { 499 | return // error is send by the readBody function 500 | } 501 | 502 | res, err := conn.client.Insert(insert, base.insertTimeout) 503 | if err != nil { 504 | sendError(w, err.Error(), http.StatusInternalServerError) 505 | return 506 | } 507 | 508 | sendData(w, r, res) 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /handlersprod.go: -------------------------------------------------------------------------------- 1 | // +build !debug 2 | 3 | package main 4 | 5 | import "net/http" 6 | 7 | func handlerMain(w http.ResponseWriter, r *http.Request) { 8 | if r.URL.Path != "/" { 9 | handlerNotFound(w, r) 10 | } else if base.connections[0].client.IsConnected() { 11 | w.Header().Set("Content-Type", "text/html") 12 | w.Write(FileIndexHTML) 13 | } else { 14 | w.Header().Set("Content-Type", "text/html") 15 | w.Write(FileWaitingHTML) 16 | } 17 | 18 | } 19 | 20 | func handlerJsBundle(w http.ResponseWriter, r *http.Request) { 21 | w.Header().Set("Content-Type", "text/javascript") 22 | w.Write(FileBundleMinJS) 23 | } 24 | 25 | func handlerFaviconIco(w http.ResponseWriter, r *http.Request) { 26 | w.Header().Set("Content-Type", "image/x-icon") 27 | w.Write(FileFaviconICO) 28 | } 29 | 30 | func handlerBootstrapCSS(w http.ResponseWriter, r *http.Request) { 31 | w.Header().Set("Content-Type", "text/css") 32 | w.Write(FileBootstrapMinCSS) 33 | } 34 | 35 | func handlerLayout(w http.ResponseWriter, r *http.Request) { 36 | w.Header().Set("Content-Type", "text/css") 37 | w.Write(FileLayoutMinCSS) 38 | } 39 | 40 | func handlerFontAwesomeMinCSS(w http.ResponseWriter, r *http.Request) { 41 | w.Header().Set("Content-Type", "text/css") 42 | w.Write(FileFontAwesomeMinCSS) 43 | } 44 | 45 | func handlerFontsFaOTF(w http.ResponseWriter, r *http.Request) { 46 | w.Header().Set("Content-Type", "application/font-otf") 47 | w.Write(FileFontAwesomeOTF) 48 | } 49 | 50 | func handlerFontsFaEOT(w http.ResponseWriter, r *http.Request) { 51 | w.Header().Set("Content-Type", "application/font-eot") 52 | w.Write(FileFontawesomeWebfontEOT) 53 | } 54 | 55 | func handlerFontsFaSVG(w http.ResponseWriter, r *http.Request) { 56 | w.Header().Set("Content-Type", "application/font-svg") 57 | w.Write(FileFontawesomeWebfontSVG) 58 | } 59 | 60 | func handlerFontsFaTTF(w http.ResponseWriter, r *http.Request) { 61 | w.Header().Set("Content-Type", "application/font-ttf") 62 | w.Write(FileFontawesomeWebfontTTF) 63 | } 64 | 65 | func handlerFontsFaWOFF(w http.ResponseWriter, r *http.Request) { 66 | w.Header().Set("Content-Type", "application/font-woff") 67 | w.Write(FileFontawesomeWebfontWOFF) 68 | } 69 | 70 | func handlerFontsFaWOFF2(w http.ResponseWriter, r *http.Request) { 71 | w.Header().Set("Content-Type", "application/font-woff2") 72 | w.Write(FileFontawesomeWebfontWOFF2) 73 | } 74 | 75 | func handlerSiriDBLargePNG(w http.ResponseWriter, r *http.Request) { 76 | w.Header().Set("Content-Type", "image/png") 77 | w.Write(FileSiriDBLargePNG) 78 | } 79 | 80 | func handlerSiriDBSmallPNG(w http.ResponseWriter, r *http.Request) { 81 | w.Header().Set("Content-Type", "image/png") 82 | w.Write(FileSiriDBSmallPNG) 83 | } 84 | 85 | func handlerLoaderGIF(w http.ResponseWriter, r *http.Request) { 86 | w.Header().Set("Content-Type", "image/gif") 87 | w.Write(FileLoaderGIF) 88 | } 89 | -------------------------------------------------------------------------------- /handlerstest.go: -------------------------------------------------------------------------------- 1 | // +build debug 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | ) 10 | 11 | func handlerMain(w http.ResponseWriter, r *http.Request) { 12 | if r.URL.Path != "/" { 13 | handlerNotFound(w, r) 14 | } else if base.connections[0].client.IsConnected() { 15 | handleFileRequest(w, "./src/index.html", "text/html") 16 | } else { 17 | handleFileRequest(w, "./src/waiting.html", "text/html") 18 | } 19 | } 20 | 21 | func handlerJsBundle(w http.ResponseWriter, r *http.Request) { 22 | handleFileRequest(w, "./build/bundle.js", "text/javascript") 23 | } 24 | 25 | func handlerFaviconIco(w http.ResponseWriter, r *http.Request) { 26 | handleFileRequest(w, "./static/favicon.ico", "image/x-icon") 27 | } 28 | 29 | func handlerBootstrapCSS(w http.ResponseWriter, r *http.Request) { 30 | handleFileRequest(w, "./static/css/bootstrap.css", "text/css") 31 | } 32 | 33 | func handlerLayout(w http.ResponseWriter, r *http.Request) { 34 | handleFileRequest(w, "./build/layout.css", "text/css") 35 | } 36 | 37 | func handlerFontAwesomeMinCSS(w http.ResponseWriter, r *http.Request) { 38 | handleFileRequest(w, "./static/css/font-awesome.min.css", "text/css") 39 | } 40 | 41 | func handlerFontsFaOTF(w http.ResponseWriter, r *http.Request) { 42 | handleFileRequest(w, "./static/fonts/FontAwesome.otf", "application/font-otf") 43 | } 44 | 45 | func handlerFontsFaEOT(w http.ResponseWriter, r *http.Request) { 46 | handleFileRequest(w, "./static/fonts/fontawesome-webfont.eot", "application/font-eot") 47 | } 48 | 49 | func handlerFontsFaSVG(w http.ResponseWriter, r *http.Request) { 50 | handleFileRequest(w, "./static/fonts/fontawesome-webfont.svg", "application/font-svg") 51 | } 52 | 53 | func handlerFontsFaTTF(w http.ResponseWriter, r *http.Request) { 54 | handleFileRequest(w, "./static/fonts/fontawesome-webfont.ttf", "application/font-ttf") 55 | } 56 | 57 | func handlerFontsFaWOFF(w http.ResponseWriter, r *http.Request) { 58 | handleFileRequest(w, "./static/fonts/fontawesome-webfont.woff", "application/font-woff") 59 | } 60 | 61 | func handlerFontsFaWOFF2(w http.ResponseWriter, r *http.Request) { 62 | handleFileRequest(w, "./static/fonts/fontawesome-webfont.woff2", "application/font-woff2") 63 | } 64 | 65 | func handlerSiriDBLargePNG(w http.ResponseWriter, r *http.Request) { 66 | handleFileRequest(w, "./static/img/siridb-large.png", "image/png") 67 | } 68 | 69 | func handlerSiriDBSmallPNG(w http.ResponseWriter, r *http.Request) { 70 | handleFileRequest(w, "./static/img/siridb-small.png", "image/png") 71 | } 72 | 73 | func handlerLoaderGIF(w http.ResponseWriter, r *http.Request) { 74 | handleFileRequest(w, "./static/img/loader.gif", "image/gif") 75 | } 76 | 77 | func handleFileRequest(w http.ResponseWriter, fn, ct string) { 78 | b, err := ioutil.ReadFile(fn) 79 | if err == nil { 80 | w.Header().Set("Content-Type", ct) 81 | _, err = w.Write(b) 82 | } else { 83 | w.WriteHeader(http.StatusInternalServerError) 84 | _, err = fmt.Fprintf(w, "Internal server error: %s", err) 85 | } 86 | if err != nil { 87 | fmt.Println(err) 88 | } 89 | } 90 | 91 | func init() { 92 | fmt.Println("# DEBUG MODE: using original template files...") 93 | } 94 | -------------------------------------------------------------------------------- /siridb-http.conf: -------------------------------------------------------------------------------- 1 | # SiriDB HTTP Configuration file 2 | [Database] 3 | # User with at least 'show' privileges. 4 | user = 5 | 6 | # A password is required. To protect the password this file should be placed in 7 | # a folder where unauthorized users have no access. 8 | password = 9 | 10 | # Database to connect to. 11 | dbname = 12 | 13 | # Multiple servers are allowed and should be comma separated. When a port 14 | # is not provided the default 9000 is used. IPv6 address are supported and 15 | # should be wrapped in square brackets [] in case an alternative port is 16 | # required. SiriDB HTTP will randomly select an available siridb server 17 | # for each request. 18 | # 19 | # Valid examples: 20 | # siridb01.local,siridb02.local,siridb03.local,siridb04.local 21 | # 10.20.30.40 22 | # [::1]:5050,[::1]:5051 23 | # 2001:0db8:85a3:0000:0000:8a2e:0370:7334 24 | servers = localhost 25 | 26 | [Configuration] 27 | # Listening to TCP port. 28 | port = 5050 29 | 30 | # When disabled no authentication is required. When enabled session 31 | # authentication or basic authentication is required. 32 | require_authentication = True 33 | 34 | # When enabled /socket.io/ will be enabled and Socket-IO can be used as an 35 | # alternative to the standard http rest api. 36 | enable_socket_io = True 37 | 38 | # When enabled the crt_file and key_file must be configured and the server 39 | # will be hosted on https. 40 | enable_ssl = False 41 | 42 | # When enabled a website is hosted on the configured port. When disabled the 43 | # resource URIs like /query, /insert, /auth/.. etc. are still available. 44 | enable_web = True 45 | 46 | # When enabled the /query and /insert resource URIs can be used with basic 47 | # authentication. 48 | enable_basic_auth = False 49 | 50 | # When multi user is disabled, only the user/password combination provided in 51 | # this configuration file can be used. 52 | enable_multi_user = False 53 | 54 | # Cookie max age is used to set the cookie expiration time in seconds. 55 | cookie_max_age = 604800 56 | 57 | # The query api allows you to specify a timeout for each query, but the insert 58 | # api only accepts data. Therefore the insert timeout is set as a general 59 | # value and is applicable to each insert. 60 | insert_timeout = 60 61 | 62 | [SSL] 63 | # Self-signed certificates can be created with the following command: 64 | # 65 | # openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ 66 | # -keyout certificate.key -out certificate.crt 67 | # 68 | crt_file = certificate.crt 69 | key_file = certificate.key 70 | 71 | # 72 | # Welcome and thank you for using SiriDB! 73 | # 74 | # A configuration file is required and should be provided with the 75 | # --config argument. 76 | # Above you find an example template which can be used. 77 | # 78 | 79 | -------------------------------------------------------------------------------- /siridb-http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | siridb "github.com/SiriDB/go-siridb-connector" 15 | kingpin "github.com/alecthomas/kingpin/v2" 16 | "github.com/astaxie/beego/session" 17 | socketio "github.com/googollee/go-socket.io" 18 | ini "gopkg.in/ini.v1" 19 | ) 20 | 21 | // AppVersion exposes version information 22 | const AppVersion = "2.0.20" 23 | 24 | const retryConnectTime = 5 25 | 26 | // Conn is used to store the user/password with the client. 27 | type Conn struct { 28 | user string 29 | password string 30 | client *siridb.Client 31 | } 32 | 33 | type store struct { 34 | connections []Conn 35 | dbname string 36 | timePrecision string 37 | version string 38 | servers []server 39 | port uint16 40 | insertTimeout uint16 41 | logCh chan string 42 | reqAuth bool 43 | multiUser bool 44 | enableWeb bool 45 | enableSio bool 46 | enableSSL bool 47 | enableBasicAuth bool 48 | ssessions map[string]string 49 | cookieMaxAge uint64 50 | crtFile string 51 | keyFile string 52 | gsessions *session.Manager 53 | } 54 | 55 | type server struct { 56 | host string 57 | port uint16 58 | } 59 | 60 | var ( 61 | xApp = kingpin.New("siridb-http", "Provides a HTTP API and optional web interface for SiriDB.") 62 | xConfig = xApp.Flag("config", "Configuration and connection file for SiriDB HTTP.").Default("").Short('c').String() 63 | xVerbose = xApp.Flag("verbose", "Enable verbose logging.").Bool() 64 | xVersion = xApp.Flag("version", "Print version information and exit.").Short('v').Bool() 65 | ) 66 | 67 | var base = store{} 68 | 69 | func getHostAndPort(addr string) (server, error) { 70 | parts := strings.Split(addr, ":") 71 | // IPv4 72 | if len(parts) == 1 { 73 | return server{parts[0], 9000}, nil 74 | } 75 | if len(parts) == 2 { 76 | u, err := strconv.ParseUint(parts[1], 10, 16) 77 | return server{parts[0], uint16(u)}, err 78 | } 79 | // IPv6 80 | if addr[0] != '[' { 81 | return server{fmt.Sprintf("[%s]", addr), 9000}, nil 82 | } 83 | if addr[len(addr)-1] == ']' { 84 | return server{addr, 9000}, nil 85 | } 86 | u, err := strconv.ParseUint(parts[len(parts)-1], 10, 16) 87 | addr = strings.Join(parts[:len(parts)-1], ":") 88 | 89 | return server{addr, uint16(u)}, err 90 | } 91 | 92 | func getServers(addrstr string) ([]server, error) { 93 | arr := strings.Split(addrstr, ",") 94 | servers := make([]server, len(arr)) 95 | for i, addr := range arr { 96 | addr = strings.TrimSpace(addr) 97 | server, err := getHostAndPort(addr) 98 | if err != nil { 99 | return nil, err 100 | } 101 | servers[i] = server 102 | } 103 | return servers, nil 104 | } 105 | 106 | func serversToInterface(servers []server) [][]interface{} { 107 | ret := make([][]interface{}, len(servers)) 108 | for i, svr := range servers { 109 | ret[i] = make([]interface{}, 2) 110 | ret[i][0] = svr.host 111 | ret[i][1] = int(svr.port) 112 | } 113 | return ret 114 | } 115 | 116 | func logHandle(logCh chan string) { 117 | for { 118 | msg := <-logCh 119 | if *xVerbose { 120 | println(msg) 121 | } 122 | } 123 | } 124 | 125 | func sigHandle(sigCh chan os.Signal) { 126 | for { 127 | <-sigCh 128 | quit(nil) 129 | } 130 | } 131 | 132 | func quit(err error) { 133 | rc := 0 134 | if err != nil { 135 | fmt.Printf("%s\n", err) 136 | rc = 1 137 | } 138 | 139 | for _, conn := range base.connections { 140 | if conn.client != nil { 141 | conn.client.Close() 142 | } 143 | } 144 | 145 | os.Exit(rc) 146 | } 147 | 148 | func connect(conn Conn) { 149 | for !conn.client.IsConnected() { 150 | base.logCh <- fmt.Sprintf("not connected to SiriDB, try again in %d seconds", retryConnectTime) 151 | time.Sleep(retryConnectTime * time.Second) 152 | } 153 | res, err := conn.client.Query("show time_precision, version", 10) 154 | if err != nil { 155 | quit(err) 156 | } 157 | v, ok := res.(map[string]interface{}) 158 | if !ok { 159 | quit(fmt.Errorf("missing 'map' in data")) 160 | } 161 | 162 | arr, ok := v["data"].([]interface{}) 163 | if !ok || len(arr) != 2 { 164 | quit(fmt.Errorf("missing array 'data' or length 2 in map")) 165 | } 166 | 167 | base.timePrecision, ok = arr[0].(map[string]interface{})["value"].(string) 168 | base.version, ok = arr[1].(map[string]interface{})["value"].(string) 169 | 170 | if !ok { 171 | quit(fmt.Errorf("cannot find time_precision and/or version in data")) 172 | } 173 | } 174 | 175 | func readBool(section *ini.Section, key string) (b bool) { 176 | if bIni, err := section.GetKey(key); err != nil { 177 | quit(err) 178 | } else if b, err = bIni.Bool(); err != nil { 179 | quit(err) 180 | } 181 | return b 182 | } 183 | 184 | func readString(section *ini.Section, key string) (s string) { 185 | if sIni, err := section.GetKey(key); err != nil { 186 | quit(err) 187 | } else { 188 | s = sIni.String() 189 | } 190 | return s 191 | } 192 | 193 | type customServer struct { 194 | Server *socketio.Server 195 | } 196 | 197 | func (s *customServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 198 | w.Header().Set("Access-Control-Allow-Credentials", "true") 199 | origin := r.Header.Get("Origin") 200 | w.Header().Set("Access-Control-Allow-Origin", origin) 201 | s.Server.ServeHTTP(w, r) 202 | } 203 | 204 | func main() { 205 | 206 | // parse arguments 207 | _, err := xApp.Parse(os.Args[1:]) 208 | if err != nil { 209 | quit(err) 210 | } 211 | 212 | if *xVersion { 213 | fmt.Printf("%s\n", AppVersion) 214 | os.Exit(0) 215 | } 216 | 217 | if *xConfig == "" { 218 | fmt.Printf( 219 | `# SiriDB HTTP Configuration file 220 | [Database] 221 | # User with at least 'show' privileges. 222 | user = 223 | 224 | # A password is required. To protect the password this file should be placed in 225 | # a folder where unauthorized users have no access. 226 | password = 227 | 228 | # Database to connect to. 229 | dbname = 230 | 231 | # Multiple servers are allowed and should be comma separated. When a port 232 | # is not provided the default 9000 is used. IPv6 address are supported and 233 | # should be wrapped in square brackets [] in case an alternative port is 234 | # required. SiriDB HTTP will randomly select an available siridb server 235 | # for each request. 236 | # 237 | # Valid examples: 238 | # siridb01.local,siridb02.local,siridb03.local,siridb04.local 239 | # 10.20.30.40 240 | # [::1]:5050,[::1]:5051 241 | # 2001:0db8:85a3:0000:0000:8a2e:0370:7334 242 | servers = localhost 243 | 244 | [Configuration] 245 | # Listening to TCP port. 246 | port = 5050 247 | 248 | # When disabled no authentication is required. When enabled session 249 | # authentication or basic authentication is required. 250 | require_authentication = True 251 | 252 | # When enabled /socket.io/ will be enabled and Socket-IO can be used as an 253 | # alternative to the standard http rest api. 254 | enable_socket_io = True 255 | 256 | # When enabled the crt_file and key_file must be configured and the server 257 | # will be hosted on https. 258 | enable_ssl = False 259 | 260 | # When enabled a website is hosted on the configured port. When disabled the 261 | # resource URIs like /query, /insert, /auth/.. etc. are still available. 262 | enable_web = True 263 | 264 | # When enabled the /query and /insert resource URIs can be used with basic 265 | # authentication. 266 | enable_basic_auth = False 267 | 268 | # When multi user is disabled, only the user/password combination provided in 269 | # this configuration file can be used. 270 | enable_multi_user = False 271 | 272 | # Cookie max age is used to set the cookie expiration time in seconds. 273 | cookie_max_age = 604800 274 | 275 | # The query api allows you to specify a timeout for each query, but the insert 276 | # api only accepts data. Therefore the insert timeout is set as a general 277 | # value and is applicable to each insert. 278 | insert_timeout = 60 279 | 280 | [SSL] 281 | # Self-signed certificates can be created with the following command: 282 | # 283 | # openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ 284 | # -keyout certificate.key -out certificate.crt 285 | # 286 | crt_file = certificate.crt 287 | key_file = certificate.key 288 | 289 | # 290 | # Welcome and thank you for using SiriDB! 291 | # 292 | # A configuration file is required and shoud be provided with the 293 | # --config argument. 294 | # Above you find an example template which can be used. 295 | # 296 | 297 | `) 298 | os.Exit(0) 299 | } 300 | 301 | var conn Conn 302 | 303 | cfg, err := ini.Load(*xConfig) 304 | if err != nil { 305 | quit(err) 306 | } 307 | 308 | section, err := cfg.GetSection("Database") 309 | if err != nil { 310 | quit(err) 311 | } 312 | 313 | base.servers, err = getServers(readString(section, "servers")) 314 | if err != nil { 315 | quit(err) 316 | } 317 | 318 | base.dbname = readString(section, "dbname") 319 | conn.user = readString(section, "user") 320 | conn.password = readString(section, "password") 321 | 322 | base.logCh = make(chan string) 323 | go logHandle(base.logCh) 324 | 325 | sigCh := make(chan os.Signal, 1) 326 | signal.Notify(sigCh, os.Interrupt) 327 | go sigHandle(sigCh) 328 | 329 | conn.client = siridb.NewClient( 330 | conn.user, // user 331 | conn.password, // password 332 | base.dbname, // database 333 | serversToInterface(base.servers), // siridb server(s) 334 | base.logCh, // optional log channel 335 | ) 336 | base.connections = append(base.connections, conn) 337 | base.ssessions = make(map[string]string) 338 | 339 | section, err = cfg.GetSection("Configuration") 340 | if err != nil { 341 | quit(err) 342 | } 343 | 344 | base.reqAuth = readBool(section, "require_authentication") 345 | base.enableWeb = readBool(section, "enable_web") 346 | base.enableSio = readBool(section, "enable_socket_io") 347 | base.enableSSL = readBool(section, "enable_ssl") 348 | base.multiUser = readBool(section, "enable_multi_user") 349 | base.enableBasicAuth = readBool(section, "enable_basic_auth") 350 | 351 | if portIni, err := section.GetKey("port"); err != nil { 352 | quit(err) 353 | } else if port64, err := portIni.Uint64(); err != nil { 354 | quit(err) 355 | } else { 356 | base.port = uint16(port64) 357 | } 358 | 359 | if cookieMaxAgeIni, err := section.GetKey("cookie_max_age"); err != nil { 360 | quit(err) 361 | } else if base.cookieMaxAge, err = cookieMaxAgeIni.Uint64(); err != nil { 362 | quit(err) 363 | } 364 | 365 | if insertTimeoutIni, err := section.GetKey("insert_timeout"); err != nil { 366 | quit(err) 367 | } else if insertTimeout64, err := insertTimeoutIni.Uint64(); err != nil { 368 | quit(err) 369 | } else { 370 | base.insertTimeout = uint16(insertTimeout64) 371 | } 372 | 373 | if base.enableSSL { 374 | section, err = cfg.GetSection("SSL") 375 | if err != nil { 376 | quit(err) 377 | } 378 | base.crtFile = readString(section, "crt_file") 379 | base.keyFile = readString(section, "key_file") 380 | } 381 | 382 | http.HandleFunc("*", handlerNotFound) 383 | 384 | if base.enableWeb { 385 | http.HandleFunc("/", handlerMain) 386 | http.HandleFunc("/js/bundle", handlerJsBundle) 387 | http.HandleFunc("/css/bootstrap", handlerBootstrapCSS) 388 | http.HandleFunc("/css/layout", handlerLayout) 389 | http.HandleFunc("/favicon.ico", handlerFaviconIco) 390 | http.HandleFunc("/img/siridb-large.png", handlerSiriDBLargePNG) 391 | http.HandleFunc("/img/siridb-small.png", handlerSiriDBSmallPNG) 392 | http.HandleFunc("/img/loader.gif", handlerLoaderGIF) 393 | http.HandleFunc("/css/font-awesome.min.css", handlerFontAwesomeMinCSS) 394 | http.HandleFunc("/fonts/FontAwesome.otf", handlerFontsFaOTF) 395 | http.HandleFunc("/fonts/fontawesome-webfont.eot", handlerFontsFaEOT) 396 | http.HandleFunc("/fonts/fontawesome-webfont.svg", handlerFontsFaSVG) 397 | http.HandleFunc("/fonts/fontawesome-webfont.ttf", handlerFontsFaTTF) 398 | http.HandleFunc("/fonts/fontawesome-webfont.woff", handlerFontsFaWOFF) 399 | http.HandleFunc("/fonts/fontawesome-webfont.woff2", handlerFontsFaWOFF2) 400 | } 401 | 402 | http.HandleFunc("/db-info", handlerDbInfo) 403 | http.HandleFunc("/auth/fetch", handlerAuthFetch) 404 | http.HandleFunc("/query", handlerQuery) 405 | http.HandleFunc("/insert", handlerInsert) 406 | 407 | cf := new(session.ManagerConfig) 408 | cf.EnableSetCookie = true 409 | s := fmt.Sprintf(`{"cookieName":"siridbadminsessionid","gclifetime":%d}`, base.cookieMaxAge) 410 | 411 | if err = json.Unmarshal([]byte(s), cf); err != nil { 412 | quit(err) 413 | } 414 | 415 | if base.gsessions, err = session.NewManager("memory", cf); err != nil { 416 | quit(err) 417 | } 418 | 419 | go base.gsessions.GC() 420 | http.HandleFunc("/auth/login", handlerAuthLogin) 421 | http.HandleFunc("/auth/logout", handlerAuthLogout) 422 | 423 | conn.client.Connect() 424 | go connect(conn) 425 | 426 | if base.enableSio { 427 | server := socketio.NewServer(nil) 428 | if server != nil { 429 | quit(errors.New("failed to create server")) 430 | } 431 | 432 | server.OnConnect("/", func(s socketio.Conn) error { 433 | s.SetContext("/") 434 | return nil 435 | }) 436 | 437 | server.OnEvent("/", "db-info", func(so socketio.Conn, _ string) (int, interface{}) { 438 | return onDbInfo(&so) 439 | }) 440 | server.OnEvent("/", "auth fetch", func(so socketio.Conn, _ string) (int, interface{}) { 441 | return onAuthFetch(&so) 442 | }) 443 | server.OnEvent("/", "auth login", func(so socketio.Conn, req tAuthLoginReq) (int, interface{}) { 444 | return onAuthLogin(&so, &req) 445 | }) 446 | server.OnEvent("/", "query", func(so socketio.Conn, req tQuery) (int, interface{}) { 447 | return onQuery(&so, &req) 448 | }) 449 | server.OnEvent("/", "insert", func(so socketio.Conn, req interface{}) (int, interface{}) { 450 | return onInsert(&so, &req) 451 | }) 452 | server.OnDisconnect("disconnection", func(so socketio.Conn, _ string) { 453 | delete(base.ssessions, so.ID()) 454 | }) 455 | 456 | server.OnError("error", func(so socketio.Conn, err error) { 457 | base.logCh <- fmt.Sprintf("socket.io error: %s", err.Error()) 458 | }) 459 | 460 | go server.Serve() 461 | defer server.Close() 462 | 463 | http.Handle("/socket.io/", &customServer{Server: server}) 464 | } 465 | 466 | msg := "Serving SiriDB API on http%s://0.0.0.0:%d\n" 467 | if base.enableSSL { 468 | fmt.Printf(msg, "s", base.port) 469 | if err = http.ListenAndServeTLS( 470 | fmt.Sprintf(":%d", base.port), 471 | base.crtFile, 472 | base.keyFile, 473 | nil); err != nil { 474 | fmt.Printf("error: %s\n", err) 475 | } 476 | } else { 477 | fmt.Printf(msg, "", base.port) 478 | if err = http.ListenAndServe(fmt.Sprintf(":%d", base.port), nil); err != nil { 479 | fmt.Printf("error: %s\n", err) 480 | } 481 | } 482 | quit(nil) 483 | } 484 | -------------------------------------------------------------------------------- /siridb-http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiriDB/siridb-http/dc5a07e259741fc74020f0587437de7fae6930cf/siridb-http.png -------------------------------------------------------------------------------- /src/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | **/node_modules/* 3 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'es6': true, 5 | 'node': true 6 | }, 7 | 'extends': ['eslint:recommended', 'plugin:react/all'], 8 | 'parserOptions': { 9 | 'ecmaFeatures': { 10 | 'experimentalObjectRestSpread': true, 11 | 'jsx': true 12 | }, 13 | 'sourceType': 'module' 14 | }, 15 | 'parser': 'babel-eslint', 16 | 'plugins': [ 17 | 'react' 18 | ], 19 | 'settings': { 20 | 'react': { 21 | 'version': '16.8.1' 22 | } 23 | }, 24 | 'rules': { 25 | 'indent': [ 26 | 'error', 27 | 4 28 | ], 29 | 'linebreak-style': [ 30 | 'error', 31 | 'unix' 32 | ], 33 | 'quotes': [ 34 | 'error', 35 | 'single' 36 | ], 37 | 'semi': [ 38 | 'error', 39 | 'always' 40 | ], 41 | 'react/display-name': [0], 42 | 'react/forbid-prop-types': [0], 43 | 'react/function-component-definition': [0], 44 | 'react/jsx-curly-brace-presence': [0], 45 | 'react/jsx-filename-extension': [1, { 'extensions': ['.js', '.jsx'] }], 46 | 'react/jsx-max-depth': [2, { 'max': 7 }], 47 | 'react/jsx-newline': [0], 48 | 'react/no-array-index-key': [0], 49 | 'react/no-multi-comp': [2, { 'ignoreStateless': true }], 50 | 'react/no-set-state': [0], 51 | 'react/no-unstable-nested-components': [0], 52 | 'react/sort-prop-types': [0] 53 | 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/Actions/AppActions.js: -------------------------------------------------------------------------------- 1 | import Vlow from 'vlow'; 2 | export default Vlow.createActions([ 3 | 'setAppError' 4 | ]); -------------------------------------------------------------------------------- /src/Actions/AuthActions.js: -------------------------------------------------------------------------------- 1 | import Vlow from 'vlow'; 2 | export default Vlow.createActions([ 3 | 'setAuthError', 4 | 'clearAuthError', 5 | 'login', 6 | 'fetch', 7 | 'logoff' 8 | ]); -------------------------------------------------------------------------------- /src/Actions/DatabaseActions.js: -------------------------------------------------------------------------------- 1 | import Vlow from 'vlow'; 2 | export default Vlow.createActions([ 3 | 'fetch' 4 | ]); -------------------------------------------------------------------------------- /src/Actions/InsertActions.js: -------------------------------------------------------------------------------- 1 | import Vlow from 'vlow'; 2 | 3 | const InsertActions = Vlow.createActions([ 4 | 'insert', 5 | 'clearAlert' 6 | ]); 7 | 8 | export default InsertActions; -------------------------------------------------------------------------------- /src/Actions/QueryActions.js: -------------------------------------------------------------------------------- 1 | import Vlow from 'vlow'; 2 | export default Vlow.createActions([ 3 | 'query', 4 | 'clearAlert', 5 | 'clearAll' 6 | ]); -------------------------------------------------------------------------------- /src/Components/App/App.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import {withVlow} from 'vlow'; 4 | import {Route, Switch} from 'react-router-dom'; 5 | 6 | import AppStore from '../../Stores/AppStore'; 7 | import Auth from '../Auth/Auth'; 8 | import AuthStore from '../../Stores/AuthStore'; 9 | import DatabaseStore from '../../Stores/DatabaseStore'; 10 | import InfoModal from './InfoModal'; 11 | import Insert from '../Insert/Insert'; 12 | import PageDoesNotExist from './PageDoesNotExist'; 13 | import Query from '../Query/Query'; 14 | import TopMenu from './TopMenu'; 15 | 16 | 17 | const withStores = withVlow([AppStore, DatabaseStore, AuthStore]); 18 | 19 | 20 | class App extends React.Component { 21 | 22 | static propTypes = { 23 | appError: PropTypes.string, 24 | authRequired: PropTypes.bool, 25 | user: PropTypes.string, 26 | } 27 | 28 | static defaultProps = { 29 | appError: null, 30 | authRequired: null, 31 | user: null, 32 | } 33 | 34 | constructor(props) { 35 | super(props); 36 | 37 | this.state = { 38 | showInfoModal: false 39 | }; 40 | } 41 | 42 | shouldComponentUpdate () { 43 | return true; 44 | } 45 | 46 | handleShowInfoModal = () => { 47 | this.setState({ showInfoModal: true }); 48 | } 49 | 50 | handleHideInfoModal = () => { 51 | this.setState({ showInfoModal: false }); 52 | } 53 | 54 | render() { 55 | const {appError, user, authRequired} = this.props; 56 | const {showInfoModal} = this.state; 57 | 58 | return (appError !== null) ? 59 |
60 | {appError} 61 |
62 | : (user !== null) ? 63 |
64 | 68 | 72 | 73 | 78 | 82 | 86 | 87 |
88 | : (authRequired === true) ? : null; 89 | } 90 | } 91 | 92 | export default withStores(App); -------------------------------------------------------------------------------- /src/Components/App/InfoModal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {withVlow} from 'vlow'; 4 | import DatabaseStore from '../../Stores/DatabaseStore'; 5 | import {Modal} from 'react-bootstrap'; 6 | 7 | 8 | const withStores = withVlow(DatabaseStore); 9 | 10 | 11 | const InfoModal = ({onHide, show, dbname, version, httpServer}) => ( 12 | 16 | 17 | 18 | SiriDB Logo 22 |
23 |
24 | {'Database:'} 25 |
26 |
27 | {dbname} 28 |
29 |
30 | {'SiriDB:'} 31 |
32 |
33 | {version} 34 |
35 |
36 | {'HTTP Server:'} 37 |
38 |
39 | {httpServer} 40 |
41 |
42 |
43 |
44 | ); 45 | 46 | InfoModal.propTypes = { 47 | onHide: PropTypes.func.isRequired, 48 | show: PropTypes.bool.isRequired, 49 | 50 | /* DatabaseStore properties */ 51 | dbname: PropTypes.string, 52 | httpServer: PropTypes.string, 53 | version: PropTypes.string, 54 | }; 55 | 56 | InfoModal.defaultProps = { 57 | dbname: '', 58 | httpServer: '', 59 | version: '', 60 | }; 61 | 62 | export default withStores(InfoModal); 63 | -------------------------------------------------------------------------------- /src/Components/App/PageDoesNotExist.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PageDoesNotExist = () => ( 4 |
5 | {'Page does not exist'} 6 |
7 | ); 8 | 9 | export default PageDoesNotExist; -------------------------------------------------------------------------------- /src/Components/App/TopMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {NavLink} from 'react-router-dom'; 4 | 5 | import AuthActions from '../../Actions/AuthActions'; 6 | 7 | 8 | class TopMenu extends React.Component { 9 | 10 | static propTypes = { 11 | onLogoClick: PropTypes.func.isRequired, 12 | showLogoff: PropTypes.bool.isRequired 13 | }; 14 | 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | isIn: false 19 | }; 20 | } 21 | 22 | shouldComponentUpdate() { 23 | return true; 24 | } 25 | 26 | handleToggleClick = () => { 27 | /* eslint-disable react/no-set-state */ 28 | this.setState((prevState) => ({ 29 | isIn: !prevState.isIn 30 | })); 31 | } 32 | 33 | handleItemClick = () => { 34 | /* eslint-disable react/no-set-state */ 35 | this.setState({ 36 | isIn: false 37 | }); 38 | } 39 | 40 | handleLogoff = () => { 41 | AuthActions.logoff(); 42 | } 43 | 44 | render() { 45 | const {onLogoClick, showLogoff} = this.props; 46 | let logoff = showLogoff ? ( 47 |
  • 48 | 49 | {'Logoff'} 50 | 51 |
  • 52 | ) : null; 53 | 54 | const {isIn} = this.state; 55 | 56 | let navclass = isIn ? ' in' : ''; 57 | 58 | return ( 59 | 109 | ); 110 | } 111 | } 112 | 113 | export default TopMenu; -------------------------------------------------------------------------------- /src/Components/Auth/Auth.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import {withVlow} from 'vlow'; 4 | 5 | import AuthActions from '../../Actions/AuthActions'; 6 | import AuthStore from '../../Stores/AuthStore'; 7 | 8 | 9 | const withStores = withVlow(AuthStore); 10 | 11 | 12 | class Auth extends React.Component { 13 | 14 | static propTypes = { 15 | /* AuthStore properties */ 16 | authError: PropTypes.string, 17 | } 18 | 19 | static defaultProps = { 20 | authError: null, 21 | } 22 | 23 | constructor(props) { 24 | super(props); 25 | 26 | this.state = { 27 | username: '', 28 | password: '' 29 | }; 30 | } 31 | 32 | shouldComponentUpdate() { 33 | return true; 34 | } 35 | 36 | handleLogin = () => { 37 | const {username, password} = this.state; 38 | AuthActions.login(username, password); 39 | } 40 | 41 | handleUsernameChange = (event) => { 42 | AuthActions.clearAuthError(); 43 | this.setState({ 44 | username: event.target.value 45 | }); 46 | } 47 | 48 | handlePasswordChange = (event) => { 49 | AuthActions.clearAuthError(); 50 | this.setState({ 51 | password: event.target.value 52 | }); 53 | } 54 | 55 | handleKeyPress = (event) => { 56 | if (event.key == 'Enter') { 57 | this.handleLogin(); 58 | } 59 | } 60 | 61 | render() { 62 | const {authError} = this.props; 63 | const {username, password} = this.state; 64 | let error = (authError !== null) ? ( 65 |
    66 |
    67 | {authError} 68 |
    69 |
    70 | ) : null; 71 | 72 | return ( 73 |
    74 |
    75 | SiriDB Logo 79 |
    80 |
    81 |
    82 |
    83 |
    84 | 93 |
    94 |
    95 |
    96 |
    97 | 105 | 106 | 113 | 114 |
    115 |
    116 |
    117 | {error} 118 |
    119 |
    120 | ); 121 | } 122 | } 123 | 124 | export default withStores(Auth); -------------------------------------------------------------------------------- /src/Components/Insert/Insert.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import {withVlow} from 'vlow'; 4 | import moment from 'moment'; 5 | 6 | import InsertStore from '../../Stores/InsertStore'; 7 | import InsertActions from '../../Actions/InsertActions'; 8 | import DatabaseStore from '../../Stores/DatabaseStore'; 9 | 10 | 11 | const SELECT_ALL = -1; 12 | const withStores = withVlow([InsertStore, DatabaseStore]); 13 | 14 | class Insert extends React.Component { 15 | 16 | static propTypes = { 17 | /* DatabaseStore properties */ 18 | factor: PropTypes.number.isRequired, 19 | 20 | /* InsertStore properties */ 21 | alert: PropTypes.shape({ 22 | severity: PropTypes.oneOf(['success', 'warning', 'error']), 23 | message: PropTypes.string, 24 | }), 25 | sending: PropTypes.bool.isRequired, 26 | } 27 | 28 | static defaultProps = { 29 | alert: null, 30 | } 31 | 32 | constructor(props) { 33 | super(props); 34 | 35 | // set initial value, this.state.factor is required 36 | const val = Math.floor((Math.random() * 100) + 1); 37 | 38 | this.state = { 39 | data: `{\n\t"series-001": [\n\t\t[${this._now()}, ${val}]\n\t]\n}` 40 | }; 41 | 42 | // set inital cursor pos 43 | this.cursorPos = null; 44 | } 45 | 46 | shouldComponentUpdate() { 47 | return true; 48 | } 49 | 50 | componentDidUpdate() { 51 | if (this.cursorPos !== null) { 52 | this.inp.focus(); 53 | if (this.cursorPos === SELECT_ALL) { 54 | this.inp.selectionStart = 0; 55 | } else { 56 | this.inp.selectionStart = this.inp.selectionEnd = this.cursorPos; 57 | } 58 | this.cursorPos = null; 59 | } 60 | } 61 | 62 | _now() { 63 | const {factor} = this.props; 64 | return Math.floor(moment().format('x') / factor); 65 | } 66 | 67 | handleInpChange = (event) => { 68 | InsertActions.clearAlert(); 69 | this.setState({ 70 | data: event.target.value, 71 | }); 72 | } 73 | 74 | handleKeyDown = (event) => { 75 | if (event.key === 'Tab') { 76 | event.preventDefault(); 77 | this.setState((prevState) => ({ 78 | data: prevState.data.slice(0, this.inp.selectionStart) + '\t' + prevState.data.slice(this.inp.selectionEnd) 79 | })); 80 | this.cursorPos = this.inp.selectionStart + 1; 81 | } 82 | } 83 | 84 | handleInsert = () => { 85 | const {data} = this.state; 86 | this.cursorPos = SELECT_ALL; 87 | InsertActions.insert(data); 88 | } 89 | 90 | handleClearAlert = InsertActions.clearAlert; 91 | 92 | mapRef = (el) => { 93 | this.inp = el; 94 | } 95 | 96 | render() { 97 | const {alert, sending} = this.props; 98 | const {data} = this.state; 99 | let hasError = false; 100 | try { 101 | JSON.parse(data || '{}'); 102 | } catch (e) { 103 | hasError = true; 104 | } 105 | const alertComp = (alert !== null) ? ( 106 |
    107 |
    108 | 112 | {'×'} 113 | 114 | {alert.message} 115 |
    116 |
    117 | ) : null; 118 | return ( 119 |
    120 |
    121 |
    122 |
    123 |