├── .gitignore ├── static ├── img │ ├── favicon.ico │ ├── starred_24.svg │ └── unstarred_24.svg ├── README ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── css │ ├── tryit.css │ ├── local.css │ ├── xterm.css │ └── bootstrap-theme.min.css ├── js │ ├── xterm-addon-fit.js │ ├── navigation-2.js │ ├── xterm-addon-attach.js │ ├── bootstrap-rating.min.js │ ├── tryit.js │ └── bootstrap.min.js └── index.html ├── Makefile ├── AUTHORS ├── cmd └── incus-demo-server │ ├── api_terms.go │ ├── rwc.go │ ├── utils.go │ ├── api_statistics.go │ ├── api_status.go │ ├── proxy.go │ ├── config.go │ ├── api_feedback.go │ ├── main.go │ ├── api_session.go │ ├── session.go │ └── db.go ├── .github └── workflows │ └── commits.yml ├── config.yaml.example ├── README.md ├── go.mod ├── CONTRIBUTING.md ├── COPYING └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | /incus-demo-server 2 | /database.sqlite3 3 | /config.yaml 4 | *.swp 5 | -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxc/incus-demo-server/HEAD/static/img/favicon.ico -------------------------------------------------------------------------------- /static/README: -------------------------------------------------------------------------------- 1 | This is a minimal copy of the try-it page from the Incus website taken 2 | from https://github.com/lxc/linuxcontainers.org 3 | -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxc/incus-demo-server/HEAD/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxc/incus-demo-server/HEAD/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxc/incus-demo-server/HEAD/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default 2 | default: 3 | gofmt -s -w . 4 | go install -v ./... 5 | 6 | .PHONY: update-gomod 7 | update-gomod: 8 | go get -t -v -d -u ./... 9 | go mod tidy -go=1.24 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Unless mentioned otherwise in a specific file's header, all code in this 2 | project is released under the Apache 2.0 license. 3 | 4 | The list of authors and contributors can be retrieved from the git 5 | commit history and in some cases, the file headers. 6 | -------------------------------------------------------------------------------- /cmd/incus-demo-server/api_terms.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func restTermsHandler(w http.ResponseWriter, r *http.Request) { 9 | if r.Method != "GET" { 10 | http.Error(w, "Not implemented", 501) 11 | return 12 | } 13 | 14 | w.Header().Set("Access-Control-Allow-Origin", "*") 15 | w.Header().Set("Content-Type", "application/json") 16 | 17 | // Generate the response. 18 | body := make(map[string]interface{}) 19 | body["hash"] = config.Server.termsHash 20 | body["terms"] = config.Server.Terms 21 | 22 | err := json.NewEncoder(w).Encode(body) 23 | if err != nil { 24 | http.Error(w, "Internal server error", 500) 25 | return 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/commits.yml: -------------------------------------------------------------------------------- 1 | name: Commits 2 | on: 3 | - pull_request 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | dco-check: 10 | permissions: 11 | pull-requests: read # for tim-actions/get-pr-commits to get list of commits from the PR 12 | name: Signed-off-by (DCO) 13 | runs-on: ubuntu-20.04 14 | steps: 15 | - name: Get PR Commits 16 | id: 'get-pr-commits' 17 | uses: tim-actions/get-pr-commits@master 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: Check that all commits are signed-off 22 | uses: tim-actions/dco@master 23 | with: 24 | commits: ${{ steps.get-pr-commits.outputs.commits }} 25 | 26 | target-branch: 27 | permissions: 28 | contents: none 29 | name: Branch target 30 | runs-on: ubuntu-20.04 31 | steps: 32 | - name: Check branch target 33 | env: 34 | TARGET: ${{ github.event.pull_request.base.ref }} 35 | run: | 36 | set -x 37 | [ "${TARGET}" = "master" ] && exit 0 38 | 39 | echo "Invalid branch target: ${TARGET}" 40 | exit 1 41 | -------------------------------------------------------------------------------- /static/css/tryit.css: -------------------------------------------------------------------------------- 1 | .xterm { 2 | padding: 5px; 3 | } 4 | 5 | div#tryit_console_row { 6 | position: fixed; 7 | bottom: 0; 8 | z-index: 10; 9 | min-width: 100%; 10 | padding: 0; 11 | background: #fff; 12 | } 13 | 14 | div#tryit_console_panel { 15 | width: 100%; 16 | margin-right: auto; 17 | margin-left: auto; 18 | } 19 | 20 | div#tryit_console { 21 | max-height: 20em; 22 | } 23 | 24 | div#tryit-instructions { 25 | padding-bottom: 380px; 26 | } 27 | 28 | div#tryit_info_panel th { 29 | width: 15em; 30 | } 31 | 32 | div#tryit_info_panel table { 33 | margin-bottom: 0; 34 | } 35 | 36 | .p-notification__response { 37 | width: 100%; 38 | } 39 | 40 | div#tryit_examples_panel pre, 41 | div#tryit_examples_panel h3 { 42 | margin-bottom: 0; 43 | } 44 | 45 | div#tryit_examples_panel p { 46 | margin-bottom: 0.5rem; 47 | max-width: 100%; 48 | } 49 | 50 | .tryit_progress_bar { 51 | margin-top: 1em; 52 | } 53 | 54 | #tryit_progress button { 55 | margin-right: 0.2em; 56 | } 57 | 58 | @media (max-width: 500px) { 59 | .tryit_progress_bar { 60 | display: none; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /static/js/xterm-addon-fit.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})())); 2 | //# sourceMappingURL=xterm-addon-fit.js.map -------------------------------------------------------------------------------- /static/js/navigation-2.js: -------------------------------------------------------------------------------- 1 | function toggleDropdown(toggle, open) { 2 | var parentElement = toggle.parentNode; 3 | var dropdown = document.getElementById(toggle.getAttribute('aria-controls')); 4 | dropdown.setAttribute('aria-hidden', !open); 5 | 6 | if (open) { 7 | parentElement.classList.add('is-active'); 8 | } else { 9 | parentElement.classList.remove('is-active'); 10 | } 11 | } 12 | 13 | function closeAllDropdowns(toggles) { 14 | toggles.forEach(function (toggle) { 15 | toggleDropdown(toggle, false); 16 | }); 17 | } 18 | 19 | function handleClickOutside(toggles, containerClass) { 20 | document.addEventListener('click', function (event) { 21 | var target = event.target; 22 | 23 | if (target.closest) { 24 | if (!target.closest(containerClass)) { 25 | closeAllDropdowns(toggles); 26 | } 27 | } 28 | }); 29 | } 30 | 31 | function initNavDropdowns(containerClass) { 32 | var toggles = [].slice.call(document.querySelectorAll(containerClass + ' [aria-controls]')); 33 | 34 | handleClickOutside(toggles, containerClass); 35 | 36 | toggles.forEach(function (toggle) { 37 | toggle.addEventListener('click', function (e) { 38 | e.preventDefault(); 39 | 40 | const wasOpen = toggle.parentElement.classList.contains('is-active'); 41 | closeAllDropdowns(toggles); 42 | if (!wasOpen) { 43 | toggleDropdown(toggle, true); 44 | } 45 | }); 46 | }); 47 | } 48 | 49 | initNavDropdowns('.p-navigation__item--dropdown-toggle') 50 | -------------------------------------------------------------------------------- /cmd/incus-demo-server/rwc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | 7 | "github.com/gorilla/websocket" 8 | ) 9 | 10 | // wsWrapper implements ReadWriteCloser on top of a websocket connection. 11 | type wsWrapper struct { 12 | conn *websocket.Conn 13 | reader io.Reader 14 | mur sync.Mutex 15 | muw sync.Mutex 16 | } 17 | 18 | func (w *wsWrapper) Read(p []byte) (n int, err error) { 19 | w.mur.Lock() 20 | defer w.mur.Unlock() 21 | 22 | // Get new message if no active one. 23 | if w.reader == nil { 24 | var mt int 25 | 26 | mt, w.reader, err = w.conn.NextReader() 27 | if err != nil { 28 | if websocket.IsCloseError(err, websocket.CloseNormalClosure) { 29 | return 0, io.EOF 30 | } 31 | 32 | return 0, err 33 | } 34 | 35 | if mt == websocket.CloseMessage { 36 | w.reader = nil // At the end of the message, reset reader. 37 | 38 | return 0, io.EOF 39 | } 40 | } 41 | 42 | // Perform the read itself. 43 | n, err = w.reader.Read(p) 44 | if err != nil { 45 | w.reader = nil // At the end of the message, reset reader. 46 | 47 | if err == io.EOF { 48 | return n, nil // Don't return EOF error at end of message. 49 | } 50 | 51 | return n, err 52 | } 53 | 54 | return n, nil 55 | } 56 | 57 | func (w *wsWrapper) Write(p []byte) (int, error) { 58 | w.muw.Lock() 59 | defer w.muw.Unlock() 60 | 61 | // Send the data as a text message. 62 | err := w.conn.WriteMessage(websocket.TextMessage, p) 63 | if err != nil { 64 | return 0, err 65 | } 66 | 67 | return len(p), nil 68 | } 69 | -------------------------------------------------------------------------------- /static/js/xterm-addon-attach.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.AttachAddon=t():e.AttachAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;function s(e,t,s){return e.addEventListener(t,s),{dispose:()=>{s&&e.removeEventListener(t,s)}}}Object.defineProperty(t,"__esModule",{value:!0}),t.AttachAddon=void 0,t.AttachAddon=class{constructor(e,t){this._disposables=[],this._socket=e,this._socket.binaryType="arraybuffer",this._bidirectional=!(t&&!1===t.bidirectional)}activate(e){this._disposables.push(s(this._socket,"message",(t=>{const s=t.data;e.write("string"==typeof s?s:new Uint8Array(s))}))),this._bidirectional&&(this._disposables.push(e.onData((e=>this._sendData(e)))),this._disposables.push(e.onBinary((e=>this._sendBinary(e))))),this._disposables.push(s(this._socket,"close",(()=>this.dispose()))),this._disposables.push(s(this._socket,"error",(()=>this.dispose())))}dispose(){for(const e of this._disposables)e.dispose()}_sendData(e){this._checkOpenSocket()&&this._socket.send(e)}_sendBinary(e){if(!this._checkOpenSocket())return;const t=new Uint8Array(e.length);for(let s=0;s 39 | 45 | 46 | incus: 47 | client: 48 | certificate: |- 49 | PEM 50 | 51 | key: |- 52 | PEM 53 | project: PROJECT 54 | server: 55 | url: URL 56 | certificate: |- 57 | PEM 58 | target: some-server 59 | 60 | instance: 61 | allocate: 62 | count: 4 63 | expiry: 21600 64 | 65 | source: 66 | # instance: "try-it" 67 | image: "ubuntu/22.04" 68 | type: "virtual-machine" 69 | 70 | profiles: 71 | - default 72 | 73 | limits: 74 | cpu: 2 75 | disk: 50GiB 76 | processes: 2000 77 | memory: 4GiB 78 | 79 | session: 80 | command: ["bash"] 81 | expiry: 3000 82 | console_only: true 83 | network: ipv6 84 | -------------------------------------------------------------------------------- /cmd/incus-demo-server/api_statistics.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "slices" 8 | 9 | "github.com/lxc/incus/v6/shared/util" 10 | ) 11 | 12 | func restStatisticsHandler(w http.ResponseWriter, r *http.Request) { 13 | var err error 14 | 15 | if r.Method != "GET" { 16 | http.Error(w, "Not implemented", 501) 17 | return 18 | } 19 | 20 | w.Header().Set("Access-Control-Allow-Origin", "*") 21 | w.Header().Set("Content-Type", "application/json") 22 | 23 | // Validate API key. 24 | requestKey := r.FormValue("key") 25 | if !slices.Contains(config.Server.Statistics.Keys, requestKey) { 26 | http.Error(w, "Invalid authentication key", 401) 27 | return 28 | } 29 | 30 | // Unique host filtering. 31 | statsUnique := false 32 | requestUnique := r.FormValue("unique") 33 | if util.IsTrue(requestUnique) { 34 | statsUnique = true 35 | } 36 | 37 | // Time period filtering. 38 | requestPeriod := r.FormValue("period") 39 | if !slices.Contains([]string{"", "total", "current", "hour", "day", "week", "month", "year"}, requestPeriod) { 40 | http.Error(w, "Invalid period", 400) 41 | return 42 | } 43 | 44 | statsPeriod := requestPeriod 45 | 46 | if statsPeriod == "" { 47 | statsPeriod = "total" 48 | } 49 | 50 | // Network filtering. 51 | requestNetwork := r.FormValue("network") 52 | var statsNetwork *net.IPNet 53 | if requestNetwork != "" { 54 | _, statsNetwork, err = net.ParseCIDR(requestNetwork) 55 | if err != nil { 56 | http.Error(w, "Invalid network", 400) 57 | return 58 | } 59 | } 60 | 61 | // Query the database. 62 | count, err := dbGetStats(statsPeriod, statsUnique, statsNetwork) 63 | if err != nil { 64 | http.Error(w, "Unable to retrieve statistics", 500) 65 | return 66 | } 67 | 68 | // Return to client. 69 | w.Write([]byte(fmt.Sprintf("%d\n", count))) 70 | } 71 | -------------------------------------------------------------------------------- /cmd/incus-demo-server/api_status.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | type serverStatusCode int 9 | 10 | const ( 11 | serverOperational serverStatusCode = 0 12 | serverMaintenance serverStatusCode = 1 13 | ) 14 | 15 | func restStatusHandler(w http.ResponseWriter, r *http.Request) { 16 | if r.Method != "GET" { 17 | http.Error(w, "Not implemented", 501) 18 | return 19 | } 20 | 21 | w.Header().Set("Access-Control-Allow-Origin", "*") 22 | w.Header().Set("Content-Type", "application/json") 23 | 24 | var failure bool 25 | 26 | // Parse the remote client information. 27 | address, protocol, err := restClientIP(r) 28 | if err != nil { 29 | http.Error(w, "Internal server error", 500) 30 | return 31 | } 32 | 33 | // Get some instance data. 34 | var instanceCount int 35 | var instanceNext int 36 | 37 | instanceCount, err = dbActiveCount() 38 | if err != nil { 39 | failure = true 40 | } 41 | 42 | if instanceCount >= config.Server.Limits.Total { 43 | instanceNext, err = dbNextExpire() 44 | if err != nil { 45 | failure = true 46 | } 47 | } 48 | 49 | // Generate the response. 50 | body := make(map[string]interface{}) 51 | body["client_address"] = address 52 | body["client_protocol"] = protocol 53 | body["feedback"] = config.Server.Feedback.Enabled 54 | body["session_console_only"] = config.Session.ConsoleOnly 55 | body["session_network"] = config.Session.Network 56 | if !config.Server.Maintenance.Enabled && !failure && incusDaemon != nil { 57 | body["server_status"] = serverOperational 58 | } else { 59 | body["server_status"] = serverMaintenance 60 | body["server_message"] = config.Server.Maintenance.Message 61 | } 62 | body["instance_count"] = instanceCount 63 | body["instance_max"] = config.Server.Limits.Total 64 | body["instance_next"] = instanceNext 65 | 66 | err = json.NewEncoder(w).Encode(body) 67 | if err != nil { 68 | http.Error(w, "Internal server error", 500) 69 | return 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cmd/incus-demo-server/proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "io" 7 | "net" 8 | "os" 9 | "strings" 10 | "sync" 11 | ) 12 | 13 | func proxyListener() { 14 | l, err := net.Listen("tcp", config.Server.Proxy.Address) 15 | if err != nil { 16 | fmt.Fprintf(os.Stderr, "proxy: Failed to start listener: %v\n", err) 17 | return 18 | } 19 | 20 | for { 21 | conn, err := l.Accept() 22 | if err != nil { 23 | continue 24 | } 25 | 26 | go proxyHandle(conn) 27 | } 28 | } 29 | 30 | func proxyHandle(conn net.Conn) { 31 | defer conn.Close() 32 | 33 | // Establish TLS. 34 | var target string 35 | 36 | tlsConfig := &tls.Config{ 37 | GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { 38 | target = info.ServerName 39 | 40 | cert, err := tls.X509KeyPair([]byte(config.Server.Proxy.Certificate), []byte(config.Server.Proxy.Key)) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return &cert, nil 46 | }, 47 | 48 | GetClientCertificate: func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) { 49 | cert, err := tls.X509KeyPair([]byte(config.Incus.Client.Certificate), []byte(config.Incus.Client.Key)) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return &cert, nil 55 | }, 56 | 57 | InsecureSkipVerify: true, 58 | } 59 | 60 | tlsConn := tls.Server(conn, tlsConfig) 61 | err := tlsConn.Handshake() 62 | if err != nil { 63 | return 64 | } 65 | 66 | id := strings.Split(target, ".")[0] 67 | 68 | // Get the instance. 69 | sessionId, _, instanceIP, _, _, _, err := dbGetInstance(id, true) 70 | if err != nil || sessionId == -1 || instanceIP == "" { 71 | return 72 | } 73 | 74 | // Connect to Incus. 75 | backendConn, err := tls.Dial("tcp", net.JoinHostPort(instanceIP, "8443"), tlsConfig) 76 | if err != nil { 77 | return 78 | } 79 | defer backendConn.Close() 80 | 81 | var wg sync.WaitGroup 82 | wg.Add(2) 83 | 84 | go func() { 85 | io.Copy(tlsConn, backendConn) 86 | tlsConn.CloseWrite() 87 | wg.Done() 88 | }() 89 | 90 | go func() { 91 | io.Copy(backendConn, tlsConn) 92 | backendConn.CloseWrite() 93 | wg.Done() 94 | }() 95 | 96 | wg.Wait() 97 | } 98 | -------------------------------------------------------------------------------- /cmd/incus-demo-server/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type serverConfig struct { 4 | Server struct { 5 | API struct { 6 | Address string `yaml:"address"` 7 | } `yaml:"api"` 8 | 9 | Blocklist []string `yaml:"blocklist"` 10 | 11 | Feedback struct { 12 | Enabled bool `yaml:"enabled"` 13 | Timeout int `yaml:"timeout"` 14 | Email struct { 15 | Server string `yaml:"server"` 16 | From string `yaml:"from"` 17 | To string `yaml:"to"` 18 | Subject string `yaml:"subject"` 19 | } `yaml:"email"` 20 | } `yaml:"feedback"` 21 | 22 | Limits struct { 23 | Total int `yaml:"total"` 24 | IP int `yaml:"ip"` 25 | } `yaml:"limits"` 26 | 27 | Maintenance struct { 28 | Enabled bool `yaml:"enabled"` 29 | Message string `yaml:"message"` 30 | } `yaml:"maintenance"` 31 | 32 | Proxy struct { 33 | Address string `yaml:"address"` 34 | Certificate string `yaml:"certificate"` 35 | Key string `yaml:"key"` 36 | } `yaml:"proxy"` 37 | 38 | Statistics struct { 39 | Keys []string `yaml:"keys"` 40 | } `yaml:"statistics"` 41 | 42 | Terms string `yaml:"terms"` 43 | termsHash string 44 | } `yaml:"server"` 45 | 46 | Incus struct { 47 | Client struct { 48 | Certificate string `yaml:"certificate"` 49 | Key string `yaml:"key"` 50 | } 51 | 52 | Project string `yaml:"project"` 53 | 54 | Server struct { 55 | Certificate string `yaml:"certificate"` 56 | URL string `yaml:"url"` 57 | } `yaml:"server"` 58 | 59 | Target string `yaml:"target"` 60 | } `yaml:"incus"` 61 | 62 | Instance struct { 63 | Allocate struct { 64 | Count int `yaml:"count"` 65 | Expiry int `yaml:"expiry"` 66 | } `yaml:"allocate"` 67 | 68 | Source struct { 69 | Instance string `yaml:"instance"` 70 | Image string `yaml:"image"` 71 | InstanceType string `yaml:"type"` 72 | } `yaml:"source"` 73 | 74 | Profiles []string `yaml:"profiles"` 75 | 76 | Limits struct { 77 | CPU int `yaml:"cpu"` 78 | Disk string `yaml:"disk"` 79 | Processes int `yaml:"processes"` 80 | Memory string `yaml:"memory"` 81 | } `yaml:"limits"` 82 | } `yaml:"instance"` 83 | 84 | Session struct { 85 | Command []string `yaml:"command"` 86 | ReadyCommand []string `yaml:"ready_command"` 87 | Expiry int `yaml:"expiry"` 88 | ConsoleOnly bool `yaml:"console_only"` 89 | Network string `yaml:"network"` 90 | } `yaml:"session"` 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Incus demo server 2 | 3 | This repository contains the backend code of the Incus online demo service. 4 | 5 | [https://linuxcontainers.org/incus/try-it](https://linuxcontainers.org/incus/try-it) 6 | 7 | ## What is it 8 | 9 | Simply put, it's a small Go daemon exposing a REST API that users 10 | (mostly our javascript client) can interact with to create temporary 11 | test instances and attach to that instance's console. 12 | 13 | Those instances come with a bunch of resource limitations and an 14 | expiry, when the instance expires, it's automatically deleted. 15 | 16 | The main client can be found at the URL above, with its source available here: 17 | [https://github.com/lxc/linuxcontainers.org](https://github.com/lxc/linuxcontainers.org) 18 | 19 | ## Dependencies 20 | 21 | The server needs to be able to talk to an Incus daemon over the local unix 22 | socket or a remote HTTPS connection, so you need to have a Incus daemon 23 | installed and functional before using this server. 24 | 25 | Other than that, you can build the daemon with: 26 | 27 | go install github.com/lxc/incus-demo-server/cmd/incus-demo-server@latest 28 | 29 | ## Running it 30 | 31 | To run your own, you should start by copying the example configuration 32 | file "config.yaml.example" to "config.yaml", then update its content 33 | according to your environment. 34 | 35 | You will either need an instance to copy for every request or an 36 | instance image to use, set that up and set the appropriate 37 | configuration key. 38 | 39 | Once done, simply run the daemon with: 40 | 41 | ./incus-demo-server 42 | 43 | The daemon isn't verbose at all, in fact it will only log critical Incus errors. 44 | 45 | You can test things with: 46 | 47 | curl http://localhost:8080/1.0 48 | curl http://localhost:8080/1.0/terms 49 | 50 | The server monitors the current directory for changes to its configuration file. 51 | It will automatically reload the configuration after it's changed. 52 | 53 | ## Bug reports 54 | 55 | Bug reports can be filed at https://github.com/lxc/incus-demo-server/issues/new 56 | 57 | ## Contributing 58 | 59 | Fixes and new features are greatly appreciated but please read our 60 | [contributing guidelines](CONTRIBUTING.md) first. 61 | 62 | Contributions to this project should be sent as pull requests on github. 63 | 64 | ## Support and discussions 65 | 66 | We use the LXC mailing-lists for developer and user discussions, you can 67 | find and subscribe to those at: https://lists.linuxcontainers.org 68 | 69 | If you prefer live discussions, some of us also hang out in 70 | [#lxc](https://web.libera.chat/#lxc) on libera.chat. 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lxc/incus-demo-server 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.4 6 | 7 | require ( 8 | github.com/fsnotify/fsnotify v1.9.0 9 | github.com/gorilla/mux v1.8.1 10 | github.com/gorilla/websocket v1.5.3 11 | github.com/lxc/incus/v6 v6.14.0 12 | github.com/mattn/go-sqlite3 v1.14.29 13 | github.com/pborman/uuid v1.2.1 14 | gopkg.in/yaml.v3 v3.0.1 15 | ) 16 | 17 | require ( 18 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect 19 | github.com/apex/log v1.9.0 // indirect 20 | github.com/blang/semver/v4 v4.0.0 // indirect 21 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 22 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 23 | github.com/docker/go-units v0.5.0 // indirect 24 | github.com/go-jose/go-jose/v4 v4.1.1 // indirect 25 | github.com/go-logr/logr v1.4.3 // indirect 26 | github.com/go-logr/stdr v1.2.2 // indirect 27 | github.com/google/uuid v1.6.0 // indirect 28 | github.com/gorilla/securecookie v1.1.2 // indirect 29 | github.com/klauspost/compress v1.18.0 // indirect 30 | github.com/klauspost/pgzip v1.2.6 // indirect 31 | github.com/kr/fs v0.1.0 // indirect 32 | github.com/moby/sys/user v0.4.0 // indirect 33 | github.com/moby/sys/userns v0.1.0 // indirect 34 | github.com/muhlemmer/gu v0.3.1 // indirect 35 | github.com/opencontainers/go-digest v1.0.0 // indirect 36 | github.com/opencontainers/image-spec v1.1.1 // indirect 37 | github.com/opencontainers/runtime-spec v1.2.1 // indirect 38 | github.com/opencontainers/umoci v0.5.0 // indirect 39 | github.com/pkg/errors v0.9.1 // indirect 40 | github.com/pkg/sftp v1.13.9 // indirect 41 | github.com/rootless-containers/proto/go-proto v0.0.0-20230421021042-4cd87ebadd67 // indirect 42 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 43 | github.com/sirupsen/logrus v1.9.3 // indirect 44 | github.com/urfave/cli v1.22.17 // indirect 45 | github.com/vbatts/go-mtree v0.5.4 // indirect 46 | github.com/zitadel/logging v0.6.2 // indirect 47 | github.com/zitadel/oidc/v3 v3.42.0 // indirect 48 | github.com/zitadel/schema v1.3.1 // indirect 49 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 50 | go.opentelemetry.io/otel v1.37.0 // indirect 51 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 52 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 53 | golang.org/x/crypto v0.40.0 // indirect 54 | golang.org/x/net v0.42.0 // indirect 55 | golang.org/x/oauth2 v0.30.0 // indirect 56 | golang.org/x/sys v0.34.0 // indirect 57 | golang.org/x/term v0.33.0 // indirect 58 | golang.org/x/text v0.27.0 // indirect 59 | google.golang.org/protobuf v1.36.6 // indirect 60 | gopkg.in/yaml.v2 v2.4.0 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Pull requests: 2 | 3 | Changes to this project should be proposed as pull requests on Github 4 | at: https://github.com/lxc/incus-demo-server 5 | 6 | Proposed changes will then go through code review there and once acked, 7 | be merged in the main branch. 8 | 9 | 10 | # License and copyright: 11 | 12 | By default, any contribution to this project is made under the Apache 13 | 2.0 license. 14 | 15 | The author of a change remains the copyright holder of their code 16 | (no copyright assignment). 17 | 18 | 19 | # Developer Certificate of Origin: 20 | 21 | To improve tracking of contributions to this project we use the DCO 1.1 22 | and use a "sign-off" procedure for all changes going into the branch. 23 | 24 | The sign-off is a simple line at the end of the explanation for the 25 | commit which certifies that you wrote it or otherwise have the right 26 | to pass it on as an open-source contribution. 27 | 28 | > Developer Certificate of Origin 29 | > Version 1.1 30 | > 31 | > Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 32 | > 660 York Street, Suite 102, 33 | > San Francisco, CA 94110 USA 34 | > 35 | > Everyone is permitted to copy and distribute verbatim copies of this 36 | > license document, but changing it is not allowed. 37 | > 38 | > Developer's Certificate of Origin 1.1 39 | > 40 | > By making a contribution to this project, I certify that: 41 | > 42 | > (a) The contribution was created in whole or in part by me and I 43 | > have the right to submit it under the open source license 44 | > indicated in the file; or 45 | > 46 | > (b) The contribution is based upon previous work that, to the best 47 | > of my knowledge, is covered under an appropriate open source 48 | > license and I have the right under that license to submit that 49 | > work with modifications, whether created in whole or in part 50 | > by me, under the same open source license (unless I am 51 | > permitted to submit under a different license), as indicated 52 | > in the file; or 53 | > 54 | > (c) The contribution was provided directly to me by some other 55 | > person who certified (a), (b) or (c) and I have not modified 56 | > it. 57 | > 58 | > (d) I understand and agree that this project and the contribution 59 | > are public and that a record of the contribution (including all 60 | > personal information I submit with it, including my sign-off) is 61 | > maintained indefinitely and may be redistributed consistent with 62 | > this project or the open source license(s) involved. 63 | 64 | An example of a valid sign-off line is: 65 | 66 | Signed-off-by: Random J Developer 67 | 68 | Use your real name and a valid e-mail address. 69 | Sorry, no pseudonyms or anonymous contributions are allowed. 70 | 71 | We also require each commit be individually signed-off by their author, 72 | even when part of a larger set. You may find `git commit -s` useful. 73 | -------------------------------------------------------------------------------- /static/js/bootstrap-rating.min.js: -------------------------------------------------------------------------------- 1 | // bootstrap-rating - v1.4.0 - (c) 2016 dreyescat 2 | // https://github.com/dreyescat/bootstrap-rating MIT 3 | !function(a,b){"use strict";function c(c,e){this.$input=a(c),this.$rating=a("").css({cursor:"default"}).insertBefore(this.$input),this.options=function(c){return c.start=parseInt(c.start,10),c.start=isNaN(c.start)?b:c.start,c.stop=parseInt(c.stop,10),c.stop=isNaN(c.stop)?c.start+d||b:c.stop,c.step=parseInt(c.step,10)||b,c.fractions=Math.abs(parseInt(c.fractions,10))||b,c.scale=Math.abs(parseInt(c.scale,10))||b,c=a.extend({},a.fn.rating.defaults,c),c.filledSelected=c.filledSelected||c.filled,c}(a.extend({},this.$input.data(),e)),this._init()}var d=5;c.prototype={_init:function(){for(var c=this,d=this.$input,e=this.$rating,f=function(a){return function(c){d.prop("disabled")||d.prop("readonly")||d.data("readonly")!==b||a.call(this,c)}},g=1;g<=this._rateToIndex(this.options.stop);g++){var h=a('
').css({display:"inline-block",position:"relative"});a('
').appendTo(h),a('
').append("").css({display:"inline-block",position:"absolute",overflow:"hidden",left:0,right:0,width:0}).appendTo(h),e.append(h),this.options.extendSymbol.call(h,this._indexToRate(g))}this._updateRate(d.val()),d.on("change",function(){c._updateRate(a(this).val())});var i,j=function(b){var d=a(b.currentTarget),e=Math.abs((b.pageX||b.originalEvent.touches[0].pageX)-(("rtl"===d.css("direction")&&d.width())+d.offset().left));return e=e>0?e:.1*c.options.scale,d.index()+e/d.width()};e.on("mousedown touchstart",".rating-symbol",f(function(a){d.val(c._indexToRate(j(a))).change()})).on("mousemove touchmove",".rating-symbol",f(function(d){var e=c._roundToFraction(j(d));e!==i&&(i!==b&&a(this).trigger("rating.rateleave"),i=e,a(this).trigger("rating.rateenter",[c._indexToRate(i)])),c._fillUntil(e)})).on("mouseleave touchend",".rating-symbol",f(function(){i=b,a(this).trigger("rating.rateleave"),c._fillUntil(c._rateToIndex(parseFloat(d.val())))}))},_fillUntil:function(a){var b=this.$rating,c=Math.floor(a);b.find(".rating-symbol-background").css("visibility","visible").slice(0,c).css("visibility","hidden");var d=b.find(".rating-symbol-foreground");d.width(0),d.slice(0,c).width("auto").find("span").attr("class",this.options.filled),d.eq(a%1?c:c-1).find("span").attr("class",this.options.filledSelected),d.eq(c).width(a%1*100+"%")},_indexToRate:function(a){return this.options.start+Math.floor(a)*this.options.step+this.options.step*this._roundToFraction(a%1)},_rateToIndex:function(a){return(a-this.options.start)/this.options.step},_roundToFraction:function(a){var b=Math.ceil(a%1*this.options.fractions)/this.options.fractions,c=Math.pow(10,this.options.scale);return Math.floor(a)+Math.floor(b*c)/c},_contains:function(a){var b=this.options.step>0?this.options.start:this.options.stop,c=this.options.step>0?this.options.stop:this.options.start;return b<=a&&a<=c},_updateRate:function(a){var b=parseFloat(a);this._contains(b)?(this._fillUntil(this._rateToIndex(b)),this.$input.val(b)):""===a&&(this._fillUntil(0),this.$input.val(""))},rate:function(a){return a===b?this.$input.val():void this._updateRate(a)}},a.fn.rating=function(d){var e,f=Array.prototype.slice.call(arguments,1);return this.each(function(){var b=a(this),g=b.data("rating");g||b.data("rating",g=new c(this,d)),"string"==typeof d&&"_"!==d[0]&&(e=g[d].apply(g,f))}),e!==b?e:this},a.fn.rating.defaults={filled:"glyphicon glyphicon-star",filledSelected:b,empty:"glyphicon glyphicon-star-empty",start:0,stop:d,step:1,fractions:1,scale:3,extendSymbol:function(a){}},a(function(){a("input.rating").rating()})}(jQuery); -------------------------------------------------------------------------------- /static/img/starred_24.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 50 | 55 | 56 | 58 | 59 | 61 | image/svg+xml 62 | 64 | 65 | 66 | 67 | 68 | 73 | 77 | 81 | 88 | 92 | 97 | 101 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /cmd/incus-demo-server/api_feedback.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/smtp" 8 | "strings" 9 | "text/template" 10 | "time" 11 | ) 12 | 13 | type Feedback struct { 14 | Rating int `json:"rating"` 15 | Email string `json:"email"` 16 | EmailUse int `json:"email_use"` 17 | Message string `json:"message"` 18 | } 19 | 20 | func restFeedbackHandler(w http.ResponseWriter, r *http.Request) { 21 | if !config.Server.Feedback.Enabled { 22 | http.Error(w, "Feedback reporting is disabled", 400) 23 | return 24 | } 25 | 26 | if r.Method == "POST" { 27 | restFeedbackPostHandler(w, r) 28 | return 29 | } 30 | 31 | if r.Method == "GET" { 32 | restFeedbackGetHandler(w, r) 33 | return 34 | } 35 | 36 | if r.Method == "OPTIONS" { 37 | origin := r.Header.Get("Origin") 38 | if origin != "" { 39 | w.Header().Set("Access-Control-Allow-Origin", "*") 40 | w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") 41 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type") 42 | } 43 | 44 | return 45 | } 46 | 47 | http.Error(w, "Not implemented", 501) 48 | } 49 | 50 | func restFeedbackPostHandler(w http.ResponseWriter, r *http.Request) { 51 | w.Header().Set("Access-Control-Allow-Origin", "*") 52 | w.Header().Set("Content-Type", "application/json") 53 | 54 | // Get the id argument. 55 | id := r.FormValue("id") 56 | if id == "" { 57 | http.Error(w, "Missing session id", 400) 58 | return 59 | } 60 | 61 | // Get the instance. 62 | sessionId, _, _, _, _, sessionExpiry, err := dbGetInstance(id, false) 63 | if err != nil || sessionId == -1 { 64 | http.Error(w, "Session not found", 404) 65 | return 66 | } 67 | 68 | // Check if we can still store feedback. 69 | if time.Now().Unix() > sessionExpiry+int64(config.Server.Feedback.Timeout*60) { 70 | http.Error(w, "Feedback timeout has been reached", 400) 71 | return 72 | } 73 | 74 | // Parse request. 75 | feedback := Feedback{} 76 | 77 | err = json.NewDecoder(r.Body).Decode(&feedback) 78 | if err != nil { 79 | http.Error(w, "Invalid JSON data", 400) 80 | return 81 | } 82 | 83 | err = dbRecordFeedback(sessionId, feedback) 84 | if err != nil { 85 | http.Error(w, "Unable to record feedback data", 500) 86 | return 87 | } 88 | 89 | if config.Server.Feedback.Email.Server != "" { 90 | go emailFeedback(feedback) 91 | } 92 | 93 | return 94 | } 95 | 96 | var emailTpl = template.Must(template.New("emailTpl").Parse(`From: {{ .from }} 97 | To: {{ .to }} 98 | Subject: {{ .subject }} 99 | 100 | You received some new user feedback from try-it. 101 | 102 | {{ if gt .rating 0 }}Rating: {{ .rating }} / 5 103 | {{ end }}{{- if .email }}E-mail: {{ .email }} 104 | {{ end }}{{- if .message }}Message: 105 | """ 106 | {{ .message }} 107 | """ 108 | {{- end }} 109 | `)) 110 | 111 | func emailFeedback(feedback Feedback) { 112 | if (feedback.Email == "" || feedback.EmailUse == 0) && feedback.Rating == 0 && feedback.Message == "" { 113 | return 114 | } 115 | 116 | data := map[string]any{ 117 | "from": config.Server.Feedback.Email.From, 118 | "to": config.Server.Feedback.Email.To, 119 | "subject": config.Server.Feedback.Email.Subject, 120 | "rating": feedback.Rating, 121 | "email": "", 122 | "message": feedback.Message, 123 | } 124 | 125 | if feedback.EmailUse > 0 { 126 | data["email"] = feedback.Email 127 | } 128 | 129 | var sb *strings.Builder = &strings.Builder{} 130 | err := emailTpl.Execute(sb, data) 131 | if err != nil { 132 | fmt.Printf("error: %s\n", err) 133 | return 134 | } 135 | 136 | err = smtp.SendMail(config.Server.Feedback.Email.Server, nil, config.Server.Feedback.Email.From, []string{config.Server.Feedback.Email.To}, []byte(sb.String())) 137 | if err != nil { 138 | fmt.Printf("error: %s\n", err) 139 | return 140 | } 141 | } 142 | 143 | func restFeedbackGetHandler(w http.ResponseWriter, r *http.Request) { 144 | w.Header().Set("Access-Control-Allow-Origin", "*") 145 | w.Header().Set("Content-Type", "application/json") 146 | 147 | // Get the id argument. 148 | id := r.FormValue("id") 149 | if id == "" { 150 | http.Error(w, "Missing session id", 400) 151 | return 152 | } 153 | 154 | // Get the instance. 155 | sessionId, _, _, _, _, _, err := dbGetInstance(id, false) 156 | if err != nil || sessionId == -1 { 157 | http.Error(w, "Session not found", 404) 158 | return 159 | } 160 | 161 | // Get the feedback. 162 | feedbackId, feedbackRating, feedbackEmail, feedbackEmailUse, feedbackComment, err := dbGetFeedback(sessionId) 163 | if err != nil || feedbackId == -1 { 164 | http.Error(w, "No existing feedback", 404) 165 | return 166 | } 167 | 168 | // Generate the response. 169 | body := make(map[string]interface{}) 170 | body["rating"] = feedbackRating 171 | body["email"] = feedbackEmail 172 | body["email_use"] = feedbackEmailUse 173 | body["feedback"] = feedbackComment 174 | 175 | // Return to the client. 176 | err = json.NewEncoder(w).Encode(body) 177 | if err != nil { 178 | http.Error(w, "Internal server error", 500) 179 | return 180 | } 181 | 182 | return 183 | } 184 | -------------------------------------------------------------------------------- /static/css/local.css: -------------------------------------------------------------------------------- 1 | #navbar-title { 2 | color: white; 3 | } 4 | 5 | #breadcrumb-logo { 6 | height: 1em; 7 | display: inline; 8 | } 9 | 10 | a.text { 11 | color: inherit; 12 | text-decoration: inherit; 13 | } 14 | 15 | div.container p { 16 | font-size: 16px; 17 | } 18 | 19 | div.container pre { 20 | margin-left: 30px; 21 | } 22 | 23 | div.container p img { 24 | float: right; 25 | margin-left: auto; 26 | margin-right: auto; 27 | max-width: 30%; 28 | padding: 5px; 29 | } 30 | 31 | div.large_content p { 32 | font-size: 20px; 33 | } 34 | 35 | div.row p { 36 | font-size: 14px; 37 | } 38 | 39 | hr { 40 | margin-top: 40px; 41 | margin-bottom: 40px; 42 | } 43 | 44 | hr.footer { 45 | margin-bottom: 5px; 46 | } 47 | 48 | span.text-muted { 49 | white-space:nowrap; 50 | } 51 | 52 | div.codehilite pre { 53 | margin-left: 0; 54 | } 55 | 56 | div.manpage { 57 | max-width: 45em; 58 | } 59 | 60 | .dropdown-submenu { 61 | position:relative; 62 | } 63 | 64 | .dropdown-submenu>.dropdown-menu { 65 | top:0; 66 | left:100%; 67 | margin-top:-6px; 68 | margin-left:-1px; 69 | -webkit-border-radius:0 6px 6px 6px; 70 | -moz-border-radius:0 6px 6px 6px; 71 | border-radius:0 6px 6px 6px; 72 | } 73 | 74 | .dropdown-submenu:hover>.dropdown-menu { 75 | display:block; 76 | } 77 | 78 | .dropdown-submenu>a:after { 79 | display:block; 80 | content:" "; 81 | float:right; 82 | width:0; 83 | height:0; 84 | border-color:transparent; 85 | border-style:solid; 86 | border-width:5px 0 5px 5px; 87 | border-left-color:#cccccc; 88 | margin-top:5px; 89 | margin-right:-10px; 90 | } 91 | 92 | .dropdown-submenu:hover>a:after { 93 | border-left-color:#ffffff; 94 | } 95 | 96 | .dropdown-submenu.pull-left { 97 | float:none; 98 | } 99 | 100 | .dropdown-submenu.pull-left>.dropdown-menu { 101 | left:-100%; 102 | margin-left:10px; 103 | -webkit-border-radius:6px 0 6px 6px; 104 | -moz-border-radius:6px 0 6px 6px; 105 | border-radius:6px 0 6px 6px; 106 | } 107 | 108 | .tab-pane h3 { 109 | margin-top: 0; 110 | } 111 | 112 | .tab-pane nav .pager { 113 | margin-top: 5px; 114 | margin-bottom: 0; 115 | } 116 | 117 | .panel { 118 | margin-bottom: 10px; 119 | } 120 | 121 | /* paragraph anchors as generated by markdown toc */ 122 | a.headerlink { 123 | color:inherit; 124 | visibility:hidden; 125 | text-decoration:none; 126 | } 127 | h1:hover a.headerlink, 128 | h2:hover a.headerlink, 129 | h3:hover a.headerlink { 130 | visibility:visible; 131 | } 132 | 133 | 134 | @media screen and (max-width: 991px){ 135 | .columns{ 136 | height: 500px; 137 | overflow:scroll; 138 | } 139 | } 140 | 141 | @media screen and (min-width: 992px){ 142 | .columns{ 143 | -moz-column-count:2; /* Firefox */ 144 | -webkit-column-count:2; /* Safari and Chrome */ 145 | column-count:2; 146 | width: 500px; 147 | } 148 | } 149 | 150 | #tryit_console { 151 | display: table; 152 | margin: 0 auto; 153 | font-family: "DejaVu Sans Mono", "Liberation Mono", monospace; 154 | font-size: 11px; 155 | width: 100%; 156 | 157 | text-size-adjust: none; 158 | -moz-text-size-adjust: none; 159 | -webkit-text-size-adjust: none; 160 | -ms-text-size-adjust: none; 161 | } 162 | 163 | #tryit_console_measurement { 164 | color: white; 165 | display: block; 166 | float: left; 167 | border-color:black; 168 | border-style:solid; 169 | border-width:5px 5px 5px 5px; 170 | position:absolute; 171 | top:-1000px; 172 | } 173 | 174 | .tryit_run:hover { 175 | text-decoration: underline; 176 | cursor: pointer; 177 | } 178 | 179 | .btn { 180 | white-space: normal; 181 | } 182 | 183 | @keyframes spin { 184 | to { transform: rotate(1turn); } 185 | } 186 | 187 | .spinner { 188 | position: relative; 189 | display: inline-block; 190 | width: 5em; 191 | height: 5em; 192 | margin: 0 .5em; 193 | font-size: 12px; 194 | text-indent: 999em; 195 | overflow: hidden; 196 | animation: spin 1s infinite steps(8); 197 | } 198 | 199 | .small.spinner { 200 | font-size: 6px; 201 | } 202 | 203 | .large.spinner { 204 | font-size: 24px; 205 | } 206 | 207 | .spinner:before, 208 | .spinner:after, 209 | .spinner > div:before, 210 | .spinner > div:after { 211 | content: ''; 212 | position: absolute; 213 | top: 0; 214 | left: 2.25em; /* (container width - part width)/2 */ 215 | width: .5em; 216 | height: 1.5em; 217 | border-radius: .2em; 218 | background: #eee; 219 | box-shadow: 0 3.5em #eee; /* container height - part height */ 220 | transform-origin: 50% 2.5em; /* container height / 2 */ 221 | } 222 | 223 | .spinner:before { 224 | background: #555; 225 | } 226 | 227 | .spinner:after { 228 | transform: rotate(-45deg); 229 | background: #777; 230 | } 231 | 232 | .spinner > div:before { 233 | transform: rotate(-90deg); 234 | background: #999; 235 | } 236 | 237 | .spinner > div:after { 238 | transform: rotate(-135deg); 239 | background: #bbb; 240 | } 241 | -------------------------------------------------------------------------------- /static/img/unstarred_24.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 50 | 55 | 56 | 58 | 59 | 61 | image/svg+xml 62 | 64 | 65 | 66 | 67 | 68 | 73 | 77 | 81 | 88 | 92 | 97 | 101 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /static/css/xterm.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014 The xterm.js authors. All rights reserved. 3 | * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) 4 | * https://github.com/chjj/term.js 5 | * @license MIT 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | * 25 | * Originally forked from (with the author's permission): 26 | * Fabrice Bellard's javascript vt100 for jslinux: 27 | * http://bellard.org/jslinux/ 28 | * Copyright (c) 2011 Fabrice Bellard 29 | * The original design remains. The terminal itself 30 | * has been extended to include xterm CSI codes, among 31 | * other features. 32 | */ 33 | 34 | /** 35 | * Default styles for xterm.js 36 | */ 37 | 38 | .xterm { 39 | cursor: text; 40 | position: relative; 41 | user-select: none; 42 | -ms-user-select: none; 43 | -webkit-user-select: none; 44 | } 45 | 46 | .xterm.focus, 47 | .xterm:focus { 48 | outline: none; 49 | } 50 | 51 | .xterm .xterm-helpers { 52 | position: absolute; 53 | top: 0; 54 | /** 55 | * The z-index of the helpers must be higher than the canvases in order for 56 | * IMEs to appear on top. 57 | */ 58 | z-index: 5; 59 | } 60 | 61 | .xterm .xterm-helper-textarea { 62 | padding: 0; 63 | border: 0; 64 | margin: 0; 65 | /* Move textarea out of the screen to the far left, so that the cursor is not visible */ 66 | position: absolute; 67 | opacity: 0; 68 | left: -9999em; 69 | top: 0; 70 | width: 0; 71 | height: 0; 72 | z-index: -5; 73 | /** Prevent wrapping so the IME appears against the textarea at the correct position */ 74 | white-space: nowrap; 75 | overflow: hidden; 76 | resize: none; 77 | } 78 | 79 | .xterm .composition-view { 80 | /* TODO: Composition position got messed up somewhere */ 81 | background: #000; 82 | color: #FFF; 83 | display: none; 84 | position: absolute; 85 | white-space: nowrap; 86 | z-index: 1; 87 | } 88 | 89 | .xterm .composition-view.active { 90 | display: block; 91 | } 92 | 93 | .xterm .xterm-viewport { 94 | /* On OS X this is required in order for the scroll bar to appear fully opaque */ 95 | background-color: #000; 96 | overflow-y: scroll; 97 | cursor: default; 98 | position: absolute; 99 | right: 0; 100 | left: 0; 101 | top: 0; 102 | bottom: 0; 103 | } 104 | 105 | .xterm .xterm-screen { 106 | position: relative; 107 | } 108 | 109 | .xterm .xterm-screen canvas { 110 | position: absolute; 111 | left: 0; 112 | top: 0; 113 | } 114 | 115 | .xterm .xterm-scroll-area { 116 | visibility: hidden; 117 | } 118 | 119 | .xterm-char-measure-element { 120 | display: inline-block; 121 | visibility: hidden; 122 | position: absolute; 123 | top: 0; 124 | left: -9999em; 125 | line-height: normal; 126 | } 127 | 128 | .xterm.enable-mouse-events { 129 | /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ 130 | cursor: default; 131 | } 132 | 133 | .xterm.xterm-cursor-pointer, 134 | .xterm .xterm-cursor-pointer { 135 | cursor: pointer; 136 | } 137 | 138 | .xterm.column-select.focus { 139 | /* Column selection mode */ 140 | cursor: crosshair; 141 | } 142 | 143 | .xterm .xterm-accessibility, 144 | .xterm .xterm-message { 145 | position: absolute; 146 | left: 0; 147 | top: 0; 148 | bottom: 0; 149 | right: 0; 150 | z-index: 10; 151 | color: transparent; 152 | pointer-events: none; 153 | } 154 | 155 | .xterm .live-region { 156 | position: absolute; 157 | left: -9999px; 158 | width: 1px; 159 | height: 1px; 160 | overflow: hidden; 161 | } 162 | 163 | .xterm-dim { 164 | /* Dim should not apply to background, so the opacity of the foreground color is applied 165 | * explicitly in the generated class and reset to 1 here */ 166 | opacity: 1 !important; 167 | } 168 | 169 | .xterm-underline-1 { text-decoration: underline; } 170 | .xterm-underline-2 { text-decoration: double underline; } 171 | .xterm-underline-3 { text-decoration: wavy underline; } 172 | .xterm-underline-4 { text-decoration: dotted underline; } 173 | .xterm-underline-5 { text-decoration: dashed underline; } 174 | 175 | .xterm-overline { 176 | text-decoration: overline; 177 | } 178 | 179 | .xterm-overline.xterm-underline-1 { text-decoration: overline underline; } 180 | .xterm-overline.xterm-underline-2 { text-decoration: overline double underline; } 181 | .xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; } 182 | .xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; } 183 | .xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; } 184 | 185 | .xterm-strikethrough { 186 | text-decoration: line-through; 187 | } 188 | 189 | .xterm-screen .xterm-decoration-container .xterm-decoration { 190 | z-index: 6; 191 | position: absolute; 192 | } 193 | 194 | .xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer { 195 | z-index: 7; 196 | } 197 | 198 | .xterm-decoration-overview-ruler { 199 | z-index: 8; 200 | position: absolute; 201 | top: 0; 202 | right: 0; 203 | pointer-events: none; 204 | } 205 | 206 | .xterm-decoration-top { 207 | z-index: 2; 208 | position: relative; 209 | } 210 | -------------------------------------------------------------------------------- /cmd/incus-demo-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "math/rand" 9 | "net/http" 10 | "os" 11 | "strings" 12 | "time" 13 | 14 | "github.com/fsnotify/fsnotify" 15 | "github.com/gorilla/mux" 16 | "github.com/lxc/incus/v6/client" 17 | incusTls "github.com/lxc/incus/v6/shared/tls" 18 | "gopkg.in/yaml.v3" 19 | ) 20 | 21 | // Global variables. 22 | var ( 23 | incusDaemon incus.InstanceServer 24 | config serverConfig 25 | ) 26 | 27 | func main() { 28 | rand.Seed(time.Now().UTC().UnixNano()) 29 | err := run() 30 | if err != nil { 31 | fmt.Printf("error: %s\n", err) 32 | os.Exit(1) 33 | } 34 | } 35 | 36 | func parseConfig() error { 37 | data, err := ioutil.ReadFile("config.yaml") 38 | if os.IsNotExist(err) { 39 | return fmt.Errorf("The configuration file (config.yaml) doesn't exist.") 40 | } else if err != nil { 41 | return fmt.Errorf("Unable to read the configuration: %s", err) 42 | } 43 | 44 | err = yaml.Unmarshal(data, &config) 45 | if err != nil { 46 | return fmt.Errorf("Unable to parse the configuration: %s", err) 47 | } 48 | 49 | if config.Server.API.Address == "" { 50 | config.Server.API.Address = ":8080" 51 | } 52 | 53 | if config.Session.Command == nil { 54 | config.Session.Command = []string{"bash"} 55 | } 56 | 57 | if config.Instance.Source.InstanceType == "" { 58 | config.Instance.Source.InstanceType = "container" 59 | } 60 | 61 | config.Server.Terms = strings.TrimRight(config.Server.Terms, "\n") 62 | hash := sha256.New() 63 | io.WriteString(hash, config.Server.Terms) 64 | config.Server.termsHash = fmt.Sprintf("%x", hash.Sum(nil)) 65 | 66 | if config.Instance.Source.Instance == "" && config.Instance.Source.Image == "" { 67 | return fmt.Errorf("No instance or image specified in configuration") 68 | } 69 | 70 | if config.Instance.Source.Instance != "" && config.Instance.Source.Image != "" { 71 | return fmt.Errorf("Only one of instance or image can be specified as the source") 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func run() error { 78 | // Parse the initial configuration. 79 | err := parseConfig() 80 | if err != nil { 81 | return err 82 | } 83 | 84 | // Watch for configuration changes. 85 | watcher, err := fsnotify.NewWatcher() 86 | if err != nil { 87 | return fmt.Errorf("Unable to setup fsnotify: %s", err) 88 | } 89 | 90 | err = watcher.Add(".") 91 | if err != nil { 92 | return fmt.Errorf("Unable to setup fsnotify watch: %s", err) 93 | } 94 | 95 | go func() { 96 | for { 97 | select { 98 | case ev := <-watcher.Events: 99 | if ev.Name != "./config.yaml" { 100 | continue 101 | } 102 | 103 | if !ev.Has(fsnotify.Write) { 104 | continue 105 | } 106 | 107 | fmt.Printf("Reloading configuration\n") 108 | err := parseConfig() 109 | if err != nil { 110 | fmt.Printf("Failed to parse configuration: %s\n", err) 111 | } 112 | case err := <-watcher.Errors: 113 | fmt.Printf("Inotify error: %s\n", err) 114 | } 115 | } 116 | }() 117 | 118 | // Setup the database. 119 | err = dbSetup() 120 | if err != nil { 121 | return fmt.Errorf("Failed to setup the database: %s", err) 122 | } 123 | 124 | // Connect to the Incus daemon. 125 | go func() { 126 | warning := false 127 | for { 128 | if config.Incus.Server.URL == "" { 129 | incusDaemon, err = incus.ConnectIncusUnix("", nil) 130 | if err == nil { 131 | break 132 | } 133 | } else { 134 | // Setup connection arguments. 135 | args := &incus.ConnectionArgs{ 136 | TLSClientCert: config.Incus.Client.Certificate, 137 | TLSClientKey: config.Incus.Client.Key, 138 | TLSServerCert: config.Incus.Server.Certificate, 139 | } 140 | 141 | // Connect to the remote server. 142 | incusDaemon, err = incus.ConnectIncus(config.Incus.Server.URL, args) 143 | if err == nil { 144 | break 145 | } 146 | } 147 | 148 | if !warning { 149 | fmt.Printf("Waiting for the Incus server to come online.\n") 150 | warning = true 151 | } 152 | 153 | time.Sleep(time.Second) 154 | } 155 | 156 | if config.Incus.Project != "" { 157 | incusDaemon = incusDaemon.UseProject(config.Incus.Project) 158 | } 159 | 160 | if config.Incus.Target != "" { 161 | incusDaemon = incusDaemon.UseTarget(config.Incus.Target) 162 | } 163 | 164 | if warning { 165 | fmt.Printf("Incus is now available.\n") 166 | } 167 | 168 | // Restore cleanup handler for existing instances. 169 | instances, err := dbActive() 170 | if err != nil { 171 | fmt.Printf("Unable to read current instances: %s", err) 172 | return 173 | } 174 | 175 | for _, entry := range instances { 176 | instanceID := int64(entry[0].(int)) 177 | instanceName := entry[1].(string) 178 | instanceExpiry := int64(entry[2].(int)) 179 | 180 | duration := instanceExpiry - time.Now().Unix() 181 | timeDuration, err := time.ParseDuration(fmt.Sprintf("%ds", duration)) 182 | if err != nil || duration <= 0 { 183 | incusForceDelete(incusDaemon, instanceName) 184 | dbExpire(instanceID) 185 | continue 186 | } 187 | 188 | time.AfterFunc(timeDuration, func() { 189 | incusForceDelete(incusDaemon, instanceName) 190 | dbExpire(instanceID) 191 | }) 192 | } 193 | 194 | // Delete former pre-allocated instances. 195 | instances, err = dbAllocated() 196 | if err != nil { 197 | fmt.Printf("Unable to read pre-allocated instances: %s", err) 198 | return 199 | } 200 | 201 | for _, entry := range instances { 202 | instanceID := int64(entry[0].(int)) 203 | instanceName := entry[1].(string) 204 | 205 | incusForceDelete(incusDaemon, instanceName) 206 | dbDelete(instanceID) 207 | } 208 | 209 | // Cleanup instance list. 210 | err = instanceResync() 211 | if err != nil { 212 | fmt.Printf("Unable to perform instance cleanup: %s", err) 213 | return 214 | } 215 | 216 | // Resync instances every hour. 217 | go func() { 218 | for { 219 | time.Sleep(time.Hour) 220 | 221 | _ = instanceResync() 222 | } 223 | }() 224 | 225 | // Allocate new instances. 226 | for i := 0; i < config.Instance.Allocate.Count; i++ { 227 | err := instancePreAllocate() 228 | if err != nil { 229 | fmt.Printf("Failed to pre-allocate instance: %s", err) 230 | return 231 | } 232 | } 233 | }() 234 | 235 | // Spawn the proxy. 236 | if config.Server.Proxy.Address != "" { 237 | if config.Server.Proxy.Certificate == "" && config.Server.Proxy.Key == "" { 238 | cert, key, err := incusTls.GenerateMemCert(false, false) 239 | if err != nil { 240 | return fmt.Errorf("Failed to generate TLS certificate: %w", err) 241 | } 242 | 243 | config.Server.Proxy.Certificate = string(cert) 244 | config.Server.Proxy.Key = string(key) 245 | } 246 | 247 | go proxyListener() 248 | } 249 | 250 | // Setup the HTTP server. 251 | r := mux.NewRouter() 252 | r.Handle("/", http.RedirectHandler("/static", http.StatusMovedPermanently)) 253 | r.PathPrefix("/static").Handler(http.StripPrefix("/static", http.FileServer(http.Dir("static/")))) 254 | r.HandleFunc("/1.0", restStatusHandler) 255 | r.HandleFunc("/1.0/console", restConsoleHandler) 256 | r.HandleFunc("/1.0/feedback", restFeedbackHandler) 257 | r.HandleFunc("/1.0/info", restInfoHandler) 258 | r.HandleFunc("/1.0/start", restStartHandler) 259 | r.HandleFunc("/1.0/statistics", restStatisticsHandler) 260 | r.HandleFunc("/1.0/terms", restTermsHandler) 261 | 262 | err = http.ListenAndServe(config.Server.API.Address, r) 263 | if err != nil { 264 | return err 265 | } 266 | 267 | return nil 268 | } 269 | -------------------------------------------------------------------------------- /cmd/incus-demo-server/api_session.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "slices" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/gorilla/websocket" 13 | "github.com/lxc/incus/v6/client" 14 | "github.com/lxc/incus/v6/shared/api" 15 | ) 16 | 17 | func restStartHandler(w http.ResponseWriter, r *http.Request) { 18 | if r.Method != "GET" { 19 | http.Error(w, "Not implemented", 501) 20 | return 21 | } 22 | 23 | if config.Server.Maintenance.Enabled || incusDaemon == nil { 24 | http.Error(w, "Server in maintenance mode", 500) 25 | return 26 | } 27 | 28 | w.Header().Set("Access-Control-Allow-Origin", "*") 29 | w.Header().Set("Content-Type", "text/plain") 30 | w.Header().Set("Transfer-Encoding", "chunked") 31 | w.WriteHeader(http.StatusOK) 32 | 33 | flusher, ok := w.(http.Flusher) 34 | if !ok { 35 | http.Error(w, "Internal server error", 500) 36 | return 37 | } 38 | flusher.Flush() 39 | 40 | statusUpdate := func(msg string) { 41 | body := make(map[string]interface{}) 42 | body["message"] = msg 43 | _ = json.NewEncoder(w).Encode(body) 44 | flusher.Flush() 45 | } 46 | 47 | requestDate := time.Now().Unix() 48 | 49 | // Extract IP. 50 | requestIP, _, err := restClientIP(r) 51 | if err != nil { 52 | restStartError(w, err, instanceUnknownError) 53 | return 54 | } 55 | 56 | // Check Terms of Service. 57 | requestTerms := r.FormValue("terms") 58 | if requestTerms == "" { 59 | http.Error(w, "Missing terms hash", 400) 60 | return 61 | } 62 | 63 | if requestTerms != config.Server.termsHash { 64 | restStartError(w, nil, instanceInvalidTerms) 65 | return 66 | } 67 | 68 | // Check for banned users. 69 | if slices.Contains(config.Server.Blocklist, requestIP) { 70 | restStartError(w, nil, instanceUserBanned) 71 | return 72 | } 73 | 74 | // Count running instances. 75 | instanceCount, err := dbActiveCount() 76 | if err != nil { 77 | instanceCount = config.Server.Limits.Total 78 | } 79 | 80 | // Server is full. 81 | if instanceCount >= config.Server.Limits.Total { 82 | restStartError(w, nil, instanceServerFull) 83 | return 84 | } 85 | 86 | // Count instance for requestor IP. 87 | instanceCount, err = dbActiveCountForIP(requestIP) 88 | if err != nil { 89 | instanceCount = config.Server.Limits.IP 90 | } 91 | 92 | if config.Server.Limits.IP != 0 && instanceCount >= config.Server.Limits.IP { 93 | restStartError(w, nil, instanceQuotaReached) 94 | return 95 | } 96 | 97 | // Create the instance. 98 | var instanceID int64 99 | info := map[string]any{} 100 | instanceExpiry := time.Now().Unix() + int64(config.Session.Expiry) 101 | 102 | id, instanceUUID, instanceName, instanceIP, instanceUsername, instancePassword, err := dbGetAllocated(instanceExpiry, requestDate, requestIP, requestTerms) 103 | if err == nil { 104 | // Use a pre-created instance. 105 | instanceID = id 106 | info["id"] = instanceUUID 107 | info["name"] = instanceName 108 | info["ip"] = instanceIP 109 | info["username"] = instanceUsername 110 | info["password"] = instancePassword 111 | info["expiry"] = instanceExpiry 112 | 113 | // Create a replacement instance. 114 | go instancePreAllocate() 115 | } else { 116 | // Fallback to creating a new one. 117 | info, err = instanceCreate(false, statusUpdate) 118 | if err != nil { 119 | restStartError(w, err, instanceUnknownError) 120 | return 121 | } 122 | 123 | instanceExpiry = time.Now().Unix() + int64(config.Session.Expiry) 124 | instanceID, err = dbNew( 125 | 0, 126 | info["id"].(string), 127 | info["name"].(string), 128 | info["ip"].(string), 129 | info["username"].(string), 130 | info["password"].(string), 131 | instanceExpiry, requestDate, requestIP, requestTerms) 132 | if err != nil { 133 | incusForceDelete(incusDaemon, info["name"].(string)) 134 | restStartError(w, err, instanceUnknownError) 135 | return 136 | } 137 | 138 | info["expiry"] = instanceExpiry 139 | } 140 | 141 | // Setup cleanup code. 142 | duration, err := time.ParseDuration(fmt.Sprintf("%ds", config.Session.Expiry)) 143 | if err != nil { 144 | incusForceDelete(incusDaemon, info["name"].(string)) 145 | restStartError(w, err, instanceUnknownError) 146 | return 147 | } 148 | 149 | time.AfterFunc(duration, func() { 150 | incusForceDelete(incusDaemon, info["name"].(string)) 151 | dbExpire(instanceID) 152 | }) 153 | 154 | err = json.NewEncoder(w).Encode(info) 155 | if err != nil { 156 | incusForceDelete(incusDaemon, info["name"].(string)) 157 | restStartError(w, err, instanceUnknownError) 158 | return 159 | } 160 | 161 | flusher.Flush() 162 | return 163 | } 164 | 165 | func restInfoHandler(w http.ResponseWriter, r *http.Request) { 166 | if r.Method != "GET" { 167 | http.Error(w, "Not implemented", 501) 168 | return 169 | } 170 | 171 | if config.Server.Maintenance.Enabled || incusDaemon == nil { 172 | http.Error(w, "Server in maintenance mode", 500) 173 | return 174 | } 175 | 176 | w.Header().Set("Access-Control-Allow-Origin", "*") 177 | w.Header().Set("Content-Type", "application/json") 178 | 179 | // Get the id. 180 | id := r.FormValue("id") 181 | if id == "" { 182 | http.Error(w, "Missing session id", 400) 183 | return 184 | } 185 | 186 | // Get the instance. 187 | sessionId, instanceName, instanceIP, instanceUsername, instancePassword, instanceExpiry, err := dbGetInstance(id, false) 188 | if err != nil || sessionId == -1 { 189 | http.Error(w, "Session not found", 404) 190 | return 191 | } 192 | 193 | body := make(map[string]interface{}) 194 | 195 | if !config.Session.ConsoleOnly { 196 | body["ip"] = instanceIP 197 | body["username"] = instanceUsername 198 | body["password"] = instancePassword 199 | body["fqdn"] = fmt.Sprintf("%s.incus", instanceName) 200 | } 201 | body["id"] = id 202 | body["expiry"] = instanceExpiry 203 | 204 | // Return to the client. 205 | body["status"] = instanceStarted 206 | err = json.NewEncoder(w).Encode(body) 207 | if err != nil { 208 | incusForceDelete(incusDaemon, instanceName) 209 | http.Error(w, "Internal server error", 500) 210 | return 211 | } 212 | } 213 | 214 | func restConsoleHandler(w http.ResponseWriter, r *http.Request) { 215 | if r.Method != "GET" { 216 | http.Error(w, "Not implemented", 501) 217 | return 218 | } 219 | 220 | if config.Server.Maintenance.Enabled || incusDaemon == nil { 221 | http.Error(w, "Server in maintenance mode", 500) 222 | return 223 | } 224 | 225 | w.Header().Set("Access-Control-Allow-Origin", "*") 226 | 227 | // Get the id argument. 228 | id := r.FormValue("id") 229 | if id == "" { 230 | http.Error(w, "Missing session id", 400) 231 | return 232 | } 233 | 234 | // Get the instance. 235 | sessionId, instanceName, _, _, _, _, err := dbGetInstance(id, true) 236 | if err != nil || sessionId == -1 { 237 | http.Error(w, "Session not found", 404) 238 | return 239 | } 240 | 241 | // Get console width and height. 242 | width := r.FormValue("width") 243 | height := r.FormValue("height") 244 | 245 | if width == "" { 246 | width = "150" 247 | } 248 | 249 | if height == "" { 250 | height = "20" 251 | } 252 | 253 | widthInt, err := strconv.Atoi(width) 254 | if err != nil { 255 | http.Error(w, "Invalid width value", 400) 256 | } 257 | 258 | heightInt, err := strconv.Atoi(height) 259 | if err != nil { 260 | http.Error(w, "Invalid width value", 400) 261 | } 262 | 263 | // Setup websocket with the client. 264 | upgrader := websocket.Upgrader{ 265 | ReadBufferSize: 1024, 266 | WriteBufferSize: 1024, 267 | CheckOrigin: func(r *http.Request) bool { 268 | return true 269 | }, 270 | } 271 | 272 | conn, err := upgrader.Upgrade(w, r, nil) 273 | if err != nil { 274 | http.Error(w, "Internal server error", 500) 275 | return 276 | } 277 | defer conn.Close() 278 | 279 | // Connect to the instance. 280 | env := make(map[string]string) 281 | env["USER"] = "root" 282 | env["HOME"] = "/root" 283 | env["TERM"] = "xterm" 284 | 285 | inRead, inWrite := io.Pipe() 286 | outRead, outWrite := io.Pipe() 287 | 288 | // Data handler. 289 | connWrapper := &wsWrapper{conn: conn} 290 | go io.Copy(inWrite, connWrapper) 291 | go io.Copy(connWrapper, outRead) 292 | 293 | // Control socket handler. 294 | handler := func(conn *websocket.Conn) { 295 | for { 296 | _, _, err = conn.ReadMessage() 297 | if err != nil { 298 | break 299 | } 300 | } 301 | } 302 | 303 | // Send the exec request. 304 | req := api.InstanceExecPost{ 305 | Command: config.Session.Command, 306 | WaitForWS: true, 307 | Interactive: true, 308 | Environment: env, 309 | Width: widthInt, 310 | Height: heightInt, 311 | } 312 | 313 | execArgs := incus.InstanceExecArgs{ 314 | Stdin: inRead, 315 | Stdout: outWrite, 316 | Stderr: outWrite, 317 | Control: handler, 318 | DataDone: make(chan bool), 319 | } 320 | 321 | op, err := incusDaemon.ExecInstance(instanceName, req, &execArgs) 322 | if err != nil { 323 | return 324 | } 325 | 326 | err = op.Wait() 327 | if err != nil { 328 | return 329 | } 330 | 331 | <-execArgs.DataDone 332 | 333 | inWrite.Close() 334 | outRead.Close() 335 | } 336 | -------------------------------------------------------------------------------- /cmd/incus-demo-server/session.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | "github.com/lxc/incus/v6/client" 10 | "github.com/lxc/incus/v6/shared/api" 11 | "github.com/pborman/uuid" 12 | ) 13 | 14 | // muCreate is used to allow performing operations that require no new instances be created. 15 | var muCreate sync.RWMutex 16 | 17 | type statusCode int 18 | 19 | const ( 20 | instanceStarted statusCode = 0 21 | instanceInvalidTerms statusCode = 1 22 | instanceServerFull statusCode = 2 23 | instanceQuotaReached statusCode = 3 24 | instanceUserBanned statusCode = 4 25 | instanceUnknownError statusCode = 5 26 | ) 27 | 28 | func instanceCreate(allocate bool, statusUpdate func(string)) (map[string]any, error) { 29 | muCreate.RLock() 30 | defer muCreate.RUnlock() 31 | 32 | info := map[string]any{} 33 | 34 | // Create the instance. 35 | if statusUpdate != nil { 36 | statusUpdate("Creating the instance") 37 | } 38 | 39 | id := uuid.NewRandom().String() 40 | instanceName := fmt.Sprintf("tryit-%s", id) 41 | instanceUsername := "admin" 42 | instancePassword := uuid.NewRandom().String() 43 | 44 | if config.Instance.Source.Instance != "" { 45 | args := incus.InstanceCopyArgs{ 46 | Name: instanceName, 47 | InstanceOnly: true, 48 | } 49 | 50 | source, _, err := incusDaemon.GetInstance(config.Instance.Source.Instance) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | source.Profiles = config.Instance.Profiles 56 | 57 | // Setup volatile. 58 | for k := range source.Config { 59 | if !strings.HasPrefix(k, "volatile.") { 60 | continue 61 | } 62 | 63 | delete(source.Config, k) 64 | } 65 | source.Config["volatile.apply_template"] = "copy" 66 | 67 | rop, err := incusDaemon.CopyInstance(incusDaemon, *source, &args) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | err = rop.Wait() 73 | if err != nil { 74 | return nil, err 75 | } 76 | } else { 77 | req := api.InstancesPost{ 78 | Name: instanceName, 79 | Source: api.InstanceSource{ 80 | Type: "image", 81 | Alias: config.Instance.Source.Image, 82 | Server: "https://images.linuxcontainers.org", 83 | Protocol: "simplestreams", 84 | }, 85 | Type: api.InstanceType(config.Instance.Source.InstanceType), 86 | } 87 | req.Profiles = config.Instance.Profiles 88 | 89 | rop, err := incusDaemon.CreateInstance(req) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | err = rop.Wait() 95 | if err != nil { 96 | return nil, err 97 | } 98 | } 99 | 100 | // Configure the instance devices. 101 | if statusUpdate != nil { 102 | statusUpdate("Configuring the instance") 103 | } 104 | 105 | ct, etag, err := incusDaemon.GetInstance(instanceName) 106 | if err != nil { 107 | incusForceDelete(incusDaemon, instanceName) 108 | return nil, err 109 | } 110 | 111 | if config.Instance.Limits.Disk != "" { 112 | _, ok := ct.ExpandedDevices["root"] 113 | if ok { 114 | ct.Devices["root"] = ct.ExpandedDevices["root"] 115 | ct.Devices["root"]["size"] = config.Instance.Limits.Disk 116 | } else { 117 | ct.Devices["root"] = map[string]string{"type": "disk", "path": "/", "size": config.Instance.Limits.Disk} 118 | } 119 | } 120 | 121 | // Configure the instance. 122 | if api.InstanceType(ct.Type) == api.InstanceTypeContainer { 123 | ct.Config["security.nesting"] = "true" 124 | 125 | if config.Instance.Limits.Processes > 0 { 126 | ct.Config["limits.processes"] = fmt.Sprintf("%d", config.Instance.Limits.Processes) 127 | } 128 | } 129 | 130 | if config.Instance.Limits.CPU > 0 { 131 | ct.Config["limits.cpu"] = fmt.Sprintf("%d", config.Instance.Limits.CPU) 132 | } 133 | 134 | if config.Instance.Limits.Memory != "" { 135 | ct.Config["limits.memory"] = config.Instance.Limits.Memory 136 | } 137 | 138 | if !config.Session.ConsoleOnly { 139 | ct.Config["user.user-data"] = fmt.Sprintf(`#cloud-config 140 | ssh_pwauth: True 141 | manage_etc_hosts: True 142 | users: 143 | - name: %s 144 | groups: sudo 145 | plain_text_passwd: %s 146 | lock_passwd: False 147 | shell: /bin/bash 148 | `, instanceUsername, instancePassword) 149 | } 150 | 151 | op, err := incusDaemon.UpdateInstance(instanceName, ct.Writable(), etag) 152 | if err != nil { 153 | incusForceDelete(incusDaemon, instanceName) 154 | return nil, err 155 | } 156 | 157 | err = op.Wait() 158 | if err != nil { 159 | incusForceDelete(incusDaemon, instanceName) 160 | return nil, err 161 | } 162 | 163 | // Start the instance. 164 | if statusUpdate != nil { 165 | statusUpdate("Starting the instance") 166 | } 167 | 168 | req := api.InstanceStatePut{ 169 | Action: "start", 170 | Timeout: -1, 171 | } 172 | 173 | op, err = incusDaemon.UpdateInstanceState(instanceName, req, "") 174 | if err != nil { 175 | incusForceDelete(incusDaemon, instanceName) 176 | return nil, err 177 | } 178 | 179 | err = op.Wait() 180 | if err != nil { 181 | incusForceDelete(incusDaemon, instanceName) 182 | return nil, err 183 | } 184 | 185 | // Get the IP (30s timeout). 186 | time.Sleep(2 * time.Second) 187 | 188 | if statusUpdate != nil { 189 | statusUpdate("Waiting for console") 190 | } 191 | 192 | var instanceIP string 193 | timeout := 30 194 | for timeout != 0 { 195 | timeout-- 196 | instState, _, err := incusDaemon.GetInstanceState(instanceName) 197 | if err != nil { 198 | incusForceDelete(incusDaemon, instanceName) 199 | return nil, err 200 | } 201 | 202 | for netName, net := range instState.Network { 203 | if api.InstanceType(ct.Type) == api.InstanceTypeContainer { 204 | if netName != "eth0" { 205 | continue 206 | } 207 | } else { 208 | if netName != "enp5s0" { 209 | continue 210 | } 211 | } 212 | 213 | for _, addr := range net.Addresses { 214 | if addr.Address == "" { 215 | continue 216 | } 217 | 218 | if addr.Scope != "global" { 219 | continue 220 | } 221 | 222 | if config.Session.Network == "ipv6" && addr.Family != "inet6" { 223 | continue 224 | } 225 | 226 | if config.Session.Network == "ipv4" && addr.Family != "inet" { 227 | continue 228 | } 229 | 230 | instanceIP = addr.Address 231 | break 232 | } 233 | 234 | if instanceIP != "" { 235 | break 236 | } 237 | } 238 | 239 | if instanceIP != "" { 240 | break 241 | } 242 | 243 | time.Sleep(1 * time.Second) 244 | } 245 | 246 | // Wait for ready command (if set). 247 | if len(config.Session.ReadyCommand) > 0 { 248 | timeout := 30 249 | for timeout != 0 { 250 | time.Sleep(time.Second) 251 | timeout-- 252 | 253 | // Send the exec request. 254 | req := api.InstanceExecPost{ 255 | Command: config.Session.ReadyCommand, 256 | WaitForWS: false, 257 | Interactive: false, 258 | } 259 | 260 | op, err := incusDaemon.ExecInstance(instanceName, req, nil) 261 | if err != nil { 262 | continue 263 | } 264 | 265 | _ = op.Wait() 266 | 267 | opAPI := op.Get() 268 | if opAPI.Metadata != nil { 269 | exitStatusRaw, ok := opAPI.Metadata["return"].(float64) 270 | if ok && exitStatusRaw == 0 { 271 | break 272 | } 273 | } 274 | } 275 | } 276 | 277 | // Return to the client. 278 | info["username"] = "" 279 | info["password"] = "" 280 | info["fqdn"] = "" 281 | if !config.Session.ConsoleOnly { 282 | info["username"] = instanceUsername 283 | info["password"] = instancePassword 284 | info["fqdn"] = fmt.Sprintf("%s.incus", instanceName) 285 | } 286 | info["id"] = id 287 | info["ip"] = instanceIP 288 | info["name"] = instanceName 289 | info["status"] = instanceStarted 290 | 291 | return info, nil 292 | } 293 | 294 | func instancePreAllocate() error { 295 | var info map[string]any 296 | 297 | for { 298 | var err error 299 | 300 | // Try to create the isntance. 301 | info, err = instanceCreate(true, nil) 302 | if err == nil { 303 | break 304 | } 305 | 306 | // Retry in 30s. 307 | time.Sleep(30 * time.Second) 308 | } 309 | 310 | // Setup cleanup code. 311 | duration, err := time.ParseDuration(fmt.Sprintf("%ds", config.Instance.Allocate.Expiry)) 312 | if err != nil { 313 | incusForceDelete(incusDaemon, info["name"].(string)) 314 | return err 315 | } 316 | 317 | instanceExpiry := time.Now().Unix() + int64(config.Instance.Allocate.Expiry) 318 | instanceID, err := dbNew( 319 | 2, 320 | info["id"].(string), 321 | info["name"].(string), 322 | info["ip"].(string), 323 | info["username"].(string), 324 | info["password"].(string), 325 | instanceExpiry, 326 | 0, "", "") 327 | if err != nil { 328 | incusForceDelete(incusDaemon, info["name"].(string)) 329 | return err 330 | } 331 | 332 | time.AfterFunc(duration, func() { 333 | if dbIsAllocated(instanceID) { 334 | incusForceDelete(incusDaemon, info["name"].(string)) 335 | dbDelete(instanceID) 336 | instancePreAllocate() 337 | } 338 | }) 339 | 340 | return nil 341 | } 342 | 343 | func instanceResync() error { 344 | // Make sure no instances get spawned during cleanup. 345 | muCreate.Lock() 346 | defer muCreate.Unlock() 347 | 348 | // List all existing instances. 349 | instanceNames, err := incusDaemon.GetInstanceNames(api.InstanceTypeAny) 350 | if err != nil { 351 | return err 352 | } 353 | 354 | // Check each instance. 355 | for _, instanceName := range instanceNames { 356 | // Skip anything we didn't create. 357 | if !strings.HasPrefix(instanceName, "tryit-") { 358 | continue 359 | } 360 | 361 | // Check if we have a DB record. 362 | ok, err := dbShouldExist(instanceName) 363 | if err != nil { 364 | return err 365 | } 366 | 367 | // If not, delete the instance. 368 | if !ok { 369 | incusForceDelete(incusDaemon, instanceName) 370 | } 371 | } 372 | 373 | return nil 374 | } 375 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Incus demo server 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 54 | 55 | 74 | 75 | 79 | 80 | 98 | 99 | 108 | 109 | 118 | 119 | 147 | 148 | 202 |
203 | 204 | 205 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /cmd/incus-demo-server/db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/mattn/go-sqlite3" 10 | ) 11 | 12 | // Global variables. 13 | var db *sql.DB 14 | 15 | func dbSetup() error { 16 | var err error 17 | 18 | db, err = sql.Open("sqlite3", fmt.Sprintf("database.sqlite3?_busy_timeout=5000&_txlock=exclusive")) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | err = dbCreateTables() 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return nil 29 | } 30 | 31 | func dbCreateTables() error { 32 | _, err := db.Exec(` 33 | CREATE TABLE IF NOT EXISTS sessions ( 34 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 35 | uuid VARCHAR(36) NOT NULL, 36 | status INTEGER NOT NULL, 37 | instance_name VARCHAR(64) NOT NULL, 38 | instance_ip VARCHAR(39) NOT NULL, 39 | instance_username VARCHAR(10) NOT NULL, 40 | instance_password VARCHAR(10) NOT NULL, 41 | instance_expiry INT NOT NULL, 42 | request_date INT NOT NULL, 43 | request_ip VARCHAR(39) NOT NULL, 44 | request_terms VARCHAR(64) NOT NULL 45 | ); 46 | 47 | CREATE TABLE IF NOT EXISTS feedback ( 48 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 49 | session_id INTEGER NOT NULL, 50 | rating INTEGER, 51 | email VARCHAR(255), 52 | email_use INTEGER, 53 | feedback TEXT, 54 | FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE 55 | ); 56 | `) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func dbGetStats(period string, unique bool, network *net.IPNet) (int64, error) { 65 | var count int64 66 | 67 | // Deal with unique filter. 68 | what := "request_ip" 69 | if unique { 70 | what = "distinct request_ip" 71 | } 72 | 73 | // Deal with period filter. 74 | where := "" 75 | if period == "current" { 76 | where = "WHERE status=0" 77 | } else if period == "hour" { 78 | creation := time.Now().Add(-time.Hour).Unix() 79 | where = fmt.Sprintf("WHERE request_date > %d", creation) 80 | } else if period == "day" { 81 | creation := time.Now().Add(-time.Hour * 24).Unix() 82 | where = fmt.Sprintf("WHERE request_date > %d", creation) 83 | } else if period == "week" { 84 | creation := time.Now().Add(-time.Hour * 24 * 7).Unix() 85 | where = fmt.Sprintf("WHERE request_date > %d", creation) 86 | } else if period == "month" { 87 | creation := time.Now().Add(-time.Hour * time.Duration(24*30.5)).Unix() 88 | where = fmt.Sprintf("WHERE request_date > %d", creation) 89 | } else if period == "year" { 90 | creation := time.Now().Add(-time.Hour * time.Duration(24*365.25)).Unix() 91 | where = fmt.Sprintf("WHERE request_date > %d", creation) 92 | } 93 | 94 | if network == nil { 95 | err := db.QueryRow(fmt.Sprintf("SELECT count(%s) FROM sessions %s;", what, where)).Scan(&count) 96 | if err != nil { 97 | return -1, err 98 | } 99 | } else { 100 | outfmt := []interface{}{""} 101 | 102 | q := fmt.Sprintf("SELECT %s FROM sessions %s;", what, where) 103 | result, err := dbQueryScan(db, q, nil, outfmt) 104 | if err != nil { 105 | return -1, err 106 | } 107 | 108 | for _, ip := range result { 109 | netIp := net.ParseIP(ip[0].(string)) 110 | if netIp == nil { 111 | continue 112 | } 113 | 114 | if !network.Contains(netIp) { 115 | continue 116 | } 117 | 118 | count += 1 119 | } 120 | } 121 | 122 | return count, nil 123 | } 124 | 125 | func dbShouldExist(name string) (bool, error) { 126 | var count int64 127 | 128 | statement := `SELECT COUNT(id) FROM sessions WHERE instance_name=? AND status IN (0, 2);` 129 | err := db.QueryRow(statement, name).Scan(&count) 130 | if err != nil { 131 | return false, err 132 | } 133 | 134 | return count == 1, nil 135 | } 136 | 137 | func dbActive() ([][]interface{}, error) { 138 | q := fmt.Sprintf("SELECT id, instance_name, instance_expiry FROM sessions WHERE status=0;") 139 | var instanceID int 140 | var instanceName string 141 | var instanceExpiry int 142 | outfmt := []interface{}{instanceID, instanceName, instanceExpiry} 143 | result, err := dbQueryScan(db, q, nil, outfmt) 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | return result, nil 149 | } 150 | 151 | func dbAllocated() ([][]interface{}, error) { 152 | q := fmt.Sprintf("SELECT id, instance_name, instance_expiry FROM sessions WHERE status=2;") 153 | var instanceID int 154 | var instanceName string 155 | var instanceExpiry int 156 | outfmt := []interface{}{instanceID, instanceName, instanceExpiry} 157 | result, err := dbQueryScan(db, q, nil, outfmt) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | return result, nil 163 | } 164 | 165 | func dbGetInstance(id string, active bool) (int64, string, string, string, string, int64, error) { 166 | var sessionId int64 167 | var instanceName string 168 | var instanceIP string 169 | var instanceUsername string 170 | var instancePassword string 171 | var instanceExpiry int64 172 | var err error 173 | var rows *sql.Rows 174 | 175 | sessionId = -1 176 | 177 | if active { 178 | rows, err = dbQuery(db, "SELECT id, instance_name, instance_ip, instance_username, instance_password, instance_expiry FROM sessions WHERE status=0 AND uuid=?;", id) 179 | } else { 180 | rows, err = dbQuery(db, "SELECT id, instance_name, instance_ip, instance_username, instance_password, instance_expiry FROM sessions WHERE uuid=?;", id) 181 | } 182 | if err != nil { 183 | return -1, "", "", "", "", 0, err 184 | } 185 | 186 | defer rows.Close() 187 | 188 | for rows.Next() { 189 | rows.Scan(&sessionId, &instanceName, &instanceIP, &instanceUsername, &instancePassword, &instanceExpiry) 190 | } 191 | 192 | return sessionId, instanceName, instanceIP, instanceUsername, instancePassword, instanceExpiry, nil 193 | } 194 | 195 | func dbGetFeedback(id int64) (int64, int64, string, int64, string, error) { 196 | var feedbackId int64 197 | var rating int64 198 | var email string 199 | var emailUse int64 200 | var feedback string 201 | 202 | feedbackId = -1 203 | rating = -1 204 | emailUse = -1 205 | rows, err := dbQuery(db, "SELECT id, rating, email, email_use, feedback FROM feedback WHERE session_id=?;", id) 206 | if err != nil { 207 | return -1, -1, "", -1, "", err 208 | } 209 | 210 | defer rows.Close() 211 | 212 | for rows.Next() { 213 | rows.Scan(&feedbackId, &rating, &email, &emailUse, &feedback) 214 | } 215 | 216 | return feedbackId, rating, email, emailUse, feedback, nil 217 | } 218 | 219 | func dbNew(status int, id string, instanceName string, instanceIP string, instanceUsername string, instancePassword string, instanceExpiry int64, requestDate int64, requestIP string, requestTerms string) (int64, error) { 220 | res, err := db.Exec(` 221 | INSERT INTO sessions ( 222 | status, 223 | uuid, 224 | instance_name, 225 | instance_ip, 226 | instance_username, 227 | instance_password, 228 | instance_expiry, 229 | request_date, 230 | request_ip, 231 | request_terms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); 232 | `, status, id, instanceName, instanceIP, instanceUsername, instancePassword, instanceExpiry, requestDate, requestIP, requestTerms) 233 | if err != nil { 234 | return 0, err 235 | } 236 | 237 | instanceID, err := res.LastInsertId() 238 | if err != nil { 239 | return 0, err 240 | } 241 | 242 | return instanceID, nil 243 | } 244 | 245 | func dbRecordFeedback(id int64, feedback Feedback) error { 246 | // Get the feedback. 247 | feedbackId, _, _, _, _, err := dbGetFeedback(id) 248 | if err != nil { 249 | return err 250 | } 251 | 252 | if feedbackId == -1 { 253 | // Record new feedback. 254 | _, err := db.Exec(` 255 | INSERT INTO feedback ( 256 | session_id, 257 | rating, 258 | email, 259 | email_use, 260 | feedback) VALUES (?, ?, ?, ?, ?); 261 | `, id, feedback.Rating, feedback.Email, feedback.EmailUse, feedback.Message) 262 | if err != nil { 263 | return err 264 | } 265 | 266 | return nil 267 | } 268 | 269 | // Update existing feedback. 270 | _, err = db.Exec(` 271 | UPDATE feedback SET rating=?, email=?, email_use=?, feedback=? WHERE session_id=?; 272 | `, feedback.Rating, feedback.Email, feedback.EmailUse, feedback.Message, id) 273 | if err != nil { 274 | return err 275 | } 276 | 277 | return nil 278 | } 279 | 280 | func dbDelete(id int64) error { 281 | _, err := db.Exec("DELETE FROM sessions WHERE id=?;", id) 282 | return err 283 | } 284 | 285 | func dbExpire(id int64) error { 286 | _, err := db.Exec("UPDATE sessions SET status=1 WHERE id=?;", id) 287 | return err 288 | } 289 | 290 | func dbIsAllocated(id int64) bool { 291 | var count int 292 | 293 | statement := `SELECT COUNT(id) FROM sessions WHERE status=2 AND id=?;` 294 | err := db.QueryRow(statement, id).Scan(&count) 295 | if err != nil { 296 | return false 297 | } 298 | 299 | return count == 1 300 | } 301 | 302 | func dbGetAllocated(instanceExpiry int64, requestDate int64, requestIP string, requestTerms string) (int64, string, string, string, string, string, error) { 303 | var id int64 304 | var uuid string 305 | var instanceName string 306 | var instanceIP string 307 | var instanceUsername string 308 | var instancePassword string 309 | 310 | // Check if feature is enabled at all. 311 | if config.Instance.Allocate.Count == 0 { 312 | return 0, "", "", "", "", "", fmt.Errorf("Pre-allocated instances isn't enabled") 313 | } 314 | 315 | // Find oldest pre-allocated instance. 316 | statement := `SELECT id, uuid, instance_name, instance_ip, instance_username, instance_password FROM sessions WHERE status=2 ORDER BY instance_expiry ASC LIMIT 1;` 317 | err := db.QueryRow(statement, id).Scan(&id, &uuid, &instanceName, &instanceIP, &instanceUsername, &instancePassword) 318 | if err != nil { 319 | return 0, "", "", "", "", "", err 320 | } 321 | 322 | // No pre-allocated instances available. 323 | if id == 0 { 324 | return 0, "", "", "", "", "", fmt.Errorf("No available pre-allocated instances") 325 | } 326 | 327 | // Update the record to match the new request. 328 | _, err = db.Exec("UPDATE sessions SET status=0, instance_expiry=?, request_date=?, request_ip=?, request_terms=? WHERE id=?", instanceExpiry, requestDate, requestIP, requestTerms, id) 329 | if err != nil { 330 | return 0, "", "", "", "", "", err 331 | } 332 | 333 | return id, uuid, instanceName, instanceIP, instanceUsername, instancePassword, nil 334 | } 335 | 336 | func dbActiveCount() (int, error) { 337 | var count int 338 | 339 | statement := `SELECT count(*) FROM sessions WHERE status=0;` 340 | err := db.QueryRow(statement).Scan(&count) 341 | if err != nil { 342 | return 0, err 343 | } 344 | 345 | return count, nil 346 | } 347 | 348 | func dbActiveCountForIP(ip string) (int, error) { 349 | var count int 350 | 351 | statement := `SELECT count(*) FROM sessions WHERE status=0 AND request_ip=?;` 352 | err := db.QueryRow(statement, ip).Scan(&count) 353 | if err != nil { 354 | return 0, err 355 | } 356 | 357 | return count, nil 358 | } 359 | 360 | func dbNextExpire() (int, error) { 361 | var expire int 362 | 363 | statement := `SELECT MIN(instance_expiry) FROM sessions WHERE status=0;` 364 | err := db.QueryRow(statement).Scan(&expire) 365 | if err != nil { 366 | return 0, err 367 | } 368 | 369 | return expire, nil 370 | } 371 | 372 | func dbIsLockedError(err error) bool { 373 | if err == nil { 374 | return false 375 | } 376 | if err == sqlite3.ErrLocked || err == sqlite3.ErrBusy { 377 | return true 378 | } 379 | if err.Error() == "database is locked" { 380 | return true 381 | } 382 | return false 383 | } 384 | 385 | func dbIsNoMatchError(err error) bool { 386 | if err == nil { 387 | return false 388 | } 389 | if err.Error() == "sql: no rows in result set" { 390 | return true 391 | } 392 | return false 393 | } 394 | 395 | func dbQueryRowScan(db *sql.DB, q string, args []interface{}, outargs []interface{}) error { 396 | for { 397 | err := db.QueryRow(q, args...).Scan(outargs...) 398 | if err == nil { 399 | return nil 400 | } 401 | if dbIsNoMatchError(err) { 402 | return err 403 | } 404 | if !dbIsLockedError(err) { 405 | return err 406 | } 407 | time.Sleep(1 * time.Second) 408 | } 409 | } 410 | 411 | func dbQuery(db *sql.DB, q string, args ...interface{}) (*sql.Rows, error) { 412 | for { 413 | result, err := db.Query(q, args...) 414 | if err == nil { 415 | return result, nil 416 | } 417 | if !dbIsLockedError(err) { 418 | return nil, err 419 | } 420 | time.Sleep(1 * time.Second) 421 | } 422 | } 423 | 424 | func dbDoQueryScan(db *sql.DB, q string, args []interface{}, outargs []interface{}) ([][]interface{}, error) { 425 | rows, err := db.Query(q, args...) 426 | if err != nil { 427 | return [][]interface{}{}, err 428 | } 429 | defer rows.Close() 430 | result := [][]interface{}{} 431 | for rows.Next() { 432 | ptrargs := make([]interface{}, len(outargs)) 433 | for i := range outargs { 434 | switch t := outargs[i].(type) { 435 | case string: 436 | str := "" 437 | ptrargs[i] = &str 438 | case int: 439 | integer := 0 440 | ptrargs[i] = &integer 441 | default: 442 | return [][]interface{}{}, fmt.Errorf("Bad interface type: %s\n", t) 443 | } 444 | } 445 | err = rows.Scan(ptrargs...) 446 | if err != nil { 447 | return [][]interface{}{}, err 448 | } 449 | newargs := make([]interface{}, len(outargs)) 450 | for i := range ptrargs { 451 | switch t := outargs[i].(type) { 452 | case string: 453 | newargs[i] = *ptrargs[i].(*string) 454 | case int: 455 | newargs[i] = *ptrargs[i].(*int) 456 | default: 457 | return [][]interface{}{}, fmt.Errorf("Bad interface type: %s\n", t) 458 | } 459 | } 460 | result = append(result, newargs) 461 | } 462 | err = rows.Err() 463 | if err != nil { 464 | return [][]interface{}{}, err 465 | } 466 | return result, nil 467 | } 468 | 469 | func dbQueryScan(db *sql.DB, q string, inargs []interface{}, outfmt []interface{}) ([][]interface{}, error) { 470 | for { 471 | result, err := dbDoQueryScan(db, q, inargs, outfmt) 472 | if err == nil { 473 | return result, nil 474 | } 475 | if !dbIsLockedError(err) { 476 | return nil, err 477 | } 478 | time.Sleep(1 * time.Second) 479 | } 480 | } 481 | -------------------------------------------------------------------------------- /static/js/tryit.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var tryit_terms_hash = ""; 3 | var tryit_console = ""; 4 | var tryit_server = location.host; 5 | var tryit_server_rest = "http://" + tryit_server 6 | var tryit_server_websocket = "ws://" + tryit_server 7 | var original_url = window.location.href.split("?")[0]; 8 | var term = null 9 | var sock = null 10 | 11 | function getUrlParameter(sParam) { 12 | var sPageURL = decodeURIComponent(window.location.search.substring(1)), 13 | sURLVariables = sPageURL.split('&'), 14 | sParameterName, 15 | i; 16 | 17 | for (i = 0; i < sURLVariables.length; i++) { 18 | sParameterName = sURLVariables[i].split('='); 19 | 20 | if (sParameterName[0] === sParam) { 21 | if (sParameterName[1] === undefined) { 22 | return "" 23 | } 24 | 25 | if (sParameterName[1].substr(-1) === "/") { 26 | sParameterName[1] = sParameterName[1].substr(0, sParameterName[1].length - 1); 27 | } 28 | 29 | return sParameterName[1]; 30 | } 31 | } 32 | 33 | return "" 34 | }; 35 | 36 | function getTimeRemaining(endtime){ 37 | var current = Math.floor(new Date() / 1000); 38 | var remaining = endtime - current; 39 | 40 | if (remaining < 0) { 41 | remaining = 0; 42 | } 43 | 44 | return remaining 45 | } 46 | 47 | function initializeClock(id, endtime) { 48 | var clock = document.getElementById(id); 49 | var minutesSpan = clock.querySelector('.minutes'); 50 | var secondsSpan = clock.querySelector('.seconds'); 51 | 52 | function updateClock() { 53 | var t = getTimeRemaining(endtime); 54 | 55 | var minutes = Math.floor(t / 60); 56 | var seconds = t - minutes * 60; 57 | 58 | minutesSpan.innerHTML = ('0' + minutes).slice(-2); 59 | secondsSpan.innerHTML = ('0' + seconds).slice(-2); 60 | 61 | if(t <= 0) { 62 | clearInterval(timeinterval); 63 | window.location.href = original_url; 64 | } 65 | } 66 | 67 | updateClock(); 68 | var timeinterval = setInterval(updateClock, 1000); 69 | } 70 | 71 | function setupConsole(id) { 72 | term = new Terminal({fontSize: 12}); 73 | fitAddon = new FitAddon.FitAddon(); 74 | term.loadAddon(fitAddon); 75 | term.open(document.getElementById("tryit_console")); 76 | fitAddon.fit(); 77 | 78 | var height = term.rows; 79 | var width = term.cols; 80 | sock = new WebSocket(tryit_server_websocket + "/1.0/console?id=" + id + "&width=" + width + "&height=" + height); 81 | sock.onopen = function (e) { 82 | attachAddon = new AttachAddon.AttachAddon(sock); 83 | term.loadAddon(attachAddon); 84 | $('#tryit_console_reconnect').css("display", "none"); 85 | 86 | sock.onclose = function(msg) { 87 | term.dispose(); 88 | $('#tryit_console_reconnect').css("display", "inherit"); 89 | }; 90 | }; 91 | } 92 | 93 | function getSize(element, cell) { 94 | var wSubs = element.offsetWidth - element.clientWidth, 95 | w = element.clientWidth - wSubs, 96 | 97 | hSubs = element.offsetHeight - element.clientHeight, 98 | h = element.clientHeight - hSubs, 99 | 100 | x = cell.clientWidth / 22, 101 | y = cell.clientHeight, 102 | 103 | cols = Math.max(Math.floor(w / x), 10), 104 | rows = Math.max(Math.floor(h / y), 10), 105 | 106 | size = { 107 | cols: cols, 108 | rows: rows 109 | }; 110 | 111 | return size; 112 | } 113 | 114 | function createCell(element) { 115 | var cell = document.createElement('div'); 116 | 117 | cell.innerHTML = 'root@tryit-session:~#'; 118 | cell.id = "tryit_console_measurement"; 119 | 120 | element.appendChild(cell); 121 | 122 | return cell; 123 | } 124 | 125 | $('.tryit_goback').click(function() { 126 | window.location.href = original_url; 127 | }); 128 | 129 | function setupPre() { 130 | var pre = document.getElementsByTagName('pre'); 131 | for (var i = 0; i < pre.length; i++) { 132 | var lines = pre[i].innerHTML.split('\n') 133 | pre[i].innerHTML = "" 134 | for (var j = 0; j < lines.length; j++) { 135 | if (j > 0) { 136 | pre[i].innerHTML += '\n' 137 | } 138 | pre[i].innerHTML += '' + lines[j] + ''; 139 | } 140 | } 141 | } 142 | setupPre() 143 | 144 | $('.tryit_run').click(function() { 145 | if (!term || !sock) { 146 | return; 147 | } 148 | 149 | data = $(this).text() 150 | sock.send(data); 151 | sock.send("\n"); 152 | }); 153 | 154 | tryit_console = getUrlParameter("id"); 155 | 156 | if (tryit_console == "") { 157 | $.ajax({ 158 | url: tryit_server_rest + "/1.0", 159 | success: function(data) { 160 | if (data.session_console_only == true) { 161 | $('#tryit_ssh_row').css("display", "none"); 162 | } 163 | 164 | if (data.server_status == 1) { 165 | $('#tryit_maintenance_message').css("display", "inherit"); 166 | if (data.server_message != "") { 167 | $('#tryit_maintenance_message').text(data.server_message); 168 | } 169 | $('#tryit_status_panel').css("display", "inherit"); 170 | $('#tryit_status_panel').addClass('panel-warning'); 171 | $('#tryit_status_panel').removeClass('panel-success'); 172 | return 173 | } 174 | 175 | $('#tryit_protocol').text(data.client_protocol); 176 | $('#tryit_address').text(data.client_address); 177 | $('#tryit_count').text(data.instance_count); 178 | $('#tryit_max').text(data.instance_max); 179 | $('#tryit_online_message').css("display", "inherit"); 180 | $('#tryit_status_panel').css("display", "inherit"); 181 | 182 | $.ajax({ 183 | url: tryit_server_rest + "/1.0/terms" 184 | }).then(function(data) { 185 | tryit = data; 186 | $('#tryit_terms').html(data.terms); 187 | tryit_terms_hash = data.hash; 188 | $('#tryit_terms_panel').css("display", "inherit"); 189 | $('#tryit_start_panel').css("display", "inherit"); 190 | }); 191 | 192 | }, 193 | error: function(data) { 194 | $('#tryit_unreachable_message').css("display", "inherit"); 195 | $('#tryit_status_panel').css("display", "inherit"); 196 | $('#tryit_status_panel').addClass('panel-danger'); 197 | $('#tryit_status_panel').removeClass('panel-success'); 198 | return 199 | } 200 | }); 201 | } else { 202 | $.ajax({ 203 | url: tryit_server_rest + "/1.0/info?id=" + tryit_console, 204 | success: function(data) { 205 | if (data.status && data.status != 0) { 206 | $('#tryit_start_panel').css("display", "none"); 207 | $('#tryit_error_missing').css("display", "inherit"); 208 | $('#tryit_error_panel_access').css("display", "inherit"); 209 | $('#tryit_error_panel').css("display", "inherit"); 210 | return 211 | } 212 | 213 | $('#tryit_instance_id').text(data.id); 214 | $('#tryit_instance_ip').text(data.ip); 215 | $('#tryit_instance_fqdn').text(data.fqdn); 216 | $('#tryit_instance_username').text(data.username); 217 | $('#tryit_instance_password').text(data.password); 218 | 219 | initializeClock('tryit_clock', data.expiry); 220 | 221 | $('#tryit_status_panel').css("display", "none"); 222 | $('#tryit_start_panel').css("display", "none"); 223 | $('#tryit_info_panel').css("display", "inherit"); 224 | $('#tryit_feedback_panel').css("display", "inherit"); 225 | $('#tryit_console_panel').css("display", "inherit"); 226 | $('#tryit_examples_panel').css("display", "inherit"); 227 | $('footer.p-footer').css("display", "none"); 228 | 229 | tryit_console = data.id; 230 | window.history.pushState("", "", "?id="+tryit_console); 231 | setupConsole(tryit_console); 232 | }, 233 | error: function(data) { 234 | $('#tryit_start_panel').css("display", "none"); 235 | $('#tryit_error_missing').css("display", "inherit"); 236 | $('#tryit_error_panel_access').css("display", "inherit"); 237 | $('#tryit_error_panel').css("display", "inherit"); 238 | return 239 | } 240 | }); 241 | } 242 | 243 | $('#tryit_terms_checkbox').change(function() { 244 | if ($('#tryit_terms_checkbox').prop("checked")) { 245 | $('#tryit_accept').removeAttr("disabled"); 246 | } 247 | else { 248 | $('#tryit_accept').attr("disabled", ""); 249 | }; 250 | }); 251 | 252 | $('#tryit_accept').click(function() { 253 | $('#tryit_accept_terms').css("display", "none"); 254 | $('#tryit_terms_panel').css("display", "none"); 255 | $('#tryit_accept').css("display", "none"); 256 | $('#tryit_progress').css("display", "inherit"); 257 | 258 | var last_response_len = false; 259 | var last_response = ""; 260 | $.ajax({ 261 | url: tryit_server_rest + "/1.0/start?terms=" + tryit_terms_hash, 262 | xhrFields: { 263 | onprogress: function(e) { 264 | var this_response, response = e.currentTarget.response; 265 | if (last_response_len === false) 266 | { 267 | this_response = response; 268 | last_response_len = response.length; 269 | } 270 | else 271 | { 272 | this_response = response.substring(last_response_len); 273 | last_response_len = response.length; 274 | } 275 | last_response = this_response; 276 | 277 | data = $.parseJSON(this_response); 278 | if (data.message != "") { 279 | $('#tryit_start_status').text(data.message); 280 | } 281 | } 282 | } 283 | }).then(function(data) { 284 | data = $.parseJSON(last_response); 285 | 286 | if (data.status && data.status != 0) { 287 | if (data.status == 1) { 288 | window.location.href = original_url; 289 | return 290 | } 291 | 292 | $('#tryit_start_panel').css("display", "none"); 293 | if (data.status == 2) { 294 | $('#tryit_error_full').css("display", "inherit"); 295 | } 296 | else if (data.status == 3) { 297 | $('#tryit_error_quota').css("display", "inherit"); 298 | } 299 | else if (data.status == 4) { 300 | $('#tryit_error_banned').css("display", "inherit"); 301 | } 302 | else if (data.status == 5) { 303 | $('#tryit_error_unknown').css("display", "inherit"); 304 | } 305 | $('#tryit_error_panel_create').css("display", "inherit"); 306 | $('#tryit_error_panel').css("display", "inherit"); 307 | return 308 | } 309 | 310 | $('#tryit_instance_console').text(data.id); 311 | $('#tryit_instance_ip').text(data.ip); 312 | $('#tryit_instance_fqdn').text(data.fqdn); 313 | $('#tryit_instance_username').text(data.username); 314 | $('#tryit_instance_password').text(data.password); 315 | initializeClock('tryit_clock', data.expiry); 316 | 317 | $('#tryit_status_panel').css("display", "none"); 318 | $('#tryit_start_panel').css("display", "none"); 319 | $('#tryit_info_panel').css("display", "inherit"); 320 | $('#tryit_feedback_panel').css("display", "inherit"); 321 | $('#tryit_console_panel').css("display", "inherit"); 322 | $('#tryit_examples_panel').css("display", "inherit"); 323 | $('footer.p-footer').css("display", "none"); 324 | 325 | tryit_console = data.id; 326 | window.history.pushState("", "", "?id="+tryit_console); 327 | setupConsole(tryit_console); 328 | }); 329 | }); 330 | 331 | $('#tryit_console_reconnect').click(function() { 332 | setupConsole(tryit_console); 333 | }); 334 | 335 | $('#tryit_intro').on('shown.bs.collapse', function (e) { 336 | var offset = $('.panel.panel-default > .panel-collapse.in').offset(); 337 | if(offset) { 338 | $('html,body').animate({ 339 | scrollTop: $('.panel-collapse.in').siblings('.panel-heading').offset().top - 50 340 | }, 500); 341 | } 342 | }); 343 | 344 | $('.js-collapsable').click(function(){ 345 | $(this).toggleClass('is-hidden'); 346 | }); 347 | 348 | $('#tryit_feedback_submit').submit(function(event) { 349 | event.preventDefault(); 350 | 351 | feedbackRating = $('#feedbackRating').val(); 352 | if (feedbackRating == "") { 353 | feedbackRating = 0 354 | } 355 | 356 | feedbackEmailUse = 0 357 | if ($('#feedbackEmailUse').is(':checked')) { 358 | feedbackEmailUse = 1 359 | } 360 | 361 | data = JSON.stringify({"rating": parseInt(feedbackRating), 362 | "email": $('#feedbackEmail').val(), 363 | "email_use": feedbackEmailUse, 364 | "message": $('#feedbackText').val()}) 365 | $.ajax({url: tryit_server_rest + "/1.0/feedback?id=" + tryit_console, 366 | type: "POST", 367 | data: data, 368 | contentType: "application/json"}) 369 | $('#tryit_feedback_panel').css("display", "none"); 370 | }); 371 | }); 372 | -------------------------------------------------------------------------------- /static/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-default .badge,.btn-primary .badge,.btn-success .badge,.btn-info .badge,.btn-warning .badge,.btn-danger .badge{text-shadow:none}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-o-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#2d6ca2));background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-primary:disabled,.btn-primary[disabled]{background-color:#2d6ca2;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-o-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3071a9));background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-o-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3278b3));background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);background-repeat:repeat-x;border-color:#3278b3}.list-group-item.active .badge,.list-group-item.active:hover .badge,.list-group-item.active:focus .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= 2 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 3 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 4 | github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= 5 | github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= 6 | github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= 7 | github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= 8 | github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= 9 | github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 10 | github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= 11 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 12 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 13 | github.com/bmatcuk/doublestar/v4 v4.9.0 h1:DBvuZxjdKkRP/dr4GVV4w2fnmrk5Hxc90T51LZjv0JA= 14 | github.com/bmatcuk/doublestar/v4 v4.9.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 15 | github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= 16 | github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 17 | github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= 18 | github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 22 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 24 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 25 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 26 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 27 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 28 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 29 | github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= 30 | github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 31 | github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= 32 | github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= 33 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 34 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 35 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 36 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 37 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 38 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 39 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 40 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 41 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 42 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 43 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 44 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 45 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 46 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 47 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 48 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 49 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 50 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 51 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 52 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 53 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 54 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 55 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 56 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 57 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 58 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 59 | github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= 60 | github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= 61 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 62 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= 63 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 64 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 65 | github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= 66 | github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= 67 | github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= 68 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 69 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 70 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 71 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 72 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 73 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 74 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 75 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 76 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 77 | github.com/lxc/incus/v6 v6.14.0 h1:3PO3N7yDkDrdWjL+BVwZ3qW87MMcWbR8pp2SNQ0YGcI= 78 | github.com/lxc/incus/v6 v6.14.0/go.mod h1:IPiyygtzkbDkl3BM6zyVPHdeHglDYi4HOZH9Cwsrmfs= 79 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 80 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 81 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 82 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 83 | github.com/mattn/go-sqlite3 v1.14.29 h1:1O6nRLJKvsi1H2Sj0Hzdfojwt8GiGKm+LOfLaBFaouQ= 84 | github.com/mattn/go-sqlite3 v1.14.29/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 85 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 86 | github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= 87 | github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 88 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 89 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 90 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 91 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 92 | github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= 93 | github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= 94 | github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= 95 | github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= 96 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 97 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 98 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 99 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 100 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 101 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 102 | github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= 103 | github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 104 | github.com/opencontainers/umoci v0.5.0 h1:/cevW4k2m1CxyJhtJlzYVri1MwbD+dnYtgIbnNozIt8= 105 | github.com/opencontainers/umoci v0.5.0/go.mod h1:2+qvgmRYcFzHboDe2T+oKq3dl/QVtyzfp2Ox2afk5aA= 106 | github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= 107 | github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 108 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 109 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 110 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 111 | github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw= 112 | github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA= 113 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 114 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 115 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 116 | github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 117 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 118 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 119 | github.com/rootless-containers/proto/go-proto v0.0.0-20230421021042-4cd87ebadd67 h1:58jvc5cZ+hGKidQ4Z37/+rj9eQxRRjOOsqNEwPSZXR4= 120 | github.com/rootless-containers/proto/go-proto v0.0.0-20230421021042-4cd87ebadd67/go.mod h1:LLjEAc6zmycfeN7/1fxIphWQPjHpTt7ElqT7eVf8e4A= 121 | github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= 122 | github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 123 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 124 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 125 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 126 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 127 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 128 | github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= 129 | github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= 130 | github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= 131 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 132 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 133 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 134 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 135 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 136 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 137 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 138 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 139 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 140 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 141 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 142 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 143 | github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= 144 | github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= 145 | github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= 146 | github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc= 147 | github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= 148 | github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= 149 | github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= 150 | github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= 151 | github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= 152 | github.com/vbatts/go-mtree v0.5.4 h1:OMAb8jaCyiFA7zXj0Zc/oARcxBDBoeu2LizjB8BVJl0= 153 | github.com/vbatts/go-mtree v0.5.4/go.mod h1:5GqJbVhm9BBiCc4K5uc/c42FPgXulHaQs4sFUEfIWMo= 154 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 155 | github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU= 156 | github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4= 157 | github.com/zitadel/oidc/v3 v3.42.0 h1:cqlCYIEapmDprp5a5hUl9ivkUOu1SQxOqbrKdalHqGk= 158 | github.com/zitadel/oidc/v3 v3.42.0/go.mod h1:Y/rY7mHTzMGrZgf7REgQZFWxySlaSVqqFdBmNZq+9wA= 159 | github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= 160 | github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU= 161 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 162 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 163 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 164 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 165 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 166 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 167 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 168 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 169 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 170 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 171 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 172 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 173 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 174 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 175 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 176 | golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= 177 | golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= 178 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 179 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 180 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 181 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 182 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 183 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 184 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 185 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 186 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 187 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 188 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 189 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 190 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 191 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 192 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 193 | golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= 194 | golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 195 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 196 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 197 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 198 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 199 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 200 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 201 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 202 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 203 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 204 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 205 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 206 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 207 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 208 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 210 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 211 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 212 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 213 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 214 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 215 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 216 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 217 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 218 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 219 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 220 | golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= 221 | golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 222 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 223 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 224 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 225 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 226 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 227 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 228 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 229 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 230 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 231 | golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= 232 | golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= 233 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 234 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 235 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 236 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 237 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 238 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 239 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 240 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 241 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 242 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 243 | golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= 244 | golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= 245 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 246 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 247 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 248 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 249 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 250 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 251 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 252 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 253 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 254 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 255 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 256 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 257 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 258 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 259 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 260 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 261 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 262 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 263 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 264 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 265 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 266 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 267 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 268 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 269 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 270 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 271 | -------------------------------------------------------------------------------- /static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.0",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.0",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus","focus"==b.type)})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.0",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c="prev"==a?-1:1,d=this.getItemIndex(b),e=(d+c)%this.$items.length;return this.$items.eq(e)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i="next"==b?"first":"last",j=this;if(!f.length){if(!this.options.wrap)return;f=this.$element.find(".item")[i]()}if(f.hasClass("active"))return this.sliding=!1;var k=f[0],l=a.Event("slide.bs.carousel",{relatedTarget:k,direction:h});if(this.$element.trigger(l),!l.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var m=a(this.$indicators.children()[this.getItemIndex(f)]);m&&m.addClass("active")}var n=a.Event("slid.bs.carousel",{relatedTarget:k,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),j.sliding=!1,setTimeout(function(){j.$element.trigger(n)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(n)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a(this.options.trigger).filter('[href="#'+b.id+'"], [data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.0",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0,trigger:'[data-toggle="collapse"]'},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.find("> .panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":a.extend({},e.data(),{trigger:this});c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.0",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('