├── .dockerignore ├── .github ├── dependabot.yml ├── renovate.json └── workflows │ ├── build.yml │ ├── check.yml │ ├── hub.yml │ └── test.yml ├── .gitignore ├── Dockerfile ├── license.md ├── readme.md └── src ├── doc.go ├── go.mod ├── go.sum └── main.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .git 3 | .github 4 | .gitignore 5 | .gitlab-ci.yml 6 | .gitmodules 7 | Dockerfile 8 | Dockerfile.archive 9 | compose.yml 10 | compose.yaml 11 | docker-compose.yml 12 | docker-compose.yaml 13 | 14 | *.md 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: docker 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | - package-ecosystem: gomod 12 | directory: /src 13 | schedule: 14 | interval: daily 15 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended", ":disableDependencyDashboard"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | paths-ignore: 9 | - '**/*.md' 10 | - '**/*.yml' 11 | - '.gitignore' 12 | - '.dockerignore' 13 | - '.github/**' 14 | - '.github/workflows/**' 15 | 16 | concurrency: 17 | group: build 18 | cancel-in-progress: false 19 | 20 | jobs: 21 | check: 22 | name: Test 23 | uses: ./.github/workflows/check.yml 24 | build: 25 | name: Build 26 | needs: check 27 | runs-on: ubuntu-latest 28 | permissions: 29 | actions: write 30 | packages: write 31 | contents: read 32 | steps: 33 | - 34 | name: Checkout 35 | uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 38 | - 39 | name: Docker metadata 40 | id: meta 41 | uses: docker/metadata-action@v5 42 | with: 43 | context: git 44 | images: | 45 | ${{ secrets.DOCKERHUB_REPO }} 46 | ghcr.io/${{ github.repository }} 47 | tags: | 48 | type=raw,value=latest,priority=100 49 | type=raw,value=${{ vars.MAJOR }}.${{ vars.MINOR }} 50 | labels: | 51 | org.opencontainers.image.title=${{ vars.NAME }} 52 | env: 53 | DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index 54 | - 55 | name: Set up Docker Buildx 56 | uses: docker/setup-buildx-action@v3 57 | - 58 | name: Login into Docker Hub 59 | uses: docker/login-action@v3 60 | with: 61 | username: ${{ secrets.DOCKERHUB_USERNAME }} 62 | password: ${{ secrets.DOCKERHUB_TOKEN }} 63 | - 64 | name: Login to GitHub Container Registry 65 | uses: docker/login-action@v3 66 | with: 67 | registry: ghcr.io 68 | username: ${{ github.actor }} 69 | password: ${{ secrets.GITHUB_TOKEN }} 70 | - 71 | name: Build Docker image 72 | uses: docker/build-push-action@v6 73 | with: 74 | context: . 75 | push: true 76 | provenance: false 77 | platforms: linux/amd64,linux/arm64 78 | tags: ${{ steps.meta.outputs.tags }} 79 | labels: ${{ steps.meta.outputs.labels }} 80 | annotations: ${{ steps.meta.outputs.annotations }} 81 | build-args: | 82 | VERSION_ARG=${{ steps.meta.outputs.version }} 83 | - 84 | name: Extract binary 85 | uses: shrink/actions-docker-extract@v3 86 | with: 87 | image: ${{ secrets.DOCKERHUB_REPO }}:latest 88 | path: "qemu-host.bin" 89 | destination: "output" 90 | - 91 | name: Create a release 92 | uses: action-pack/github-release@v2 93 | with: 94 | tag: "v${{ steps.meta.outputs.version }}" 95 | title: "v${{ steps.meta.outputs.version }}" 96 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 97 | - 98 | name: Update release 99 | uses: AButler/upload-release-assets@v3.0 100 | with: 101 | files: 'output/qemu-host.bin' 102 | release-tag: "v${{ steps.meta.outputs.version }}" 103 | repo-token: ${{ secrets.REPO_ACCESS_TOKEN }} 104 | - 105 | name: Increment version variable 106 | uses: action-pack/bump@v2 107 | with: 108 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 109 | - 110 | name: Push to Gitlab mirror 111 | uses: action-pack/gitlab-sync@v3 112 | with: 113 | url: ${{ secrets.GITLAB_URL }} 114 | token: ${{ secrets.GITLAB_TOKEN }} 115 | username: ${{ secrets.GITLAB_USERNAME }} 116 | - 117 | name: Send mail 118 | uses: action-pack/send-mail@v1 119 | with: 120 | to: ${{secrets.MAILTO}} 121 | from: Github Actions <${{secrets.MAILTO}}> 122 | connection_url: ${{secrets.MAIL_CONNECTION}} 123 | subject: Build of ${{ github.event.repository.name }} v${{ steps.meta.outputs.version }} completed 124 | body: | 125 | The build job of ${{ github.event.repository.name }} v${{ steps.meta.outputs.version }} was completed successfully! 126 | 127 | See https://github.com/${{ github.repository }}/actions for more information. 128 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | on: [workflow_call] 2 | name: "Check" 3 | permissions: {} 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 1 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@v5 16 | with: 17 | cache: false 18 | go-version-file: 'src/go.mod' 19 | 20 | - name: Run golangci-lint 21 | uses: golangci/golangci-lint-action@v6 22 | with: 23 | version: latest 24 | working-directory: src 25 | args: --out-format=colored-line-number 26 | 27 | - name: Run staticcheck 28 | uses: dominikh/staticcheck-action@v1 29 | with: 30 | version: latest 31 | install-go: false 32 | working-directory: src 33 | 34 | - name: Run Go vet 35 | run: go vet ./... 36 | working-directory: src 37 | 38 | - name: Lint Dockerfile 39 | uses: hadolint/hadolint-action@v3.1.0 40 | with: 41 | dockerfile: Dockerfile 42 | ignore: DL3008 43 | failure-threshold: warning 44 | -------------------------------------------------------------------------------- /.github/workflows/hub.yml: -------------------------------------------------------------------------------- 1 | name: Update 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - readme.md 8 | - README.md 9 | 10 | jobs: 11 | dockerHubDescription: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - 16 | name: Docker Hub Description 17 | uses: peter-evans/dockerhub-description@v4 18 | with: 19 | username: ${{ secrets.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_TOKEN }} 21 | repository: ${{ secrets.DOCKERHUB_REPO }} 22 | short-description: ${{ github.event.repository.description }} 23 | readme-filepath: ./readme.md 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | pull_request: 4 | paths: 5 | - '**/*.go' 6 | - 'Dockerfile' 7 | - '.github/workflows/test.yml' 8 | - '.github/workflows/check.yml' 9 | 10 | name: "Test" 11 | permissions: {} 12 | 13 | jobs: 14 | check: 15 | name: Test 16 | uses: ./.github/workflows/check.yml 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM golang:1.22-alpine as builder 2 | 3 | COPY src/ /src/qemu-host/ 4 | WORKDIR /src/qemu-host 5 | 6 | RUN go mod download 7 | 8 | ARG VERSION_ARG="0.0" 9 | ARG TARGETOS TARGETARCH 10 | RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -a -installsuffix cgo -ldflags "-X main.Version=$VERSION_ARG" -o /src/qemu-host/main . 11 | 12 | FROM scratch 13 | 14 | COPY --chmod=755 --from=builder /src/qemu-host/main /qemu-host.bin 15 | 16 | ENTRYPOINT ["/qemu-host.bin"] 17 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

