Skip to content

Commit e48b8e4

Browse files
committed
Correctly handle user cookies + support CloudLinux PHP Selector plugin
1 parent a72e076 commit e48b8e4

File tree

12 files changed

+396
-88
lines changed

12 files changed

+396
-88
lines changed

README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
# DirectAdmin Go SDK
2+
23
Interface with a DirectAdmin installation using Go.
34

4-
This library supports both the legacy/default DirectAdmin API, as well as their new modern API that's still in active development.
5+
This library supports both the legacy/default DirectAdmin API, and their new modern API still in active development.
56

6-
**Note: This is in an experimental state. While it's being used in production, the library is very likely to change (especially in-line with DA's own changes). DA features are being added as needed on our end, but PRs are always welcome!**
7+
**Note: This is in an experimental state. While it's being used in production, the library is very likely to change (
8+
especially in-line with DA's own changes). DA features are being added as needed on our end, but PRs are always welcome!
9+
**
710

8-
**If you wonder why something has been handled in an unusual way, it's most likely a workaround required by one of DA's many quirks.**
11+
**If you wonder why something has been handled unusually, it's most likely a workaround required by one of DA's many
12+
quirks.**
913

1014
## Login as Admin / Reseller / User
15+
1116
To open a session as an admin/reseller/user, follow the following code block:
1217

1318
```go
@@ -16,7 +21,6 @@ package main
1621
import (
1722
"time"
1823

19-
"github.com/goccy/go-json"
2024
"github.com/levelzerotechnology/directadmin-go"
2125
)
2226

@@ -43,20 +47,22 @@ func main() {
4347
From here, you can call user functions via `userCtx`.
4448

4549
For example, if you wanted to print each of your databases to your terminal:
50+
4651
```go
4752
dbs, err := userCtx.GetDatabases()
4853
if err != nil {
49-
log.Fatalln(err)
54+
log.Fatalln(err)
5055
}
5156

