├── .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 | [](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 |
--------------------------------------------------------------------------------