Skip to content

Commit 1110179

Browse files
author
DeDTihoN
committed
added docker_logs pages
1 parent 550b46f commit 1110179

File tree

4 files changed

+295
-35
lines changed

4 files changed

+295
-35
lines changed

main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ func main() {
4343
mux.HandleFunc("/cpu.html", pkg.MakeAttestationHTMLHandler(pkg.CPUAttestationFile, "CPU"))
4444
mux.HandleFunc("/self.html", pkg.MakeAttestationHTMLHandler(pkg.SelfAttestationFile, "Self"))
4545

46+
// Docker logs endpoints
47+
mux.HandleFunc("/docker_logs", pkg.MakeDockerLogsHandler())
48+
mux.HandleFunc("/docker_logs.html", pkg.MakeDockerLiveLogsHandler())
49+
4650
// Apply middleware chain - order matters here
4751
// First CORS, then security headers, and finally logging
4852
handler := pkg.LoggingMiddleware(

pkg/handlers.go

Lines changed: 86 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"path/filepath"
1010
"secret-vm-attest-rest-server/pkg/html"
11+
"strconv"
1112
"text/template"
1213
"time"
1314
)
@@ -99,41 +100,91 @@ func MakeAttestationHTMLHandler(fileName, attestationType string) http.HandlerFu
99100
return
100101
}
101102

102-
// Determine Title and Description per type (Self uses "Report")
103-
var titleText, descText string
104-
if attestationType == "Self" {
105-
titleText = fmt.Sprintf("%s Attestation Report", attestationType)
106-
descText = fmt.Sprintf("Below is the %s attestation report. Click the copy button to copy it to your clipboard.", attestationType)
107-
} else {
108-
titleText = fmt.Sprintf("%s Attestation Quote", attestationType)
109-
descText = fmt.Sprintf("Below is the %s attestation quote. Click the copy button to copy it to your clipboard.", attestationType)
110-
}
111-
112-
// Prepare template data
113-
data := struct {
114-
Title string
115-
Description string
116-
Quote string
117-
ShowVerify bool
118-
}{
119-
Title: titleText,
120-
Description: descText,
121-
Quote: string(content),
122-
ShowVerify: attestationType == "CPU", // only CPU shows verification link
123-
}
124-
125-
// Parse and execute HTML template
126-
tmpl, err := template.New("attestationHtml").Parse(html.HtmlTemplate)
127-
if err != nil {
128-
log.Printf("Error parsing HTML template: %v", err)
129-
http.Error(w, "Internal server error", http.StatusInternalServerError)
130-
return
131-
}
132-
w.Header().Set("Content-Type", "text/html; charset=utf-8")
133-
if err := tmpl.Execute(w, data); err != nil {
134-
log.Printf("Error executing HTML template: %v", err)
135-
http.Error(w, "Internal server error", http.StatusInternalServerError)
136-
}
103+
// Determine Title and Description per type (Self uses "Report")
104+
var titleText, descText string
105+
if attestationType == "Self" {
106+
titleText = fmt.Sprintf("%s Attestation Report", attestationType)
107+
descText = fmt.Sprintf("Below is the %s attestation report. Click the copy button to copy it to your clipboard.", attestationType)
108+
} else {
109+
titleText = fmt.Sprintf("%s Attestation Quote", attestationType)
110+
descText = fmt.Sprintf("Below is the %s attestation quote. Click the copy button to copy it to your clipboard.", attestationType)
111+
}
112+
113+
// Prepare template data
114+
data := struct {
115+
Title string
116+
Description string
117+
Quote string
118+
ShowVerify bool
119+
}{
120+
Title: titleText,
121+
Description: descText,
122+
Quote: string(content),
123+
ShowVerify: attestationType == "CPU", // only CPU shows verification link
124+
}
125+
126+
// Parse and execute HTML template
127+
tmpl, err := template.New("attestationHtml").Parse(html.HtmlTemplate)
128+
if err != nil {
129+
log.Printf("Error parsing HTML template: %v", err)
130+
http.Error(w, "Internal server error", http.StatusInternalServerError)
131+
return
132+
}
133+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
134+
if err := tmpl.Execute(w, data); err != nil {
135+
log.Printf("Error executing HTML template: %v", err)
136+
http.Error(w, "Internal server error", http.StatusInternalServerError)
137+
}
138+
}
139+
}
140+
141+
// MakeDockerLogsHandler serves plain-text Docker logs,
142+
// supporting a 'lines' query param (default 1000).
143+
func MakeDockerLogsHandler() http.HandlerFunc {
144+
return func(w http.ResponseWriter, r *http.Request) {
145+
if r.Method != http.MethodGet {
146+
respondWithError(w, http.StatusMethodNotAllowed, "Method not allowed", "Only GET requests are supported")
147+
return
148+
}
149+
lines := 1000
150+
if l := r.URL.Query().Get("lines"); l != "" {
151+
if v, err := strconv.Atoi(l); err == nil {
152+
lines = v
153+
}
154+
}
155+
logs, err := fetchDockerLogs(lines)
156+
if err != nil {
157+
log.Printf("Error fetching Docker logs: %v", err)
158+
respondWithError(w, http.StatusInternalServerError, "Failed to fetch Docker logs", err.Error())
159+
return
160+
}
161+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
162+
w.Header().Set("X-Content-Type-Options", "nosniff")
163+
w.WriteHeader(http.StatusOK)
164+
w.Write([]byte(logs))
165+
}
166+
}
167+
168+
// MakeDockerLiveLogsHandler serves the live-updating HTML page
169+
// that polls /docker_logs with a selectable line count.
170+
func MakeDockerLiveLogsHandler() http.HandlerFunc {
171+
return func(w http.ResponseWriter, r *http.Request) {
172+
if r.Method != http.MethodGet {
173+
respondWithError(w, http.StatusMethodNotAllowed, "Method not allowed", "Only GET requests are supported")
174+
return
175+
}
176+
data := struct{ Title string }{Title: "Live Docker Container Logs"}
177+
tmpl, err := template.New("liveLogs").Parse(html.DockerLiveLogsTemplate)
178+
if err != nil {
179+
log.Printf("Error parsing live logs template: %v", err)
180+
http.Error(w, "Internal server error", http.StatusInternalServerError)
181+
return
182+
}
183+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
184+
if err := tmpl.Execute(w, data); err != nil {
185+
log.Printf("Error executing live logs template: %v", err)
186+
http.Error(w, "Internal server error", http.StatusInternalServerError)
187+
}
137188
}
138189
}
139190

