Skip to content

Commit 96c8eed

Browse files
CopilotAWildLeon
authored andcommitted
Add Socket Mode (Chmod)
1 parent b11c108 commit 96c8eed

File tree

5 files changed

+255
-0
lines changed

5 files changed

+255
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
/playground
44
/.idea
55
/glance*.yml
6+
glance

docs/configuration.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ Unix socket example:
290290
```yaml
291291
server:
292292
socket-path: /tmp/glance.sock
293+
socket-mode: "0666"
293294
assets-path: /home/user/glance-assets
294295
```
295296

@@ -300,6 +301,7 @@ server:
300301
| host | string | no | |
301302
| port | number | no | 8080 |
302303
| socket-path | string | no | |
304+
| socket-mode | string | no | |
303305
| proxied | boolean | no | false |
304306
| base-url | string | no | |
305307
| assets-path | string | no | |
@@ -321,6 +323,22 @@ server:
321323

322324
This is useful for running behind reverse proxies that support Unix sockets, or in containerized environments where you want to share the socket via a volume mount.
323325

326+
#### `socket-mode`
327+
File permissions to set on the Unix socket file, specified as octal permissions (e.g., "0666" or "666"). Only valid when `socket-path` is also specified. If not specified, the socket will use the default permissions set by the system.
328+
329+
Example:
330+
```yaml
331+
server:
332+
socket-path: /tmp/glance.sock
333+
socket-mode: "0666"
334+
```
335+
336+
Common values:
337+
- `"0600"` - Owner read/write only
338+
- `"0660"` - Owner and group read/write
339+
- `"0666"` - All users read/write (most permissive)
340+
- `"0664"` - Owner and group read/write, others read only
341+
324342
#### `proxied`
325343
Set to `true` if you're using a reverse proxy in front of Glance. This will make Glance use the `X-Forwarded-*` headers to determine the original request details.
326344