5257
for _, db := range dbs {
53-
fmt.Println(db.Name)
58+
fmt.Println(db.Name)
5459
}
5560
```
5661

5762
## Roadmap
5863

59-
- [ ] Cleanup repo structure (e.g. redis actions being within `admin.go` could go into a dedicated `redis.go` file perhaps)
64+
- [ ] Cleanup repo structure (e.g. redis actions being within `admin.go` could go into a dedicated `redis.go` file
65+
perhaps)
6066
- [ ] Explore DA's new API's update versions of old functions (e.g. user config/usage)
6167
- [ ] Implement testing for all functions
6268
- [ ] Reach stable v1.0

admin.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func (c *AdminContext) ConvertUserToReseller(username string) error {
3535
}
3636

3737
func (c *AdminContext) DisableRedis() error {
38-
var response apiGenericResponseN
38+
var response apiGenericResponseNew
3939

4040
if _, err := c.makeRequestNew(http.MethodPost, "redis/disable", nil, &response); err != nil {
4141
return err
@@ -45,7 +45,7 @@ func (c *AdminContext) DisableRedis() error {
4545
}
4646

4747
func (c *AdminContext) EnableRedis() error {
48-
var response apiGenericResponseN
48+
var response apiGenericResponseNew
4949

5050
if _, err := c.makeRequestNew(http.MethodPost, "redis/enable", nil, &response); err != nil {
5151
return err
@@ -85,7 +85,7 @@ func (c *AdminContext) MoveUserToReseller(username string, reseller string) erro
8585
}
8686

8787
func (c *AdminContext) RestartDirectAdmin() error {
88-
var response apiGenericResponseN
88+
var response apiGenericResponseNew
8989

9090
if _, err := c.makeRequestNew(http.MethodPost, "restart", nil, &response); err != nil {
9191
return err
@@ -95,7 +95,7 @@ func (c *AdminContext) RestartDirectAdmin() error {
9595
}
9696

9797
func (c *AdminContext) UpdateDirectAdmin() error {
98-
var response apiGenericResponseN
98+
var response apiGenericResponseNew
9999

100100
if _, err := c.makeRequestNew(http.MethodPost, "version/update", nil, &response); err != nil {
101101
return err

auth.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"net/http"
7+
"net/http/cookiejar"
78
"strings"
89
"time"
910
)
@@ -43,16 +44,6 @@ func (c *UserContext) CreateLoginURL(loginKeyURL *LoginKeyURL) error {
4344
return nil
4445
}
4546

46-
func (c *UserContext) GetLoginURLs() ([]*LoginKeyURL, error) {
47-
var loginKeyURLs []*LoginKeyURL
48-
49-
if _, err := c.makeRequestNew(http.MethodGet, "login-keys/urls", nil, &loginKeyURLs); err != nil {
50-
return nil, fmt.Errorf("failed to get login URLs: %w", err)
51-
}
52-
53-
return loginKeyURLs, nil
54-
}
55-
5647
func (c *AdminContext) GetLoginHistory() ([]*LoginHistory, error) {
5748
var loginHistory []*LoginHistory
5849

@@ -67,10 +58,20 @@ func (c *AdminContext) GetLoginHistory() ([]*LoginHistory, error) {
6758
return loginHistory, nil
6859
}
6960

61+
func (c *UserContext) GetLoginURLs() ([]*LoginKeyURL, error) {
62+
var loginKeyURLs []*LoginKeyURL
63+
64+
if _, err := c.makeRequestNew(http.MethodGet, "login-keys/urls", nil, &loginKeyURLs); err != nil {
65+
return nil, fmt.Errorf("failed to get login URLs: %w", err)
66+
}
67+
68+
return loginKeyURLs, nil
69+
}
70+
7071
// GetMyUsername returns the current user's username. This is particularly useful when logging in as another user, as it
7172
// trims the admin/reseller username automatically
7273
func (c *UserContext) GetMyUsername() string {
73-
// if user is logged in via reseller, we need to remove the reseller username from the context's username
74+
// If the user is logged in via reseller, we need to remove the reseller username from the context's username.
7475
if strings.Contains(c.credentials.username, "|") {
7576
return strings.Split(c.credentials.username, "|")[1]
7677
}
@@ -156,15 +157,21 @@ func (c *ResellerContext) LoginAsMyUser(username string) (*UserContext, error) {
156157
}
157158

158159
func (a *API) login(username string, passkey string) (*UserContext, error) {
160+
jar, err := cookiejar.New(nil)
161+
if err != nil {
162+
return nil, fmt.Errorf("failed to create cookie jar: %w", err)
163+
}
164+
159165
userCtx := UserContext{
160-
api: a,
166+
api: a,
167+
cookieJar: jar,
161168
credentials: credentials{
162169
username: username,
163170
passkey: passkey,
164171
},
165172
}
166173

167-
if err := userCtx.Login(); err != nil {
174+
if err = userCtx.Login(); err != nil {
168175
return nil, err
169176
}
170177

directadmin.go

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,53 +16,55 @@ const (
1616

1717
type API struct {
1818
cache struct {
19-
domains map[string]Domain // domain name is key
19+
domains map[string]Domain // Domain name is key.
2020
domainsMutex *sync.Mutex
21-
emailAccounts map[string]EmailAccount // domain name is key
21+
emailAccounts map[string]EmailAccount // Domain name is key.
2222
emailAccountsMutex *sync.Mutex
23-
packages map[string]Package // package name is key
23+
packages map[string]Package // Package name is key.
2424
packagesMutex *sync.Mutex
25-
users map[string]User // username is key
25+
users map[string]User // Username is key.
2626
usersMutex *sync.Mutex
2727
}
2828
cacheEnabled bool
2929
debug bool
3030
httpClient *http.Client
31+
parsedURL *url.URL
3132
timeout time.Duration
3233
url string
3334
}
3435

35-
type apiGenericResponse struct {
36-
Error string `json:"error,omitempty"`
37-
Result string `json:"result,omitempty"`
38-
Success string `json:"success,omitempty"`
39-
}
36+
type (
37+
apiGenericResponse struct {
38+
Error string `json:"error,omitempty"`
39+
Result string `json:"result,omitempty"`
40+
Success string `json:"success,omitempty"`
41+
}
4042

41-
type apiGenericResponseN struct {
42-
Message string `json:"message"`
43-
Type string `json:"type"`
44-
}
43+
apiGenericResponseNew struct {
44+
Message string `json:"message"`
45+
Type string `json:"type"`
46+
}
47+
)
4548

46-
// TODO: implement caching layer which can be enabled/disabled on New()
47-
// essentially, for domains it'd have map[string]Domain at the API level
48-
// then if any user called from the API, it would check the cache first
49-
// would either need a cache lifetime field added to domains, or
50-
// add an additional map for lifetime checks
49+
// TODO: implement caching layer which can be enabled/disabled on New() essentially, for domains it'd have
50+
// map[string]Domain at the API level then if any user called from the API, it would check the cache first would either
51+
// need a cache lifetime field added to domains, or add an additional map for lifetime checks.
5152

5253
func New(serverUrl string, timeout time.Duration, cacheEnabled bool, debug bool) (*API, error) {
53-
parsedUrl, err := url.ParseRequestURI(serverUrl)
54+
parsedURL, err := url.ParseRequestURI(serverUrl)
5455
if err != nil {
5556
return nil, err
5657
}
5758

58-
if parsedUrl.Host == "" {
59+
if parsedURL.Host == "" {
5960
return nil, errors.New("invalid host provided, ensure that the host is a full URL e.g. https://your-ip-address:2222")
6061
}
6162

6263
api := API{
6364
cacheEnabled: cacheEnabled,
6465
debug: debug,
65-
url: parsedUrl.String(),
66+
parsedURL: parsedURL,
67+
url: parsedURL.String(),
6668
}
6769

6870
if cacheEnabled {

domain.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type Domain struct {
2020
DiskQuota int `json:"diskQuota" yaml:"diskQuota"`
2121
DiskUsage int `json:"diskUsage" yaml:"diskUsage"`
2222
Domain string `json:"domain" yaml:"domain"`
23-
IpAddresses []string `json:"ipAddresses" yaml:"ipAddresses"`
23+
IPAddresses []string `json:"ipAddresses" yaml:"ipAddresses"`
2424
ModSecurityEnabled bool `json:"modSecurityEnabled" yaml:"modSecurityEnabled"`
2525
OpenBaseDirEnabled bool `json:"openBaseDirEnabled" yaml:"openBaseDirEnabled"`
2626
PhpEnabled bool `json:"phpEnabled" yaml:"phpEnabled"`

domain_raw.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func (d *Domain) translate() (domain rawDomain) {
4646
DiskQuota: reverseParseNum(d.DiskQuota, false),
4747
DiskUsage: reverseParseNum(d.DiskUsage, true),
4848
Domain: d.Domain,
49-
IpAddresses: d.IpAddresses,
49+
IpAddresses: d.IPAddresses,
5050
OpenBaseDirEnabled: reverseParseOnOff(d.OpenBaseDirEnabled, false),
5151
PhpEnabled: reverseParseOnOff(d.PhpEnabled, false),
5252
SafeMode: reverseParseOnOff(d.SafeMode, false),
@@ -83,7 +83,7 @@ func (d *rawDomain) translate() (domain Domain) {
8383
DiskQuota: parseNum(d.DiskQuota),
8484
DiskUsage: parseNum(d.DiskUsage),
8585
Domain: d.Domain,
86-
IpAddresses: d.IpAddresses,
86+
IPAddresses: d.IpAddresses,
8787
ModSecurityEnabled: parseOnOff(d.ExtraData.ModSecurityEnabled),
8888
OpenBaseDirEnabled: parseOnOff(d.OpenBaseDirEnabled),
8989
PhpEnabled: parseOnOff(d.PhpEnabled),

http.go

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,52 @@ type httpDebug struct {
1717
Body string
1818
BodyTruncated bool
1919
Code int
20+
Cookies []string
2021
Endpoint string
22+
Method string
2123
Start time.Time
2224
}
2325

26+
func (c *UserContext) getRequestURLNew(endpoint string) string {
27+
return fmt.Sprintf("%s/api/%s", c.api.url, endpoint)
28+
}
29+
30+
func (c *UserContext) getRequestURLOld(endpoint string) string {
31+
return fmt.Sprintf("%s/CMD_%s", c.api.url, endpoint)
32+
}
33+
2434
// makeRequest is the underlying function for HTTP requests. It handles debugging statements, and simple error handling
2535
func (c *UserContext) makeRequest(req *http.Request) ([]byte, error) {
36+
var debugCookies []string
37+
38+
cookiesToSet := c.cookieJar.Cookies(req.URL)
39+
sessionCookieSet := false
40+
for _, cookie := range cookiesToSet {
41+
req.AddCookie(cookie)
42+
43+
if cookie.Name == "csrftoken" {
44+
req.Header.Set("X-CSRFToken", cookie.Value)
45+
} else if cookie.Name == "session" {
46+
sessionCookieSet = true
47+
}
48+
49+
if c.api.debug {
50+
debugCookies = append(debugCookies, cookie.String())
51+
}
52+
}
53+
2654
debug := httpDebug{
55+
Cookies: debugCookies,
2756
Endpoint: getPathWithQuery(req),
57+
Method: req.Method,
2858
Start: time.Now(),
2959
}
3060
defer c.api.printDebugHTTP(&debug)
3161

62+
if !sessionCookieSet {
63+
req.SetBasicAuth(c.credentials.username, c.credentials.passkey)
64+
}
65+
3266
resp, err := c.api.httpClient.Do(req)
3367
if err != nil {
3468
return nil, err
@@ -39,12 +73,9 @@ func (c *UserContext) makeRequest(req *http.Request) ([]byte, error) {
3973
debug.Code = resp.StatusCode
4074
}
4175

42-
// exists solely for user session switching when logging in as a user under a reseller
76+
// Required for plugin usage in particular (session and csrf token cookies).
4377
for _, cookie := range resp.Cookies() {
44-
if cookie.Name == "session" {
45-
c.sessionID = cookie.Value
46-
break
47-
}
78+
c.cookieJar.SetCookies(req.URL, []*http.Cookie{cookie})
4879
}
4980

5081
var responseBytes []byte
@@ -85,7 +116,7 @@ func (c *UserContext) makeRequestNew(method string, endpoint string, body any, o
85116
}
86117
}
87118

88-
req, err := http.NewRequest(strings.ToUpper(method), c.api.url+"/api/"+endpoint, bytes.NewBuffer(bodyBytes))
119+
req, err := http.NewRequest(strings.ToUpper(method), c.getRequestURLNew(endpoint), bytes.NewBuffer(bodyBytes))
89120
if err != nil {
90121
return nil, fmt.Errorf("error creating request: %w", err)
91122
}
@@ -96,12 +127,6 @@ func (c *UserContext) makeRequestNew(method string, endpoint string, body any, o
96127
req.Header.Set("Referer", c.api.url)
97128
req.URL.RawQuery = query.Encode()
98129

99-
if c.sessionID != "" {
100-
req.AddCookie(&http.Cookie{Name: "session", Value: c.sessionID})
101-
} else {
102-
req.SetBasicAuth(c.credentials.username, c.credentials.passkey)
103-
}
104-
105130
resp, err := c.makeRequest(req)
106131
if err != nil {
107132
return nil, fmt.Errorf("error making request: %w", err)
@@ -118,7 +143,7 @@ func (c *UserContext) makeRequestNew(method string, endpoint string, body any, o
118143

119144
// makeRequestOld supports DirectAdmin's old API
120145
func (c *UserContext) makeRequestOld(method string, endpoint string, body url.Values, object any) ([]byte, error) {
121-
req, err := http.NewRequest(strings.ToUpper(method), c.api.url+"/CMD_"+endpoint, strings.NewReader(body.Encode()))
146+
req, err := http.NewRequest(strings.ToUpper(method), c.getRequestURLOld(endpoint), strings.NewReader(body.Encode()))
122147
if err != nil {
123148
return nil, fmt.Errorf("error creating request: %w", err)
124149
}
@@ -130,12 +155,6 @@ func (c *UserContext) makeRequestOld(method string, endpoint string, body url.Va
130155
req.Header.Set("Referer", c.api.url)
131156
req.URL.RawQuery = query.Encode()
132157

133-
if c.sessionID != "" {
134-
req.AddCookie(&http.Cookie{Name: "session", Value: c.sessionID})
135-
} else {
136-
req.SetBasicAuth(c.credentials.username, c.credentials.passkey)
137-
}
138-
139158
var genericResponse apiGenericResponse
140159

141160
resp, err := c.makeRequest(req)
@@ -172,7 +191,6 @@ func (c *UserContext) uploadFile(method string, endpoint string, data []byte, ob
172191
req.Header.Set("Accept", "application/json")
173192
req.Header.Set("Content-Type", contentType)
174193
req.Header.Set("Referer", c.api.url)
175-
req.SetBasicAuth(c.credentials.username, c.credentials.passkey)
176194
req.URL.RawQuery = query.Encode()
177195

178196
resp, err := c.makeRequest(req)
@@ -196,7 +214,7 @@ func (a *API) printDebugHTTP(debug *httpDebug) {
196214
bodyTruncated = " (truncated)"
197215
}
198216

199-
fmt.Printf("\nENDPOINT: %v\nSTATUS CODE: %v\nTIME STARTED: %v\nTIME ENDED: %v\nTIME TAKEN: %v\nBODY%s: %v\n", debug.Endpoint, debug.Code, debug.Start, time.Now(), time.Since(debug.Start), bodyTruncated, debug.Body)
217+
fmt.Printf("\nENDPOINT: %v %v\nSTATUS CODE: %v\nTIME STARTED: %v\nTIME ENDED: %v\nTIME TAKEN: %v\nCOOKIES: %s\nRESP BODY%s: %v\n", debug.Method, debug.Endpoint, debug.Code, debug.Start, time.Now(), time.Since(debug.Start), strings.Join(debug.Cookies, ";"), bodyTruncated, debug.Body)
200218
}
201219
}
202220

0 commit comments

Comments
 (0)