pkg/html/docker_live_logs.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// File: pkg/html/docker_live_logs.go
2+
package html
3+
4+
// DockerLiveLogsTemplate defines a live‑updating Docker logs page
5+
// that preserves scroll position when user scrolls up.
6+
const DockerLiveLogsTemplate = `<!DOCTYPE html>
7+
<html lang="en">
8+
<head>
9+
<meta charset="UTF-8">
10+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
11+
<title>{{.Title}}</title>
12+
<style>
13+
body {
14+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
15+
background-color: #1e1e1e;
16+
color: #e0e0e0;
17+
margin: 0;
18+
padding: 0;
19+
}
20+
.container {
21+
max-width: 1000px;
22+
margin: 0 auto;
23+
padding: 20px;
24+
}
25+
header {
26+
margin-bottom: 20px;
27+
}
28+
h1 {
29+
font-size: 28px;
30+
font-weight: 500;
31+
margin: 0 0 8px 0;
32+
color: #ffffff;
33+
}
34+
#controls {
35+
background-color: #252525;
36+
padding: 12px;
37+
border-radius: 6px;
38+
border: 1px solid #333;
39+
display: flex;
40+
align-items: center;
41+
gap: 8px;
42+
}
43+
#controls label {
44+
font-size: 14px;
45+
color: #cccccc;
46+
}
47+
#controls select {
48+
background-color: #1e1e1e;
49+
color: #e0e0e0;
50+
border: 1px solid #444;
51+
border-radius: 4px;
52+
padding: 4px 8px;
53+
font-size: 14px;
54+
}
55+
.logs-container {
56+
background-color: #252525;
57+
border-radius: 6px;
58+
border: 1px solid #333;
59+
padding: 16px;
60+
margin-top: 16px;
61+
height: 500px;
62+
overflow-y: auto;
63+
white-space: pre-wrap;
64+
word-break: break-all;
65+
font-family: 'Consolas', 'Courier New', monospace;
66+
font-size: 13px;
67+
line-height: 1.4;
68+
}
69+
.button-container {
70+
margin-top: 12px;
71+
text-align: right;
72+
}
73+
.copy-button {
74+
background-color: #2c2c2c;
75+
color: #e0e0e0;
76+
border: 1px solid #444;
77+
border-radius: 4px;
78+
padding: 8px 16px;
79+
font-size: 14px;
80+
cursor: pointer;
81+
transition: background-color 0.2s ease;
82+
}
83+
.copy-button:hover {
84+
background-color: #3a3a3a;
85+
}
86+
.toast {
87+
position: fixed;
88+
bottom: 20px;
89+
right: 20px;
90+
background-color: #333;
91+
color: #fff;
92+
padding: 12px 20px;
93+
border-radius: 4px;
94+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
95+
display: none;
96+
z-index: 1000;
97+
}
98+
.toast.show {
99+
display: block;
100+
animation: fadeInOut 2s ease;
101+
}
102+
@keyframes fadeInOut {
103+
0% { opacity: 0; }
104+
10% { opacity: 1; }
105+
90% { opacity: 1; }
106+
100% { opacity: 0; }
107+
}
108+
</style>
109+
</head>
110+
<body>
111+
<div class="container">
112+
<header>
113+
<h1>{{.Title}}</h1>
114+
<div id="controls">
115+
<label for="linesSelect">Show last</label>
116+
<select id="linesSelect">
117+
<option value="100">100</option>
118+
<option value="500">500</option>
119+
<option value="1000" selected>1000</option>
120+
</select>
121+
<label for="linesSelect">lines</label>
122+
</div>
123+
</header>
124+
<div class="logs-container" id="logs">Loading logs...</div>
125+
<div class="button-container">
126+
<button class="copy-button" id="copyButton">Copy Logs</button>
127+
</div>
128+
</div>
129+
<div class="toast" id="toast">Logs copied to clipboard</div>
130+
<script>
131+
// Fetch logs and update container, preserving scroll if user scrolled up
132+
async function fetchLogs() {
133+
const logDiv = document.getElementById('logs');
134+
// determine if user is at bottom before update
135+
const atBottom = logDiv.scrollHeight - logDiv.scrollTop === logDiv.clientHeight;
136+
const count = document.getElementById('linesSelect').value;
137+
try {
138+
const res = await fetch('/docker_logs?lines=' + count);
139+
const text = await res.text();
140+
logDiv.textContent = text;
141+
// only auto-scroll if user was at bottom
142+
if (atBottom) {
143+
logDiv.scrollTop = logDiv.scrollHeight;
144+
}
145+
} catch (e) {
146+
console.error(e);
147+
}
148+
}
149+
150+
// copy logs to clipboard
151+
document.getElementById('copyButton').addEventListener('click', () => {
152+
const text = document.getElementById('logs').textContent;
153+
navigator.clipboard.writeText(text).then(() => {
154+
const toast = document.getElementById('toast');
155+
toast.classList.add('show');
156+
setTimeout(() => toast.classList.remove('show'), 2000);
157+
}).catch(err => console.error('Copy failed', err));
158+
});
159+
160+
// re-fetch on selector change
161+
document.getElementById('linesSelect').addEventListener('change', fetchLogs);
162+
// initial fetch and interval refresh
163+
fetchLogs();
164+
setInterval(fetchLogs, 2000);
165+
</script>
166+
</body>
167+
</html>`

pkg/utils.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package pkg
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os/exec"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
// fetchDockerLogs returns the last 'lines' lines of logs from the
12+
// "secret-vm-docker" container or, if not found, the first running container.
13+
func fetchDockerLogs(lines int) (string, error) {
14+
out, err := exec.Command("docker", "ps", "-q", "--filter", "name=secret-vm-docker").Output()
15+
if err != nil {
16+
return "", err
17+
}
18+
id := strings.TrimSpace(string(out))
19+
if id == "" {
20+
out, err = exec.Command("docker", "ps", "-q").Output()
21+
if err != nil {
22+
return "", err
23+
}
24+
ids := strings.Fields(string(out))
25+
if len(ids) == 0 {
26+
return "", fmt.Errorf("no running containers")
27+
}
28+
id = ids[0]
29+
}
30+
cmd := exec.Command("docker", "logs", "--tail", strconv.Itoa(lines), id)
31+
var buf bytes.Buffer
32+
cmd.Stdout = &buf
33+
cmd.Stderr = &buf
34+
if err := cmd.Run(); err != nil {
35+
return "", err
36+
}
37+
return buf.String(), nil
38+
}

0 commit comments

Comments
 (0)