internal/glance/config.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"os"
1212
"path/filepath"
1313
"regexp"
14+
"strconv"
1415
"strings"
1516
"sync"
1617
"time"
@@ -32,6 +33,7 @@ type config struct {
3233
Host string `yaml:"host"`
3334
Port uint16 `yaml:"port"`
3435
SocketPath string `yaml:"socket-path"`
36+
SocketMode string `yaml:"socket-mode"`
3537
Proxied bool `yaml:"proxied"`
3638
AssetsPath string `yaml:"assets-path"`
3739
BaseURL string `yaml:"base-url"`
@@ -496,6 +498,18 @@ func isConfigStateValid(config *config) error {
496498
return fmt.Errorf("must specify either socket-path or host:port for server")
497499
}
498500

501+
// Validate socket-mode parameter
502+
if config.Server.SocketMode != "" {
503+
if !hasSocketPath {
504+
return fmt.Errorf("socket-mode can only be specified when using socket-path")
505+
}
506+
507+
// Parse and validate the socket mode as octal permissions
508+
if _, err := strconv.ParseUint(config.Server.SocketMode, 8, 32); err != nil {
509+
return fmt.Errorf("invalid socket-mode '%s': must be valid octal permissions (e.g., '0666', '666')", config.Server.SocketMode)
510+
}
511+
}
512+
499513
for i := range config.Pages {
500514
page := &config.Pages[i]
501515

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package glance
2+
3+
import (
4+
"io/fs"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
"time"
9+
)
10+
11+
func TestSocketModeValidation(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
config config
15+
expectError bool
16+
errorMsg string
17+
}{
18+
{
19+
name: "valid socket mode with socket path",
20+
config: func() config {
21+
c := config{Pages: []page{{Title: "Test", Columns: []struct {
22+
Size string `yaml:"size"`
23+
Widgets widgets `yaml:"widgets"`
24+
}{{Size: "full"}}}}}
25+
c.Server.SocketPath = "/tmp/test.sock"
26+
c.Server.SocketMode = "0666"
27+
return c
28+
}(),
29+
expectError: false,
30+
},
31+
{
32+
name: "socket mode without socket path should fail",
33+
config: func() config {
34+
c := config{Pages: []page{{Title: "Test"}}}
35+
c.Server.Host = "localhost"
36+
c.Server.Port = 8080
37+
c.Server.SocketMode = "0666"
38+
return c
39+
}(),
40+
expectError: true,
41+
errorMsg: "socket-mode can only be specified when using socket-path",
42+
},
43+
{
44+
name: "invalid socket mode should fail",
45+
config: func() config {
46+
c := config{Pages: []page{{Title: "Test"}}}
47+
c.Server.SocketPath = "/tmp/test.sock"
48+
c.Server.SocketMode = "999"
49+
return c
50+
}(),
51+
expectError: true,
52+
errorMsg: "invalid socket-mode '999': must be valid octal permissions (e.g., '0666', '666')",
53+
},
54+
{
55+
name: "non-numeric socket mode should fail",
56+
config: func() config {
57+
c := config{Pages: []page{{Title: "Test"}}}
58+
c.Server.SocketPath = "/tmp/test.sock"
59+
c.Server.SocketMode = "rwxr--r--"
60+
return c
61+
}(),
62+
expectError: true,
63+
errorMsg: "invalid socket-mode 'rwxr--r--': must be valid octal permissions (e.g., '0666', '666')",
64+
},
65+
{
66+
name: "valid three-digit socket mode",
67+
config: func() config {
68+
c := config{Pages: []page{{Title: "Test", Columns: []struct {
69+
Size string `yaml:"size"`
70+
Widgets widgets `yaml:"widgets"`
71+
}{{Size: "full"}}}}}
72+
c.Server.SocketPath = "/tmp/test.sock"
73+
c.Server.SocketMode = "666"
74+
return c
75+
}(),
76+
expectError: false,
77+
},
78+
}
79+
80+
for _, tt := range tests {
81+
t.Run(tt.name, func(t *testing.T) {
82+
err := isConfigStateValid(&tt.config)
83+
if tt.expectError {
84+
if err == nil {
85+
t.Errorf("expected error but got none")
86+
} else if tt.errorMsg != "" && err.Error() != tt.errorMsg {
87+
t.Errorf("expected error message '%s' but got '%s'", tt.errorMsg, err.Error())
88+
}
89+
} else {
90+
if err != nil {
91+
t.Errorf("expected no error but got: %v", err)
92+
}
93+
}
94+
})
95+
}
96+
}
97+
98+
func TestSocketCreationWithMode(t *testing.T) {
99+
// Create a temporary directory for test sockets
100+
tempDir, err := os.MkdirTemp("", "glance_socket_test")
101+
if err != nil {
102+
t.Fatalf("Failed to create temp dir: %v", err)
103+
}
104+
defer os.RemoveAll(tempDir)
105+
106+
socketPath := filepath.Join(tempDir, "test.sock")
107+
108+
// Create a test application with socket configuration
109+
app := &application{
110+
CreatedAt: time.Now(),
111+
Config: config{},
112+
}
113+
app.Config.Server.SocketPath = socketPath
114+
app.Config.Server.SocketMode = "0644"
115+
116+
// Test socket creation
117+
start, stop := app.server()
118+
119+
// Start the server in a goroutine
120+
done := make(chan error, 1)
121+
go func() {
122+
done <- start()
123+
}()
124+
125+
// Give the server a moment to start
126+
time.Sleep(100 * time.Millisecond)
127+
128+
// Check if socket was created
129+
if _, err := os.Stat(socketPath); os.IsNotExist(err) {
130+
t.Fatalf("Socket file was not created")
131+
}
132+
133+
// Check socket permissions
134+
info, err := os.Stat(socketPath)
135+
if err != nil {
136+
t.Fatalf("Failed to stat socket file: %v", err)
137+
}
138+
139+
expectedMode := fs.FileMode(0644)
140+
actualMode := info.Mode() & fs.ModePerm
141+
if actualMode != expectedMode {
142+
t.Errorf("Expected socket permissions %o, but got %o", expectedMode, actualMode)
143+
}
144+
145+
// Stop the server
146+
if err := stop(); err != nil {
147+
t.Errorf("Failed to stop server: %v", err)
148+
}
149+
150+
// Wait for server to stop
151+
select {
152+
case err := <-done:
153+
if err != nil && err.Error() != "http: Server closed" {
154+
t.Errorf("Server stopped with unexpected error: %v", err)
155+
}
156+
case <-time.After(5 * time.Second):
157+
t.Error("Server did not stop within timeout")
158+
}
159+
}
160+
161+
func TestSocketCreationWithoutMode(t *testing.T) {
162+
// Create a temporary directory for test sockets
163+
tempDir, err := os.MkdirTemp("", "glance_socket_test")
164+
if err != nil {
165+
t.Fatalf("Failed to create temp dir: %v", err)
166+
}
167+
defer os.RemoveAll(tempDir)
168+
169+
socketPath := filepath.Join(tempDir, "test.sock")
170+
171+
// Create a test application with socket configuration but no mode
172+
app := &application{
173+
CreatedAt: time.Now(),
174+
Config: config{},
175+
}
176+
app.Config.Server.SocketPath = socketPath
177+
// SocketMode is empty, should use default permissions
178+
179+
// Test socket creation
180+
start, stop := app.server()
181+
182+
// Start the server in a goroutine
183+
done := make(chan error, 1)
184+
go func() {
185+
done <- start()
186+
}()
187+
188+
// Give the server a moment to start
189+
time.Sleep(100 * time.Millisecond)
190+
191+
// Check if socket was created
192+
if _, err := os.Stat(socketPath); os.IsNotExist(err) {
193+
t.Fatalf("Socket file was not created")
194+
}
195+
196+
// Stop the server
197+
if err := stop(); err != nil {
198+
t.Errorf("Failed to stop server: %v", err)
199+
}
200+
201+
// Wait for server to stop
202+
select {
203+
case err := <-done:
204+
if err != nil && err.Error() != "http: Server closed" {
205+
t.Errorf("Server stopped with unexpected error: %v", err)
206+
}
207+
case <-time.After(5 * time.Second):
208+
t.Error("Server did not stop within timeout")
209+
}
210+
}

internal/glance/glance.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,18 @@ func (a *application) server() (func() error, func() error) {
510510
return fmt.Errorf("failed to listen on unix socket: %w", err)
511511
}
512512

513+
// Set socket file permissions if socket-mode is specified
514+
if a.Config.Server.SocketMode != "" {
515+
mode, err := strconv.ParseUint(a.Config.Server.SocketMode, 8, 32)
516+
if err != nil {
517+
return fmt.Errorf("failed to parse socket-mode: %w", err)
518+
}
519+
520+
if err := os.Chmod(a.Config.Server.SocketPath, os.FileMode(mode)); err != nil {
521+
return fmt.Errorf("failed to set socket permissions: %w", err)
522+
}
523+
}
524+
513525
log.Printf("Starting server on unix socket %s (base-url: \"%s\", assets-path: \"%s\")\n",
514526
a.Config.Server.SocketPath,
515527
a.Config.Server.BaseURL,

0 commit comments

Comments
 (0)