Skip to content

Commit d604e96

Browse files
committed
Share CLI credentials over a unix socket
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
1 parent 48741f7 commit d604e96

File tree

5 files changed

+264
-26
lines changed

5 files changed

+264
-26
lines changed

cli/command/auth/auth.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package auth
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/docker/cli/cli/config"
7+
"github.com/docker/cli/cli/config/server"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
func NewAuthCommand() *cobra.Command {
12+
authCmd := &cobra.Command{
13+
Use: "auth",
14+
}
15+
16+
proxyServerCmd := &cobra.Command{
17+
Use: "credential-server",
18+
RunE: func(cmd *cobra.Command, args []string) error {
19+
file := config.LoadDefaultConfigFile(cmd.ErrOrStderr())
20+
fmt.Fprint(cmd.OutOrStdout(), "Starting credential server...\n")
21+
err := server.StartCredentialsServer(cmd.Context(), config.Dir(), file)
22+
if err != nil {
23+
return err
24+
}
25+
return nil
26+
},
27+
}
28+
authCmd.AddCommand(proxyServerCmd)
29+
30+
return authCmd
31+
}

cli/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/docker/cli/cli/config/configfile"
1414
"github.com/docker/cli/cli/config/credentials"
15+
"github.com/docker/cli/cli/config/server"
1516
"github.com/docker/cli/cli/config/types"
1617
"github.com/pkg/errors"
1718
)
@@ -135,6 +136,10 @@ func load(configDir string) (*configfile.ConfigFile, error) {
135136
filename := filepath.Join(configDir, ConfigFileName)
136137
configFile := configfile.New(filename)
137138

139+
if err := server.CheckCredentialServer(configDir); err == nil {
140+
configFile.SocketCredentialStore = true
141+
}
142+
138143
file, err := os.Open(filename)
139144
if err != nil {
140145
if os.IsNotExist(err) {

cli/config/configfile/file.go

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,33 @@ import (
1616

1717
// ConfigFile ~/.docker/config.json file info
1818
type ConfigFile struct {
19-
AuthConfigs map[string]types.AuthConfig `json:"auths"`
20-
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
21-
PsFormat string `json:"psFormat,omitempty"`
22-
ImagesFormat string `json:"imagesFormat,omitempty"`
23-
NetworksFormat string `json:"networksFormat,omitempty"`
24-
PluginsFormat string `json:"pluginsFormat,omitempty"`
25-
VolumesFormat string `json:"volumesFormat,omitempty"`
26-
StatsFormat string `json:"statsFormat,omitempty"`
27-
DetachKeys string `json:"detachKeys,omitempty"`
28-
CredentialsStore string `json:"credsStore,omitempty"`
29-
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
30-
Filename string `json:"-"` // Note: for internal use only
31-
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
32-
ServicesFormat string `json:"servicesFormat,omitempty"`
33-
TasksFormat string `json:"tasksFormat,omitempty"`
34-
SecretFormat string `json:"secretFormat,omitempty"`
35-
ConfigFormat string `json:"configFormat,omitempty"`
36-
NodesFormat string `json:"nodesFormat,omitempty"`
37-
PruneFilters []string `json:"pruneFilters,omitempty"`
38-
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
39-
Experimental string `json:"experimental,omitempty"`
40-
CurrentContext string `json:"currentContext,omitempty"`
41-
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
42-
Plugins map[string]map[string]string `json:"plugins,omitempty"`
43-
Aliases map[string]string `json:"aliases,omitempty"`
44-
Features map[string]string `json:"features,omitempty"`
19+
AuthConfigs map[string]types.AuthConfig `json:"auths"`
20+
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
21+
PsFormat string `json:"psFormat,omitempty"`
22+
ImagesFormat string `json:"imagesFormat,omitempty"`
23+
NetworksFormat string `json:"networksFormat,omitempty"`
24+
PluginsFormat string `json:"pluginsFormat,omitempty"`
25+
VolumesFormat string `json:"volumesFormat,omitempty"`
26+
StatsFormat string `json:"statsFormat,omitempty"`
27+
DetachKeys string `json:"detachKeys,omitempty"`
28+
CredentialsStore string `json:"credsStore,omitempty"`
29+
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
30+
Filename string `json:"-"` // Note: for internal use only
31+
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
32+
ServicesFormat string `json:"servicesFormat,omitempty"`
33+
TasksFormat string `json:"tasksFormat,omitempty"`
34+
SecretFormat string `json:"secretFormat,omitempty"`
35+
ConfigFormat string `json:"configFormat,omitempty"`
36+
NodesFormat string `json:"nodesFormat,omitempty"`
37+
PruneFilters []string `json:"pruneFilters,omitempty"`
38+
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
39+
Experimental string `json:"experimental,omitempty"`
40+
CurrentContext string `json:"currentContext,omitempty"`
41+
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
42+
Plugins map[string]map[string]string `json:"plugins,omitempty"`
43+
Aliases map[string]string `json:"aliases,omitempty"`
44+
Features map[string]string `json:"features,omitempty"`
45+
SocketCredentialStore bool `json:"-"`
4546
}
4647

4748
// ProxyConfig contains proxy configuration settings
@@ -254,6 +255,9 @@ func decodeAuth(authStr string) (string, string, error) {
254255
// GetCredentialsStore returns a new credentials store from the settings in the
255256
// configuration file
256257
func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) credentials.Store {
258+
if configFile.SocketCredentialStore {
259+
return credentials.NewSocketStore()
260+
}
257261
if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" {
258262
return newNativeStore(configFile, helper)
259263
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package credentials
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/url"
7+
8+
"github.com/docker/cli/cli/config/types"
9+
)
10+
11+
type socketStore struct {
12+
socketAddress string
13+
client http.Client
14+
}
15+
16+
// Erase implements Store.
17+
func (s *socketStore) Erase(serverAddress string) error {
18+
panic("unimplemented")
19+
}
20+
21+
// Get implements Store.
22+
func (s *socketStore) Get(serverAddress string) (types.AuthConfig, error) {
23+
q := url.Values{"key": {serverAddress}}
24+
req, err := http.NewRequest("GET", s.socketAddress+"/credentials?"+q.Encode(), nil)
25+
if err != nil {
26+
return types.AuthConfig{}, err
27+
}
28+
resp, err := s.client.Do(req)
29+
if err != nil {
30+
return types.AuthConfig{}, err
31+
}
32+
defer resp.Body.Close()
33+
34+
var authConfig types.AuthConfig
35+
if err := json.NewDecoder(resp.Body).Decode(&authConfig); err != nil {
36+
return types.AuthConfig{}, err
37+
}
38+
return authConfig, nil
39+
}
40+
41+
// GetAll implements Store.
42+
func (s *socketStore) GetAll() (map[string]types.AuthConfig, error) {
43+
req, err := http.NewRequest("GET", s.socketAddress+"/credentials", nil)
44+
if err != nil {
45+
return nil, err
46+
}
47+
resp, err := s.client.Do(req)
48+
if err != nil {
49+
return nil, err
50+
}
51+
defer resp.Body.Close()
52+
53+
var authConfigs map[string]types.AuthConfig
54+
if err := json.NewDecoder(resp.Body).Decode(&authConfigs); err != nil {
55+
return nil, err
56+
}
57+
return authConfigs, nil
58+
}
59+
60+
// Store implements Store.
61+
func (s *socketStore) Store(authConfig types.AuthConfig) error {
62+
panic("unimplemented")
63+
}
64+
65+
func NewSocketStore() Store {
66+
return &socketStore{}
67+
}

cli/config/server/server.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package server
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net"
7+
"net/http"
8+
"path/filepath"
9+
"sync/atomic"
10+
"time"
11+
12+
"github.com/docker/cli/cli/config/types"
13+
)
14+
15+
const CredentialServerSocket = "docker_cli_credential_server"
16+
17+
// GetCredentialServerSocket returns the path to the Unix socket
18+
// configDir is the directory where the docker configuration file is stored
19+
func GetCredentialServerSocket(configDir string) string {
20+
return filepath.Join(configDir, CredentialServerSocket)
21+
}
22+
23+
type CredentialConfig interface {
24+
GetAuthConfig(serverAddress string) (types.AuthConfig, error)
25+
GetAllCredentials() (map[string]types.AuthConfig, error)
26+
}
27+
28+
func CheckCredentialServer(configDir string) error {
29+
_, err := net.Dial("unix", GetCredentialServerSocket(configDir))
30+
return err
31+
}
32+
33+
// StartCredentialsServer hosts a Unix socket server that exposes
34+
// the credentials store to the Docker CLI running in a container.
35+
func StartCredentialsServer(ctx context.Context, configDir string, config CredentialConfig) error {
36+
ctx, cancel := context.WithCancel(ctx)
37+
defer cancel()
38+
39+
l, err := net.ListenUnix("unix", &net.UnixAddr{
40+
Name: GetCredentialServerSocket(configDir),
41+
Net: "unix",
42+
})
43+
if err != nil {
44+
return err
45+
}
46+
47+
mux := http.NewServeMux()
48+
mux.HandleFunc("/credentials", func(w http.ResponseWriter, r *http.Request) {
49+
if r.Method == http.MethodGet {
50+
if key := r.URL.Query().Get("key"); key != "" {
51+
credential, err := config.GetAuthConfig(key)
52+
if err != nil {
53+
http.Error(w, err.Error(), http.StatusInternalServerError)
54+
return
55+
}
56+
if err := json.NewEncoder(w).Encode(credential); err != nil {
57+
http.Error(w, err.Error(), http.StatusInternalServerError)
58+
return
59+
}
60+
return
61+
}
62+
// Get credentials
63+
credentials, err := config.GetAllCredentials()
64+
if err != nil {
65+
http.Error(w, err.Error(), http.StatusInternalServerError)
66+
return
67+
}
68+
// Write credentials
69+
err = json.NewEncoder(w).Encode(credentials)
70+
if err != nil {
71+
http.Error(w, err.Error(), http.StatusInternalServerError)
72+
return
73+
}
74+
} else if r.Method == http.MethodPost {
75+
// Store credentials
76+
} else if r.Method == http.MethodDelete {
77+
// Erase credentials
78+
} else {
79+
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
80+
}
81+
})
82+
mux.HandleFunc("/keepalive", func(w http.ResponseWriter, r *http.Request) {
83+
if r.Method == http.MethodGet {
84+
// Keep the proxy alive
85+
} else {
86+
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
87+
}
88+
})
89+
mux.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {
90+
if r.Method == http.MethodPost {
91+
// Shutdown the proxy
92+
l.Close()
93+
} else {
94+
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
95+
}
96+
})
97+
98+
timer := time.NewTimer(10 * time.Second)
99+
activeConnections := atomic.Int32{}
100+
s := http.Server{
101+
BaseContext: func(l net.Listener) context.Context { return ctx },
102+
ReadTimeout: 5 * time.Second,
103+
WriteTimeout: 5 * time.Second,
104+
IdleTimeout: 5 * time.Second,
105+
ConnState: func(c net.Conn, cs http.ConnState) {
106+
switch cs {
107+
case http.StateActive, http.StateNew, http.StateHijacked:
108+
if activeConnections.Load() == 0 {
109+
timer.Stop()
110+
}
111+
activeConnections.Add(1)
112+
case http.StateClosed, http.StateIdle:
113+
if activeConnections.Load() == 0 {
114+
timer.Reset(10 * time.Second)
115+
}
116+
activeConnections.Add(-1)
117+
}
118+
},
119+
Handler: mux,
120+
}
121+
122+
go func() {
123+
select {
124+
case <-ctx.Done():
125+
case <-timer.C:
126+
}
127+
s.Shutdown(ctx)
128+
}()
129+
130+
return s.Serve(l)
131+
}

0 commit comments

Comments
 (0)