QEMU Host
2 |
3 | 4 |
5 |
6 | 7 | [![Build]][build_url] 8 | [![Version]][tag_url] 9 | [![Size]][tag_url] 10 | [![Package]][pkg_url] 11 | [![Pulls]][hub_url] 12 | 13 |

14 | 15 | Tool for communicating with a QEMU Guest Agent daemon. 16 | 17 | It is used to exchange information between the host and guest, and to execute commands in the guest. 18 | 19 | ## Usage 🐳 20 | 21 | ##### Via Docker Compose: 22 | 23 | ```yaml 24 | services: 25 | qemu: 26 | container_name: qemu 27 | image: qemux/qemu-host 28 | ``` 29 | 30 | ##### Via Docker CLI: 31 | 32 | ```bash 33 | docker run -it --rm qemux/qemu-host 34 | ``` 35 | 36 | ### Background 📜 37 | 38 | Ultimately the QEMU Guest Agent aims to provide access to a system-level agent via standard QMP commands. 39 | 40 | This support is targeted for a future QAPI-based rework of QMP, however, so currently, for QEMU 0.15, the guest agent is exposed to the host via a separate QEMU chardev device (generally, a unix socket) that communicates with the agent using the QMP wire protocol (minus the negotiation) over a virtio-serial or isa-serial channel to the guest. Assuming the agent will be listening inside the guest using the virtio-serial device at /dev/virtio-ports/org.qemu.guest_agent.0 (the default), the corresponding host-side QEMU invocation would be something: 41 | 42 | ``` 43 | qemu \ 44 | ... 45 | -chardev socket,path=/tmp/qga.sock,server=on,wait=off,id=qga0 \ 46 | -device virtio-serial \ 47 | -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 48 | ``` 49 | 50 | Commands would be then be issued by connecting to /tmp/qga.sock, writing the QMP-formatted guest agent command, reading the QMP-formatted response, then disconnecting from the socket. (It's not strictly necessary to disconnect after a command, but should be done to allow sharing of the guest agent with multiple client when exposing it as a standalone service in this fashion. When guest agent passthrough support is added to QMP, QEMU/QMP will handle arbitration between multiple clients). 51 | 52 | When QAPI-based QMP is available (somewhere around the QEMU 0.16 timeframe), a different host-side invocation that doesn't involve access to the guest agent outside of QMP will be used. Something like: 53 | 54 | ``` 55 | qemu \ 56 | ... 57 | -chardev qga_proxy,id=qga0 \ 58 | -device virtio-serial \ 59 | -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 60 | -qmp tcp:localhost:4444,server 61 | ``` 62 | 63 | Currently this is planned to be done as a pseudo-chardev that only QEMU/QMP sees or interacts with, but the ultimate implementation may vary to some degree. The net effect should the same however: guest agent commands will be exposed in the same manner as QMP commands using the same QMP server, and communication with the agent will be handled by QEMU, transparently to the client. 64 | 65 | The current list of supported RPCs is documented in qemu.git/qapi-schema-guest.json. 66 | 67 | ## Stars 🌟 68 | [![Stars](https://starchart.cc/qemus/qemu-host.svg?variant=adaptive)](https://starchart.cc/qemus/qemu-host) 69 | 70 | [build_url]: https://github.com/qemus/qemu-host/ 71 | [hub_url]: https://hub.docker.com/r/qemux/qemu-host/ 72 | [tag_url]: https://hub.docker.com/r/qemux/qemu-host/tags 73 | [pkg_url]: https://github.com/qemus/qemu-host/pkgs/container/qemu-host 74 | 75 | [Build]: https://github.com/qemus/qemu-host/actions/workflows/build.yml/badge.svg 76 | [Size]: https://img.shields.io/docker/image-size/qemux/qemu-host/latest?color=066da5&label=size 77 | [Pulls]: https://img.shields.io/docker/pulls/qemux/qemu-host.svg?style=flat&label=pulls&logo=docker 78 | [Version]: https://img.shields.io/docker/v/qemux/qemu-host/latest?arch=amd64&sort=semver&color=066da5 79 | [Package]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fipitio.github.io%2Fbackage%2Fqemus%2Fqemu-host%2Fqemu-host.json&query=%24.downloads&logo=github&style=flat&color=066da5&label=pulls 80 | -------------------------------------------------------------------------------- /src/doc.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module qemu-host 2 | 3 | go 1.22 4 | -------------------------------------------------------------------------------- /src/go.sum: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "fmt" 7 | "log" 8 | "net" 9 | "time" 10 | "flag" 11 | "sync" 12 | "bytes" 13 | "errors" 14 | "strings" 15 | "syscall" 16 | "strconv" 17 | "os/exec" 18 | "net/http" 19 | "math/rand" 20 | "crypto/md5" 21 | "sync/atomic" 22 | "path/filepath" 23 | "encoding/binary" 24 | ) 25 | 26 | var commandsName = map[int]string{ 27 | 2: "Guest info", 28 | 3: "Guest power", 29 | 4: "Host version", 30 | 5: "Guest SN", 31 | 6: "Guest shutdown", 32 | 7: "Guest CPU info", 33 | 8: "VM version", 34 | 9: "Host version", 35 | 10: "Get Guest Info", 36 | 11: "Guest UUID", 37 | 12: "Cluster UUID", 38 | 13: "Host SN", 39 | 14: "Host MAC", 40 | 15: "Host model", 41 | 16: "Update Deadline", 42 | 17: "Guest Timestamp", 43 | } 44 | 45 | type RET struct { 46 | req REQ 47 | data string 48 | } 49 | 50 | type REQ struct { 51 | RandID int64 52 | GuestUUID[16] byte 53 | GuestID int64 54 | IsReq int32 55 | IsResp int32 56 | NeedResponse int32 57 | ReqLength int32 58 | RespLength int32 59 | CommandID int32 60 | SubCommand int32 61 | Reserve int32 62 | } 63 | 64 | const Header = 64 65 | const Packet = 4096 66 | 67 | var Version string 68 | var Chan chan RET 69 | var WaitingFor int32 70 | var Writer sync.Mutex 71 | var Connection net.Conn 72 | var Executed atomic.Bool 73 | 74 | var GuestCPUs = flag.Int("cpu", 1, "Number of CPU cores") 75 | var VmVersion = flag.String("version", "2.6.5-12202", "VM Version") 76 | var VmTimestamp = flag.Int("ts", int(time.Now().Unix()), "VM Time") 77 | var HostFixNumber = flag.Int("fixNumber", 0, "Fix number of Host") 78 | var HostBuildNumber = flag.Int("build", 69057, "Build number of Host") 79 | var HostModel = flag.String("model", "Virtualhost", "Host model name") 80 | var HostMAC = flag.String("mac", "00:00:00:00:00:00", "Host MAC address") 81 | var HostSN = flag.String("hostsn", "0000000000000", "Host serial number") 82 | var GuestSN = flag.String("guestsn", "0000000000000", "Guest serial number") 83 | var GuestCPU_Arch = flag.String("cpu_arch", "QEMU, Virtual CPU, X86_64", "CPU arch") 84 | 85 | var ApiPort = flag.String("api", ":2210", "API port") 86 | var ApiTimeout = flag.Int("timeout", 10, "Default timeout") 87 | var ListenAddr = flag.String("addr", "0.0.0.0:12345", "Listen address") 88 | 89 | func main() { 90 | 91 | flag.Parse() 92 | 93 | go http_listener(*ApiPort) 94 | 95 | listener, err := net.Listen("tcp", *ListenAddr) 96 | 97 | if err != nil { 98 | log.Println("Error listening:", err) 99 | return 100 | } 101 | 102 | defer listener.Close() 103 | 104 | fmt.Printf("Version %s started listening on %s\n", Version, *ListenAddr) 105 | 106 | for { 107 | conn, err := listener.Accept() 108 | 109 | if err != nil { 110 | log.Println("Error on accept:", err) 111 | return 112 | } 113 | 114 | fmt.Printf("New connection from %s\n", conn.RemoteAddr().String()) 115 | 116 | go incoming_conn(conn) 117 | } 118 | } 119 | 120 | func http_listener(port string) { 121 | 122 | Chan = make(chan RET, 1) 123 | 124 | router := http.NewServeMux() 125 | router.HandleFunc("/", home) 126 | router.HandleFunc("/read", read) 127 | router.HandleFunc("/write", write) 128 | 129 | err := http.ListenAndServe(port, router) 130 | 131 | if err != nil && err != http.ErrServerClosed { 132 | log.Fatalf("Error listening: %s", err) 133 | } 134 | } 135 | 136 | func incoming_conn(conn net.Conn) { 137 | 138 | defer conn.Close() 139 | Connection = conn 140 | 141 | for { 142 | buf := make([]byte, Packet) 143 | len, err := conn.Read(buf) 144 | 145 | if err != nil { 146 | if err == io.EOF || errors.Is(err, syscall.ECONNRESET) { 147 | fmt.Println("Disconnected:", err) 148 | } else { 149 | log.Println("Read error:", err) 150 | } 151 | if len != Packet { return } 152 | } 153 | 154 | if len != Packet { 155 | // Something wrong, close and wait for reconnect 156 | log.Printf("Read error: Received %d bytes, not %d\n", len, Packet) 157 | return 158 | } 159 | 160 | process_req(buf, conn) 161 | } 162 | } 163 | 164 | func process_req(buf []byte, conn net.Conn) { 165 | 166 | var req REQ 167 | 168 | err := binary.Read(bytes.NewReader(buf), binary.LittleEndian, &req) 169 | 170 | if err != nil { 171 | log.Printf("Error on decode: %s\n", err) 172 | return 173 | } 174 | 175 | var data string 176 | var title string 177 | 178 | if req.IsReq == 1 { 179 | 180 | title = "Received" 181 | data = string(buf[Header : Header+req.ReqLength]) 182 | if req.CommandID == 3 { Executed.Store(false) } 183 | 184 | } else if req.IsResp == 1 { 185 | 186 | title = "Response" 187 | data = string(buf[Header : Header+req.RespLength]) 188 | 189 | if req.CommandID == atomic.LoadInt32(&WaitingFor) && req.CommandID != 0 { 190 | atomic.StoreInt32(&WaitingFor, 0) 191 | resp := RET{ 192 | req: req, 193 | data: strings.Replace(data, "\x00", "", -1), 194 | } 195 | Chan <- resp 196 | } 197 | } 198 | 199 | fmt.Printf("%s: %s [%d] %s\n", title, commandsName[int(req.CommandID)], 200 | int(req.CommandID), strings.Replace(data, "\x00", "", -1)) 201 | 202 | // if it's a req and need a response 203 | if req.IsReq == 1 && req.NeedResponse == 1 { 204 | process_resp(req, conn) 205 | } 206 | } 207 | 208 | func process_resp(req REQ, conn net.Conn) { 209 | 210 | req.IsReq = 0 211 | req.IsResp = 1 212 | req.ReqLength = 0 213 | req.RespLength = 0 214 | req.NeedResponse = 0 215 | 216 | data := payload(req) 217 | 218 | if data != "" { 219 | req.RespLength = int32(len([]byte(data)) + 1) 220 | } else { 221 | log.Printf("No handler available for command: %d\n", req.CommandID) 222 | } 223 | 224 | fmt.Printf("Replied: %s [%d]\n", data, int(req.CommandID)) 225 | 226 | logerr(conn.Write(packet(req, data))) 227 | } 228 | 229 | func packet(req REQ, data string) []byte { 230 | 231 | buf := make([]byte, 0, Packet) 232 | writer := bytes.NewBuffer(buf) 233 | 234 | // write to buf 235 | logw(binary.Write(writer, binary.LittleEndian, &req)) 236 | if data != "" { writer.Write([]byte(data)) } 237 | 238 | // full fill 4096 239 | buf = make([]byte, Packet) 240 | copy(buf, writer.Bytes()) 241 | 242 | return buf 243 | } 244 | 245 | func payload(req REQ) string { 246 | 247 | var data string 248 | 249 | switch req.CommandID { 250 | case 4: // Host version 251 | data = fmt.Sprintf(`{"buildnumber":%d,"smallfixnumber":%d}`, 252 | *HostBuildNumber, *HostFixNumber) 253 | case 5: // Guest SN 254 | run_once() 255 | data = strings.ToUpper(*GuestSN) 256 | case 7: // CPU info 257 | data = fmt.Sprintf(`{"cpuinfo":"%s","vcpu_num":%d}`, 258 | *GuestCPU_Arch+", "+strconv.Itoa(*GuestCPUs), *GuestCPUs) 259 | case 8: // VM version 260 | data = fmt.Sprintf(`{"id":"Virtualization","name":"Virtual Machine Manager","timestamp":%d,"version":"%s"}`, 261 | *VmTimestamp, *VmVersion) 262 | case 10: // Set network 263 | data = `{"detail":[{"success":false,"type":"set_net"}]}` 264 | case 11: // Guest UUID 265 | run_once() 266 | data = uuid(guest_id()) 267 | case 12: // Cluster UUID 268 | run_once() 269 | data = uuid(host_id()) 270 | case 13: // Host SN 271 | run_once() 272 | data = strings.ToUpper(*HostSN) 273 | case 14: // Host MAC 274 | data = strings.ToLower(strings.ReplaceAll(*HostMAC, "-", ":")) 275 | case 15: // Host model 276 | data = *HostModel 277 | case 16: // Update Dead line time, always 0x7fffffffffffffff 278 | data = "9223372036854775807" 279 | } 280 | 281 | return data 282 | } 283 | 284 | func send_command(CommandID int32, SubCommand int32, needsResp int32) bool { 285 | 286 | req := REQ{ 287 | IsReq: 1, 288 | IsResp: 0, 289 | ReqLength: 0, 290 | RespLength: 0, 291 | GuestID: 10000000, 292 | RandID: rand.Int63(), 293 | GuestUUID: guest_id(), 294 | NeedResponse: needsResp, 295 | CommandID: CommandID, 296 | SubCommand: SubCommand, 297 | } 298 | 299 | //fmt.Printf("Writing command %d\n", CommandID) 300 | 301 | if Connection == nil { return false } 302 | _, err := Connection.Write(packet(req, "")) 303 | if err == nil { return true } 304 | 305 | log.Println("Write error:", err) 306 | return false 307 | } 308 | 309 | func read(w http.ResponseWriter, r *http.Request) { 310 | 311 | w.Header().Set("Content-Type", "application/json") 312 | 313 | Writer.Lock() 314 | defer Writer.Unlock() 315 | 316 | query := r.URL.Query() 317 | cmd := query.Get("command") 318 | timeout := query.Get("timeout") 319 | wait := time.Duration(*ApiTimeout) 320 | 321 | if len(strings.TrimSpace(cmd)) == 0 { 322 | fail(w, "No command specified") 323 | return 324 | } 325 | 326 | commandID, err := strconv.Atoi(cmd) 327 | 328 | if err != nil || commandID < 1 { 329 | fail(w, fmt.Sprintf("Failed to parse command: %s", cmd)) 330 | return 331 | } 332 | 333 | if len(strings.TrimSpace(timeout)) > 0 { 334 | duration, err := strconv.Atoi(timeout) 335 | if err != nil || duration < 1 { 336 | fail(w, fmt.Sprintf("Failed to parse timeout: %s", timeout)) 337 | return 338 | } 339 | wait = time.Duration(duration) 340 | } 341 | 342 | if Connection == nil || Chan == nil { 343 | fail(w, "No connection to guest") 344 | return 345 | } 346 | 347 | for len(Chan) > 0 { 348 | log.Println("Warning: channel was not empty?") 349 | <-Chan 350 | } 351 | 352 | fmt.Printf("Request: %s [%d]\n", commandsName[commandID], commandID) 353 | atomic.StoreInt32(&WaitingFor, (int32)(commandID)) 354 | 355 | if !send_command((int32)(commandID), 1, 1) { 356 | atomic.StoreInt32(&WaitingFor, 0) 357 | fail(w, fmt.Sprintf("Failed reading command %d from guest", commandID)) 358 | return 359 | } 360 | 361 | var resp RET 362 | 363 | select { 364 | case res := <-Chan: 365 | resp = res 366 | case <-time.After(wait * time.Second): 367 | atomic.StoreInt32(&WaitingFor, 0) 368 | fail(w, fmt.Sprintf("Timeout while reading command %d from guest", commandID)) 369 | return 370 | } 371 | 372 | atomic.StoreInt32(&WaitingFor, 0) 373 | 374 | if resp.req.CommandID != (int32)(commandID) { 375 | fail(w, fmt.Sprintf("Received wrong response for command %d from guest: %d", 376 | commandID, resp.req.CommandID)) 377 | return 378 | } 379 | 380 | if resp.data == "" && resp.req.CommandID != 6 { 381 | fail(w, fmt.Sprintf("Received no data for command %d", commandID)) 382 | return 383 | } 384 | 385 | ok(w, resp.data) 386 | } 387 | 388 | func write(w http.ResponseWriter, r *http.Request) { 389 | 390 | w.Header().Set("Content-Type", "application/json") 391 | 392 | Writer.Lock() 393 | defer Writer.Unlock() 394 | 395 | if Connection == nil { 396 | fail(w, "No connection to guest") 397 | return 398 | } 399 | 400 | query := r.URL.Query() 401 | cmd := query.Get("command") 402 | 403 | if len(strings.TrimSpace(cmd)) == 0 { 404 | fail(w, "No command specified") 405 | return 406 | } 407 | 408 | commandID, err := strconv.Atoi(cmd) 409 | 410 | if err != nil || commandID < 1 { 411 | fail(w, fmt.Sprintf("Failed to parse command: %s", cmd)) 412 | return 413 | } 414 | 415 | fmt.Printf("Command: %s [%d]\n", commandsName[commandID], commandID) 416 | 417 | if !send_command((int32)(commandID), 1, 0) { 418 | fail(w, fmt.Sprintf("Failed sending command %d to guest", commandID)) 419 | return 420 | } 421 | 422 | ok(w, "") 423 | } 424 | 425 | func home(w http.ResponseWriter, r *http.Request) { 426 | 427 | w.Header().Set("Content-Type", "application/json") 428 | fail(w, "No command specified") 429 | } 430 | 431 | func escape(msg string) string { 432 | 433 | msg = strings.Replace(msg, "\\", "\\\\", -1) 434 | msg = strings.Replace(msg, "\"", "\\\"", -1) 435 | msg = strings.Replace(msg, "\n", " ", -1) 436 | msg = strings.Replace(msg, "\r", " ", -1) 437 | msg = strings.Replace(msg, "\t", " ", -1) 438 | 439 | return msg 440 | } 441 | 442 | func fail(w http.ResponseWriter, msg string) { 443 | 444 | log.Println("API: " + msg) 445 | w.WriteHeader(http.StatusInternalServerError) 446 | logerr(w.Write([]byte(`{"status": "error", "message": "` + escape(msg) + `", "data": null}`))) 447 | } 448 | 449 | func ok(w http.ResponseWriter, data string) { 450 | 451 | if strings.TrimSpace(data) == "" { 452 | data = "null" 453 | } else { 454 | if !strings.HasPrefix(strings.TrimSpace(data), "{") { 455 | data = "\"" + escape(data) + "\"" 456 | } 457 | } 458 | 459 | w.WriteHeader(http.StatusOK) 460 | logerr(w.Write([]byte(`{"status": "success", "data": ` + data + `, "message": null}`))) 461 | } 462 | 463 | func logerr(n int, err error) { 464 | logw(err) 465 | } 466 | 467 | func logw(err error) { 468 | if err != nil { log.Println("Write failed:", err) } 469 | } 470 | 471 | func host_id() [16]byte { 472 | return md5.Sum([]byte("h" + strings.ToUpper(*HostSN))) 473 | } 474 | 475 | func guest_id() [16]byte { 476 | return md5.Sum([]byte("g" + strings.ToUpper(*GuestSN))) 477 | } 478 | 479 | func uuid(b [16]byte) string { 480 | return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) 481 | } 482 | 483 | func run_once() { 484 | 485 | if Executed.Load() { return } 486 | 487 | Executed.Store(true) 488 | file := path() + "/print.sh" 489 | if exists(file) { execute(file, nil) } 490 | } 491 | 492 | func path() string { 493 | 494 | exePath, err := os.Executable() 495 | if err == nil { return filepath.Dir(exePath) } 496 | 497 | log.Println("Path error:", err) 498 | return "" 499 | } 500 | 501 | func exists(name string) bool { 502 | 503 | _, err := os.Stat(name) 504 | return err == nil 505 | } 506 | 507 | func execute(script string, command []string) bool { 508 | 509 | cmd := &exec.Cmd{ 510 | Path: script, 511 | Args: command, 512 | Stdout: os.Stdout, 513 | Stderr: os.Stderr, 514 | } 515 | 516 | err := cmd.Start() 517 | if err == nil { return true } 518 | 519 | log.Println("Cannot run:", err) 520 | return false 521 | } 522 | --------------------------------------------------------------------------------